#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#ifdef CMARK
#include
#endif
#include "config.h"
#include "strlcpy.c"
#define T(S) (fputs((S), fp))
struct deltainfo {
git_patch *patch;
size_t addcount;
size_t delcount;
};
struct commitinfo {
git_commit *commit;
char oid[GIT_OID_HEXSZ + 1];
char parentoid[GIT_OID_HEXSZ + 1];
const git_signature *author;
const git_signature *committer;
const char *summary;
const char *msg;
};
struct commitstats {
size_t addcount;
size_t delcount;
size_t ndeltas;
struct deltainfo **deltas;
};
static git_repository *repo;
static const char *repodir;
/* reponame is a pointer into repodirabs */
char repodirabs[PATH_MAX + 1];
static char *reponame = "";
static char description[255];
static char url[255];
void
joinpath(char *buf, size_t bufsiz, const char *path, const char *path2)
{
int r;
r = snprintf(buf, bufsiz, "%s%s%s",
path, path[0] && path[strlen(path) - 1] != '/' ? "/" : "", path2);
if (r < 0 || (size_t)r >= bufsiz)
errx(1, "path truncated: '%s%s%s'",
path, path[0] && path[strlen(path) - 1] != '/' ? "/" : "", path2);
}
int
mkdirp(const char *path)
{
char tmp[PATH_MAX], *p;
if (strlcpy(tmp, path, sizeof(tmp)) >= sizeof(tmp))
errx(1, "path truncated: '%s'", path);
for (p = tmp + (tmp[0] == '/'); *p; p++) {
if (*p != '/')
continue;
*p = '\0';
if (mkdir(tmp, S_IRWXU | S_IRWXG | S_IRWXO) < 0 && errno != EEXIST)
return -1;
*p = '/';
}
if (mkdir(tmp, S_IRWXU | S_IRWXG | S_IRWXO) < 0 && errno != EEXIST)
return -1;
return 0;
}
FILE *
efopen(const char *name, const char *flags)
{
FILE *fp;
if (!(fp = fopen(name, flags)))
err(1, "fopen: '%s'", name);
return fp;
}
void
deltainfo_free(struct deltainfo *di)
{
if (!di)
return;
git_patch_free(di->patch);
memset(di, 0, sizeof(*di));
free(di);
}
void
commitstats_free(struct commitstats *cs)
{
size_t i;
if (!cs)
return;
for (i = 0; i < cs->ndeltas; i++)
deltainfo_free(cs->deltas[i]);
free(cs->deltas);
memset(cs, 0, sizeof(*cs));
free(cs);
}
void
commitinfo_free(struct commitinfo *ci)
{
if (!ci)
return;
git_commit_free(ci->commit);
memset(ci, 0, sizeof(*ci));
free(ci);
}
struct commitinfo *
commitinfo_getbyoid(const git_oid *id)
{
struct commitinfo *ci;
if (!(ci = calloc(1, sizeof(struct commitinfo))))
err(1, "calloc");
if (git_commit_lookup(&(ci->commit), repo, id))
goto err;
git_oid_tostr(ci->oid, sizeof(ci->oid), git_commit_id(ci->commit));
git_oid_tostr(ci->parentoid, sizeof(ci->parentoid), git_commit_parent_id(ci->commit, 0));
ci->author = git_commit_author(ci->commit);
ci->committer = git_commit_committer(ci->commit);
ci->summary = git_commit_summary(ci->commit);
ci->msg = git_commit_message(ci->commit);
return ci;
err:
commitinfo_free(ci);
return NULL;
}
int
git_commit_get_diff(git_diff **diff, git_commit *commit)
{
git_commit *parent;
git_tree *commit_tree;
git_tree *parent_tree;
git_diff_options opts;
git_diff_find_options fopts;
if (git_tree_lookup(&commit_tree, repo, git_commit_tree_id(commit)))
goto err;
if (git_commit_parent(&parent, commit, 0)) {
parent = NULL;
}
if (parent && git_tree_lookup(&parent_tree, repo, git_commit_tree_id(parent))) {
parent_tree = NULL;
}
if (git_diff_init_options(&opts, GIT_DIFF_OPTIONS_VERSION))
goto err;
opts.flags |= GIT_DIFF_DISABLE_PATHSPEC_MATCH | GIT_DIFF_INCLUDE_TYPECHANGE;
if (git_diff_tree_to_tree(diff, repo, parent_tree, commit_tree, &opts))
goto err;
git_tree_free(commit_tree);
git_tree_free(parent_tree);
git_commit_free(parent);
if (git_diff_find_init_options(&fopts, GIT_DIFF_FIND_OPTIONS_VERSION))
goto err;
fopts.flags |= GIT_DIFF_FIND_RENAMES | GIT_DIFF_FIND_COPIES;
if (git_diff_find_similar(*diff, &fopts)) {
git_diff_free(*diff);
goto err;
}
return 0;
err:
git_tree_free(commit_tree);
git_tree_free(parent_tree);
git_commit_free(parent);
return -1;
}
struct commitstats *
commitinfo_getstats(struct commitinfo *ci)
{
struct commitstats *cs;
struct deltainfo *di;
const git_diff_delta *delta;
const git_diff_hunk *hunk;
const git_diff_line *line;
git_diff *diff;
git_patch *patch = NULL;
size_t ndeltas, nhunks, nhunklines, i, j, k;
if (!(cs = calloc(1, sizeof(struct commitstats))))
err(1, "calloc");
if (git_commit_get_diff(&diff, ci->commit))
goto err;
ndeltas = git_diff_num_deltas(diff);
if (ndeltas && !(cs->deltas = calloc(ndeltas, sizeof(struct deltainfo *))))
err(1, "calloc");
for (i = 0; i < ndeltas; i++) {
if (git_patch_from_diff(&patch, diff, i))
break;
if (!(di = calloc(1, sizeof(struct deltainfo))))
err(1, "calloc");
di->patch = patch;
cs->deltas[i] = di;
delta = git_patch_get_delta(patch);
/* skip stats for binary data */
if (delta->flags & GIT_DIFF_FLAG_BINARY)
continue;
nhunks = git_patch_num_hunks(patch);
for (j = 0; j < nhunks; j++) {
if (git_patch_get_hunk(&hunk, &nhunklines, patch, j))
break;
for (k = 0; ; k++) {
if (git_patch_get_line_in_hunk(&line, patch, j, k))
break;
if (line->old_lineno == -1) {
di->addcount++;
cs->addcount++;
} else if (line->new_lineno == -1) {
di->delcount++;
cs->delcount++;
}
}
}
}
cs->ndeltas = i;
git_diff_free(diff);
err:
return cs;
}
void
xmlencode(FILE *fp, const char *s, size_t len)
{
size_t i;
for (i = 0; *s && i < len; s++, i++) {
switch(*s) {
case '<': fputs("<", fp); break;
case '>': fputs(">", fp); break;
case '\'': fputs("'", fp); break;
case '&': fputs("&", fp); break;
case '"': fputs(""", fp); break;
default: fputc(*s, fp);
}
}
}
void
printtimez(FILE *fp, const git_time *intime)
{
struct tm *intm;
time_t t;
char out[32];
t = (time_t)intime->time;
if (!(intm = gmtime(&t)))
return;
strftime(out, sizeof(out), "%Y-%m-%dT%H:%M:%SZ", intm);
fputs(out, fp);
}
void
printtimeshort(FILE *fp, const git_time *intime)
{
struct tm *intm;
time_t t;
char out[32];
t = (time_t)intime->time;
if (!(intm = gmtime(&t)))
return;
strftime(out, sizeof(out), "%Y-%m-%d %H:%M", intm);
fputs(out, fp);
}
void
write_header(FILE *fp, const char *title, const char *relpath)
{
T("\n");
T("\n");
T("\n");
T("\n");
T("\n");
T("{reponame}");
if (description[0]) {
T(": {description}");
}
if (title[0]) {
T(" - {title}");
}
T("\n");
if (favicon[0])
T("\n");
T("\n");
if (stylesheet[0])
T("\n");
T("\n");
T("\n");
T("\n");
T("
\n");
T("\n");
}
void
write_footer(FILE *fp)
{
T("\n\n\n");
}
void
write_readme(FILE *fp, const git_blob *blob)
{
const void *raw = git_blob_rawcontent(blob);
git_off_t len = git_blob_rawsize(blob);
#ifdef CMARK
char *rendered = cmark_markdown_to_html(raw, len, CMARK_OPT_SAFE);
T("\n");
fputs(rendered, fp);
T("
\n");
free(rendered);
#else
T("\n");
fwrite(raw, 1, len, fp);
T("
\n");
#endif
}
void
write_commit_statline(FILE *fp, struct deltainfo *di, size_t i)
{
char c;
int total;
size_t j;
const git_diff_delta *delta;
delta = git_patch_get_delta(di->patch);
switch (delta->status) {
case GIT_DELTA_ADDED: c = 'A'; break;
case GIT_DELTA_COPIED: c = 'C'; break;
case GIT_DELTA_DELETED: c = 'D'; break;
case GIT_DELTA_MODIFIED: c = 'M'; break;
case GIT_DELTA_RENAMED: c = 'R'; break;
case GIT_DELTA_TYPECHANGE: c = 'T'; break;
default: c = ' '; break;
}
T("\n");
T("{c:%c} | \n");
T("");
T("{delta->old_file.path}");
if (strcmp(delta->old_file.path, delta->new_file.path)) {
T(" -> {delta->new_file.path}");
}
T(" | \n");
total = di->addcount + di->delcount;
T("{total:%i} | \n");
T("");
for (j = 0; j < di->addcount && j * total < di->addcount * 60; j++)
fputc('+', fp);
T("");
for (j = 0; j < di->delcount && j * total < di->delcount * 60; j++)
fputc('-', fp);
T(" |
\n");
}
void
write_commit_file(FILE *fp, struct deltainfo *di, size_t i)
{
const git_diff_delta *delta;
const git_diff_hunk *hunk;
const git_diff_line *line;
size_t nhunks, nhunklines, j, k;
const char *tag;
delta = git_patch_get_delta(di->patch);
nhunks = git_patch_num_hunks(di->patch);
T("\n");
if (delta->flags & GIT_DIFF_FLAG_BINARY) {
T("Binary files differ.\n");
return;
}
if (nhunks == 0)
return;
T("\n");
for (j = 0; j < nhunks; j++) {
if (git_patch_get_hunk(&hunk, &nhunklines, di->patch, j))
break;
T("");
T("{hunk->header:hunk->header_len - 1}");
T("\n");
for (k = 0; k < nhunklines; k++) {
if (git_patch_get_line_in_hunk(&line, di->patch, j, k))
break;
tag = line->old_lineno == -1 ? "ins" : line->new_lineno == -1 ? "del" : "span";
T("<{tag:%s} class=\"line\" id=\"h{i:%zu}-{j:%zu}-{k:%zu}\">");
T("{line->old_lineno:%5i} {line->new_lineno:%5i} ");
T("{line->content:line->content_len - 1}");
T("{tag:%s}>\n");
}
}
T("
\n");
}
void
write_commit(FILE *fp, struct commitinfo *ci)
{
size_t i;
struct commitstats *cs = commitinfo_getstats(ci);
T("\n");
T("- commit
- {ci->oid:%s}
\n");
if (ci->parentoid[0])
T("- parent
- parentoid:%s}.html\">{ci->parentoid:%s}
\n");
if (ci->author) {
T("- Author
- {ci->author->name} ");
T("<author->email}\">{ci->author->email}>");
T("
\n- Date
- ");
printtimeshort(fp, &(ci->author->when));
T("
\n");
}
T("
\n");
if (ci->msg) {
T("{ci->msg}
\n");
}
if (!cs->deltas)
goto err;
if (
cs->ndeltas > 1000 ||
cs->addcount > 100000 ||
cs->delcount > 100000
) {
T("Diff is too large, output suppressed.\n");
goto err;
}
T("Diffstat
\n");
T("\n");
for (i = 0; i < cs->ndeltas; i++) {
write_commit_statline(fp, cs->deltas[i], i);
}
T("
\n");
T("");
T("
{cs->ndeltas:%zu} files changed, {cs->addcount:%zu} insertions, {cs->delcount:%zu} deletions
\n");
T("
\n");
T("
\n");
for (i = 0; i < cs->ndeltas; i++) {
write_commit_file(fp, cs->deltas[i], i);
}
err:
commitstats_free(cs);
}
void
write_log_header(FILE *fp)
{
T("\n");
T("\n");
T("Date | Commit message | Author |
\n");
T("\n");
T("\n");
}
void
write_log_line(FILE *fp, struct commitinfo *ci)
{
T("");
if (ci->author) {
T("oid:%s}.html\">");
printtimeshort(fp, &(ci->author->when));
T("");
}
T(" | ");
if (ci->summary)
T("{ci->summary}");
T(" | ");
if (ci->author)
T("{ci->author->name}");
T(" |
\n");
}
void
write_log_footer(FILE *fp)
{
T("\n
\n");
}
void
write_atom_header(FILE *fp)
{
T("\n");
T("\n");
T("{reponame}, branch HEAD\n");
T("{description}\n");
}
void
write_atom_entry(FILE *fp, struct commitinfo *ci)
{
T("\n");
T("{ci->oid:%s}\n");
if (ci->author) {
T("");
printtimez(fp, &(ci->author->when));
T("\n");
}
if (ci->committer) {
T("");
printtimez(fp, &(ci->committer->when));
T("\n");
}
if (ci->summary) {
T("{ci->summary}\n");
}
T("oid:%s}.html\" />\n");
if (ci->author) {
T("\n");
T("{ci->author->name}\n");
T("{ci->author->email}\n");
T("\n");
}
if (ci->msg) {
T("{ci->msg}\n");
}
T("\n");
}
void
write_atom_footer(FILE *fp)
{
T("\n");
}
void
copy_blob(git_object *obj, const char *fpath)
{
char tmp[PATH_MAX] = "";
char *d;
git_off_t len;
const void *raw;
FILE *fp;
if (strlcpy(tmp, fpath, sizeof(tmp)) >= sizeof(tmp))
errx(1, "path truncated: '%s'", fpath);
if (!(d = dirname(tmp)))
err(1, "dirname");
if (mkdirp(d))
return;
fp = efopen(fpath, "w");
len = git_blob_rawsize((git_blob *)obj);
raw = git_blob_rawcontent((git_blob *)obj);
fwrite(raw, 1, len, fp);
fclose(fp);
}
void
write_files_header(FILE *fp)
{
T("\n");
T("\n");
T("Name | Size |
\n");
T("\n");
T("\n");
}
void
write_files_line(FILE *fp, char *entrypath, char *filepath, uintmax_t size)
{
T("{entrypath} | {size:%ju}B |
\n");
}
void
write_files_footer(FILE *fp)
{
T("\n
\n");
}
void
process_readme(FILE *fp)
{
int i;
git_object *obj = NULL;
for (i = 0; i < sizeof(readmefiles) / sizeof(*readmefiles); i++) {
if (
!git_revparse_single(&obj, repo, readmefiles[i]) &&
git_object_type(obj) == GIT_OBJ_BLOB
) {
write_readme(fp, (git_blob *)obj);
break;
}
git_object_free(obj);
}
}
int
_process_files(FILE *fp, git_tree *tree, const char *path)
{
const git_tree_entry *entry = NULL;
git_object *obj = NULL;
git_off_t filesize;
const char *entryname;
char filepath[PATH_MAX], entrypath[PATH_MAX];
size_t count, i;
int ret;
count = git_tree_entrycount(tree);
for (i = 0; i < count; i++) {
if (
!(entry = git_tree_entry_byindex(tree, i)) ||
!(entryname = git_tree_entry_name(entry))
)
return -1;
joinpath(entrypath, sizeof(entrypath), path, entryname);
joinpath(filepath, sizeof(filepath), "blob", entrypath);
if (!git_tree_entry_to_object(&obj, repo, entry)) {
switch (git_object_type(obj)) {
case GIT_OBJ_BLOB:
break;
case GIT_OBJ_TREE:
/* NOTE: recurses */
ret = _process_files(fp, (git_tree *)obj, entrypath);
git_object_free(obj);
if (ret)
return ret;
continue;
default:
git_object_free(obj);
continue;
}
copy_blob(obj, filepath);
filesize = git_blob_rawsize((git_blob *)obj);
write_files_line(fp, entrypath, filepath, filesize);
git_object_free(obj);
}
}
return 0;
}
int
process_files(FILE *fp)
{
git_tree *tree = NULL;
git_commit *commit = NULL;
git_object *obj = NULL;
const git_oid *head = NULL;
int ret = -1;
/* find HEAD */
if (!git_revparse_single(&obj, repo, "HEAD"))
head = git_object_id(obj);
git_object_free(obj);
if (!head) {
fprintf(stderr, "no HEAD found\n");
return 1;
}
if (!git_commit_lookup(&commit, repo, head) && !git_commit_tree(&tree, commit))
ret = _process_files(fp, tree, "");
git_commit_free(commit);
git_tree_free(tree);
return ret;
}
void
process_commits(FILE *fp_log, FILE *fp_atom, size_t m)
{
struct commitinfo *ci;
git_revwalk *w = NULL;
git_oid id;
FILE *fp_commit;
char path[PATH_MAX], oidstr[GIT_OID_HEXSZ + 1];
git_revwalk_new(&w, repo);
git_revwalk_push_head(w);
while (!git_revwalk_next(&id, w)) {
if (m == 0) {
fputs("More commits remaining… |
\n", fp_log);
break;
}
if (!(ci = commitinfo_getbyoid(&id)))
break;
write_log_line(fp_log, ci);
write_atom_entry(fp_atom, ci);
git_oid_tostr(oidstr, sizeof(oidstr), &id);
snprintf(path, sizeof(path), "commit/%s.html", oidstr);
if (force_commits || access(path, F_OK)) {
mkdirp("commit");
fp_commit = efopen(path, "w");
write_header(fp_commit, ci->summary, "../");
write_commit(fp_commit, ci);
write_footer(fp_commit);
fclose(fp_commit);
}
m -= 1;
commitinfo_free(ci);
}
git_revwalk_free(w);
}
void
usage(char *argv0)
{
fprintf(stderr, "%s repodir\n", argv0);
exit(1);
}
void
get_repo_name(const char *path)
{
char *p;
if (!realpath(repodir, repodirabs))
err(1, "realpath");
if (!(reponame = strrchr(repodirabs, '/'))) {
fprintf(stderr, "could not use directory name\n");
return;
}
reponame++;
if ((p = strrchr(reponame, '.')))
if (!strcmp(p, ".git"))
*p = '\0';
}
void
get_metadata(void)
{
char path[PATH_MAX];
FILE *fpread;
joinpath(path, sizeof(path), repodir, "description");
if ((fpread = fopen(path, "r"))) {
if (!fgets(description, sizeof(description), fpread))
description[0] = '\0';
fclose(fpread);
}
joinpath(path, sizeof(path), repodir, "url");
if ((fpread = fopen(path, "r"))) {
if (!fgets(url, sizeof(url), fpread))
url[0] = '\0';
fclose(fpread);
}
}
int
main(int argc, char *argv[])
{
static FILE *fp_index, *fp_log, *fp_atom;
if (argc == 2)
repodir = argv[1];
else
usage(argv[0]);
git_libgit2_init();
#ifdef __OpenBSD__
if (unveil(repodir, "r") == -1)
err(1, "unveil: %s", repodir);
if (unveil(".", "rwc") == -1)
err(1, "unveil: .");
if (pledge("stdio rpath wpath cpath", NULL) == -1)
err(1, "pledge");
#endif
if (git_repository_open_ext(
&repo, repodir, GIT_REPOSITORY_OPEN_NO_SEARCH, NULL
) < 0) {
fprintf(stderr, "%s: cannot open repository\n", argv[0]);
return 1;
}
get_repo_name(repodir);
get_metadata();
fp_index = efopen("index.html", "w");
fp_log = efopen("log.html", "w");
fp_atom = efopen("atom.xml", "w");
write_header(fp_index, "", "");
write_files_header(fp_index);
write_header(fp_log, "Log", "");
write_log_header(fp_log);
write_atom_header(fp_atom);
process_files(fp_index);
write_files_footer(fp_index);
process_readme(fp_index);
process_commits(fp_log, fp_atom, 100);
write_atom_footer(fp_atom);
write_log_footer(fp_log);
write_footer(fp_log);
write_footer(fp_index);
fclose(fp_index);
fclose(fp_log);
fclose(fp_atom);
git_repository_free(repo);
git_libgit2_shutdown();
return 0;
}