#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"); if (logo[0]) T("\"\""); T("
\n"); T("

{reponame}

"); if (description[0] || url[0]) { T("
{description}"); if (description[0] && url[0]) T("  "); if (url[0]) { T("{url}"); } T("
"); } if (cloneurl[0]) { T("
git clone {cloneurl}{reponame}.git
\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"); T("diff --git\n"); T("a/old_file.path}\" rel=\"nofollow\">{delta->old_file.path}\n"); T("b/new_file.path}\" rel=\"nofollow\">{delta->new_file.path}\n"); 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("\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("\n"); T("\n"); T("\n"); } void write_log_line(FILE *fp, struct commitinfo *ci) { T("\n"); } void write_log_footer(FILE *fp) { T("\n
DateCommit messageAuthor
"); 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_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("\n"); T("\n"); T("\n"); } void write_files_line(FILE *fp, char *entrypath, char *filepath, uintmax_t size) { T("\n"); } void write_files_footer(FILE *fp) { T("\n
NameSize
{entrypath}{size:%ju}B
\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; }