#include #include #include #include #include #include #include #include #include #include #include #include "compat.h" #include "config.h" struct commitinfo { const git_oid *id; char oid[GIT_OID_HEXSZ + 1]; char parentoid[GIT_OID_HEXSZ + 1]; const git_signature *author; const char *summary; const char *msg; git_diff_stats *stats; git_diff *diff; git_commit *commit; git_commit *parent; git_tree *commit_tree; git_tree *parent_tree; size_t addcount; size_t delcount; size_t filecount; }; static git_repository *repo; static const char *relpath = ""; static const char *repodir; static char name[255]; static char description[255]; static char cloneurl[1024]; static int hasreadme, haslicense; void commitinfo_free(struct commitinfo *ci) { if (!ci) return; git_diff_stats_free(ci->stats); git_diff_free(ci->diff); git_tree_free(ci->commit_tree); git_tree_free(ci->parent_tree); git_commit_free(ci->commit); } struct commitinfo * commitinfo_getbyoid(const git_oid *id) { struct commitinfo *ci; git_diff_options opts; int error; if (!(ci = calloc(1, sizeof(struct commitinfo)))) err(1, "calloc"); ci->id = id; 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->summary = git_commit_summary(ci->commit); ci->msg = git_commit_message(ci->commit); if ((error = git_commit_tree(&(ci->commit_tree), ci->commit))) goto err; if (!(error = git_commit_parent(&(ci->parent), ci->commit, 0))) { if ((error = git_commit_tree(&(ci->parent_tree), ci->parent))) goto err; } else { ci->parent = NULL; ci->parent_tree = NULL; } git_diff_init_options(&opts, GIT_DIFF_OPTIONS_VERSION); opts.flags |= GIT_DIFF_DISABLE_PATHSPEC_MATCH; if ((error = git_diff_tree_to_tree(&(ci->diff), repo, ci->parent_tree, ci->commit_tree, &opts))) goto err; if (git_diff_get_stats(&(ci->stats), ci->diff)) goto err; ci->addcount = git_diff_stats_insertions(ci->stats); ci->delcount = git_diff_stats_deletions(ci->stats); ci->filecount = git_diff_stats_files_changed(ci->stats); return ci; err: commitinfo_free(ci); free(ci); return NULL; } FILE * efopen(const char *name, const char *flags) { FILE *fp; if (!(fp = fopen(name, flags))) err(1, "fopen"); return fp; } /* Escape characters below as HTML 2.0 / XML 1.0. */ 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); } } } /* Some implementations of dirname(3) return a pointer to a static * internal buffer (OpenBSD). Others modify the contents of `path` (POSIX). * This is a wrapper function that is compatible with both versions. * The program will error out if dirname(3) failed, this can only happen * with the OpenBSD version. */ char * xdirname(const char *path) { char *p, *b; if (!(p = strdup(path))) err(1, "strdup"); if (!(b = dirname(p))) err(1, "basename"); if (!(b = strdup(b))) err(1, "strdup"); free(p); return b; } /* Some implementations of basename(3) return a pointer to a static * internal buffer (OpenBSD). Others modify the contents of `path` (POSIX). * This is a wrapper function that is compatible with both versions. * The program will error out if basename(3) failed, this can only happen * with the OpenBSD version. */ char * xbasename(const char *path) { char *p, *b; if (!(p = strdup(path))) err(1, "strdup"); if (!(b = basename(p))) err(1, "basename"); if (!(b = strdup(b))) err(1, "strdup"); free(p); return b; } int mkdirp(const char *path) { char tmp[PATH_MAX], *p; strlcpy(tmp, path, sizeof(tmp)); 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; } void printtimeformat(FILE *fp, const git_time *intime, const char *fmt) { struct tm *intm; time_t t; char out[32]; t = (time_t) intime->time + (intime->offset * 60); intm = gmtime(&t); strftime(out, sizeof(out), fmt, intm); fputs(out, fp); } void printtimez(FILE *fp, const git_time *intime) { printtimeformat(fp, intime, "%Y-%m-%dT%H:%M:%SZ"); } void printtime(FILE *fp, const git_time *intime) { printtimeformat(fp, intime, "%a %b %e %T %Y"); } void printtimeshort(FILE *fp, const git_time *intime) { printtimeformat(fp, intime, "%Y-%m-%d %H:%M"); } int writeheader(FILE *fp) { fputs("" "\n\n" "\n" "\n", fp); xmlencode(fp, name, strlen(name)); if (description[0]) fputs(" - ", fp); xmlencode(fp, description, strlen(description)); fprintf(fp, "\n\n", relpath); fprintf(fp, "\n", name, relpath); fprintf(fp, "\n", relpath); fputs("\n\n", fp); if (cloneurl[0]) { fputs("", fp); } fputs("
", fp); fprintf(fp, "\"\"", relpath, relpath); fputs("

", fp); xmlencode(fp, name, strlen(name)); fputs("

", fp); xmlencode(fp, description, strlen(description)); fputs("
git clone ", fp); xmlencode(fp, cloneurl, strlen(cloneurl)); fputs("
\n", fp); fprintf(fp, "Log | ", relpath); fprintf(fp, "Files | ", relpath); fprintf(fp, "Refs/branches", relpath); if (hasreadme) fprintf(fp, " | README", relpath); if (haslicense) fprintf(fp, " | LICENSE", relpath); fputs("
\n
\n", fp); return 0; } int writefooter(FILE *fp) { return !fputs("
\n", fp); } void writeblobhtml(FILE *fp, const git_blob *blob) { off_t i; size_t n = 1; char *nfmt = "%d\n"; const char *s = git_blob_rawcontent(blob); git_off_t len = git_blob_rawsize(blob); fputs("
\n", fp);

	if (len) {
		fprintf(fp, nfmt, n, n, n);
		for (i = 0; i < len - 1; i++) {
			if (s[i] == '\n') {
				n++;
				fprintf(fp, nfmt, n, n, n);
			}
		}
	}

	fputs("
\n", fp);
	xmlencode(fp, s, (size_t)len);
	fputs("
\n", fp); } void printcommit(FILE *fp, struct commitinfo *ci) { fprintf(fp, "commit %s\n", relpath, ci->oid, ci->oid); if (ci->parentoid[0]) fprintf(fp, "parent %s\n", relpath, ci->parentoid, ci->parentoid); #if 0 if ((count = (int)git_commit_parentcount(commit)) > 1) { fputs("Merge:", fp); for (i = 0; i < count; i++) { git_oid_tostr(buf, 8, git_commit_parent_id(commit, i)); fprintf(fp, " %s", relpath, buf, buf); } fputc('\n', fp); } #endif if (ci->author) { fputs("Author: ", fp); xmlencode(fp, ci->author->name, strlen(ci->author->name)); fputs(" <author->email, strlen(ci->author->email)); fputs("\">", fp); xmlencode(fp, ci->author->email, strlen(ci->author->email)); fputs(">\nDate: ", fp); printtime(fp, &(ci->author->when)); fputc('\n', fp); } fputc('\n', fp); if (ci->msg) xmlencode(fp, ci->msg, strlen(ci->msg)); fputc('\n', fp); } void printshowfile(struct commitinfo *ci) { const git_diff_delta *delta; const git_diff_hunk *hunk; const git_diff_line *line; git_patch *patch; git_buf statsbuf; size_t ndeltas, nhunks, nhunklines; FILE *fp; size_t i, j, k; char path[PATH_MAX]; snprintf(path, sizeof(path), "commit/%s.html", ci->oid); /* check if file exists if so skip it */ if (!access(path, F_OK)) return; fp = efopen(path, "w"); writeheader(fp); fputs("
\n", fp);
	printcommit(fp, ci);

	memset(&statsbuf, 0, sizeof(statsbuf));

	/* diff stat */
	if (ci->stats) {
		if (!git_diff_stats_to_buf(&statsbuf, ci->stats,
		    GIT_DIFF_STATS_FULL | GIT_DIFF_STATS_SHORT, 80)) {
			if (statsbuf.ptr && statsbuf.ptr[0]) {
				fputs("Diffstat:\n", fp);
				fputs(statsbuf.ptr, fp);
			}
		}
	}

	fputs("
", fp); ndeltas = git_diff_num_deltas(ci->diff); for (i = 0; i < ndeltas; i++) { if (git_patch_from_diff(&patch, ci->diff, i)) { git_patch_free(patch); break; } delta = git_patch_get_delta(patch); fprintf(fp, "diff --git a/%s b/%s\n", relpath, delta->old_file.path, delta->old_file.path, relpath, delta->new_file.path, delta->new_file.path); /* check binary data */ if (delta->flags & GIT_DIFF_FLAG_BINARY) { fputs("Binary files differ\n", fp); git_patch_free(patch); continue; } nhunks = git_patch_num_hunks(patch); for (j = 0; j < nhunks; j++) { if (git_patch_get_hunk(&hunk, &nhunklines, patch, j)) break; fprintf(fp, "", j, j); xmlencode(fp, hunk->header, hunk->header_len); fputs("", fp); for (k = 0; ; k++) { if (git_patch_get_line_in_hunk(&line, patch, j, k)) break; if (line->old_lineno == -1) fprintf(fp, "+", j, k, j, k); else if (line->new_lineno == -1) fprintf(fp, "-", j, k, j, k); else fputc(' ', fp); xmlencode(fp, line->content, line->content_len); if (line->old_lineno == -1 || line->new_lineno == -1) fputs("", fp); } } git_patch_free(patch); } git_buf_free(&statsbuf); fputs("
\n", fp); writefooter(fp); fclose(fp); return; } int writelog(FILE *fp, const char *branch) { struct commitinfo *ci; const git_oid *oid; git_revwalk *w = NULL; git_object *obj = NULL; git_oid id; size_t len; mkdir("commit", 0755); if (git_revparse_single(&obj, repo, branch)) return -1; oid = git_object_id(obj); git_revwalk_new(&w, repo); git_revwalk_push(w, oid); git_revwalk_sorting(w, GIT_SORT_TIME); git_revwalk_simplify_first_parent(w); fputs("\n" "" "\n\n", fp); while (!git_revwalk_next(&id, w)) { relpath = ""; if (!(ci = commitinfo_getbyoid(&id))) break; fputs("\n", fp); relpath = "../"; printshowfile(ci); commitinfo_free(ci); } fputs("
AgeCommit messageAuthorFiles+-
", fp); if (ci->author) printtimeshort(fp, &(ci->author->when)); fputs("", fp); if (ci->summary) { fprintf(fp, "", relpath, ci->oid); if ((len = strlen(ci->summary)) > summarylen) { xmlencode(fp, ci->summary, summarylen - 1); fputs("…", fp); } else { xmlencode(fp, ci->summary, len); } fputs("", fp); } fputs("", fp); if (ci->author) xmlencode(fp, ci->author->name, strlen(ci->author->name)); fputs("", fp); fprintf(fp, "%zu", ci->filecount); fputs("", fp); fprintf(fp, "+%zu", ci->addcount); fputs("", fp); fprintf(fp, "-%zu", ci->delcount); fputs("
", fp); git_revwalk_free(w); git_object_free(obj); relpath = ""; return 0; } void printcommitatom(FILE *fp, struct commitinfo *ci) { fputs("\n", fp); fprintf(fp, "%s\n", ci->oid); if (ci->author) { fputs("", fp); printtimez(fp, &(ci->author->when)); fputs("\n", fp); } if (ci->summary) { fputs("", fp); xmlencode(fp, ci->summary, strlen(ci->summary)); fputs("\n", fp); } fputs("", fp); fprintf(fp, "commit %s\n", ci->oid); if (ci->parentoid[0]) fprintf(fp, "parent %s\n", ci->parentoid); #if 0 if ((count = (int)git_commit_parentcount(commit)) > 1) { fputs("Merge:", fp); for (i = 0; i < count; i++) { git_oid_tostr(buf, 8, git_commit_parent_id(commit, i)); fprintf(fp, " %s", buf); } fputc('\n', fp); } #endif if (ci->author) { fputs("Author: ", fp); xmlencode(fp, ci->author->name, strlen(ci->author->name)); fputs(" <", fp); xmlencode(fp, ci->author->email, strlen(ci->author->email)); fputs(">\nDate: ", fp); printtime(fp, &(ci->author->when)); } fputc('\n', fp); if (ci->msg) xmlencode(fp, ci->msg, strlen(ci->msg)); fputs("\n\n", fp); if (ci->author) { fputs("", fp); xmlencode(fp, ci->author->name, strlen(ci->author->name)); fputs("\n", fp); xmlencode(fp, ci->author->email, strlen(ci->author->email)); fputs("\n\n", fp); } fputs("\n", fp); } int writeatom(FILE *fp) { struct commitinfo *ci; git_revwalk *w = NULL; git_oid id; size_t i, m = 100; /* max */ fputs("\n" "\n", fp); xmlencode(fp, name, strlen(name)); fputs(", branch master\n", fp); xmlencode(fp, description, strlen(description)); fputs("\n", fp); git_revwalk_new(&w, repo); git_revwalk_push_head(w); git_revwalk_sorting(w, GIT_SORT_TIME); git_revwalk_simplify_first_parent(w); for (i = 0; i < m && !git_revwalk_next(&id, w); i++) { if (!(ci = commitinfo_getbyoid(&id))) break; printcommitatom(fp, ci); commitinfo_free(ci); } git_revwalk_free(w); fputs("", fp); return 0; } int writeblob(git_object *obj, const char *filename, git_off_t filesize) { char fpath[PATH_MAX]; char tmp[PATH_MAX] = ""; char *d, *p; FILE *fp; snprintf(fpath, sizeof(fpath), "file/%s.html", filename); d = xdirname(fpath); if (mkdirp(d)) { free(d); return 1; } free(d); p = fpath; while (*p) { if (*p == '/') strlcat(tmp, "../", sizeof(tmp)); p++; } relpath = tmp; fp = efopen(fpath, "w"); writeheader(fp); fputs("

", fp); xmlencode(fp, filename, strlen(filename)); fprintf(fp, " (%jub)", (uintmax_t)filesize); fputs("


", fp); if (git_blob_is_binary((git_blob *)obj)) { fputs("

Binary file

\n", fp); } else { writeblobhtml(fp, (git_blob *)obj); if (ferror(fp)) err(1, "fwrite"); } writefooter(fp); fclose(fp); relpath = ""; return 0; } const char * filemode(git_filemode_t m) { static char mode[11]; memset(mode, '-', sizeof(mode) - 1); mode[10] = '\0'; if (S_ISREG(m)) mode[0] = '-'; else if (S_ISBLK(m)) mode[0] = 'b'; else if (S_ISCHR(m)) mode[0] = 'c'; else if (S_ISDIR(m)) mode[0] = 'd'; else if (S_ISFIFO(m)) mode[0] = 'p'; else if (S_ISLNK(m)) mode[0] = 'l'; else if (S_ISSOCK(m)) mode[0] = 's'; else mode[0] = '?'; if (m & S_IRUSR) mode[1] = 'r'; if (m & S_IWUSR) mode[2] = 'w'; if (m & S_IXUSR) mode[3] = 'x'; if (m & S_IRGRP) mode[4] = 'r'; if (m & S_IWGRP) mode[5] = 'w'; if (m & S_IXGRP) mode[6] = 'x'; if (m & S_IROTH) mode[7] = 'r'; if (m & S_IWOTH) mode[8] = 'w'; if (m & S_IXOTH) mode[9] = 'x'; if (m & S_ISUID) mode[3] = (mode[3] == 'x') ? 's' : 'S'; if (m & S_ISGID) mode[6] = (mode[6] == 'x') ? 's' : 'S'; if (m & S_ISVTX) mode[9] = (mode[9] == 'x') ? 't' : 'T'; return mode; } int writefilestree(FILE *fp, git_tree *tree, const char *branch, const char *path) { const git_tree_entry *entry = NULL; const char *filename; char filepath[PATH_MAX]; git_object *obj = NULL; git_off_t filesize; 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)) || git_tree_entry_to_object(&obj, repo, entry)) return -1; filename = git_tree_entry_name(entry); switch (git_object_type(obj)) { case GIT_OBJ_BLOB: break; case GIT_OBJ_TREE: /* NOTE: recurses */ ret = writefilestree(fp, (git_tree *)obj, branch, filename); git_object_free(obj); if (ret) return ret; continue; default: git_object_free(obj); continue; } if (path[0]) { snprintf(filepath, sizeof(filepath), "%s/%s", path, filename); filename = filepath; } filesize = git_blob_rawsize((git_blob *)obj); fputs("", fp); fprintf(fp, "%s", filemode(git_tree_entry_filemode(entry))); fprintf(fp, "", fp); xmlencode(fp, filename, strlen(filename)); fputs("", fp); fprintf(fp, "%ju", (uintmax_t)filesize); fputs("\n", fp); writeblob(obj, filename, filesize); } return 0; } int writefiles(FILE *fp, const char *branch) { const git_oid *id; git_tree *tree = NULL; git_object *obj = NULL; git_commit *commit = NULL; int ret = -1; fputs("\n" "" "\n\n", fp); if (git_revparse_single(&obj, repo, branch)) goto err; id = git_object_id(obj); if (git_commit_lookup(&commit, repo, id) || git_commit_tree(&tree, commit)) goto err; ret = writefilestree(fp, tree, branch, ""); err: fputs("
ModeNameSize
", fp); git_object_free(obj); git_commit_free(commit); git_tree_free(tree); return ret; } int refs_cmp(const void *v1, const void *v2) { git_reference *r1 = (*(git_reference **)v1); git_reference *r2 = (*(git_reference **)v2); int t1, t2; t1 = git_reference_is_branch(r1); t2 = git_reference_is_branch(r2); if (t1 != t2) return t1 - t2; return strcmp(git_reference_shorthand(r1), git_reference_shorthand(r2)); } int writerefs(FILE *fp) { struct commitinfo *ci; const git_oid *id = NULL; git_object *obj = NULL; git_reference *dref = NULL, *r, *ref = NULL; git_reference_iterator *it = NULL; git_reference **refs = NULL; size_t count, i, j, len, refcount = 0; const char *cols[] = { "Branch", "Tag" }; /* first column title */ const char *titles[] = { "Branches", "Tags" }; const char *ids[] = { "branches", "tags" }; const char *name; if (git_reference_iterator_new(&it, repo)) return -1; for (refcount = 0; !git_reference_next(&ref, it); refcount++) { if (!(refs = reallocarray(refs, refcount + 1, sizeof(git_reference *)))) err(1, "realloc"); refs[refcount] = ref; } git_reference_iterator_free(it); /* sort by type then shorthand name */ qsort(refs, refcount, sizeof(git_reference *), refs_cmp); for (j = 0; j < 2; j++) { for (i = 0, count = 0; i < refcount; i++) { if (git_reference_is_branch(refs[i]) && j == 0) ; else if (git_reference_is_tag(refs[i]) && j == 1) ; else continue; id = NULL; r = NULL; switch (git_reference_type(refs[i])) { case GIT_REF_SYMBOLIC: if (git_reference_resolve(&dref, refs[i])) goto err; r = dref; break; case GIT_REF_OID: r = refs[i]; break; default: continue; } if (!(id = git_reference_target(r))) goto err; if (git_reference_peel(&obj, r, GIT_OBJ_ANY)) goto err; if (!(id = git_object_id(obj))) goto err; if (!(ci = commitinfo_getbyoid(id))) break; /* print header if it has an entry (first). */ if (++count == 1) { fprintf(fp, "

%s

\n" "" "" "\n\n", titles[j], ids[j], cols[j]); } relpath = ""; name = git_reference_shorthand(r); fputs("\n", fp); relpath = "../"; commitinfo_free(ci); git_reference_free(dref); dref = NULL; } /* table footer */ if (count) fputs("
%sAgeCommit messageAuthorFiles+-
", fp); xmlencode(fp, name, strlen(name)); fputs("", fp); if (ci->author) printtimeshort(fp, &(ci->author->when)); fputs("", fp); if (ci->summary) { fprintf(fp, "", relpath, ci->oid); if ((len = strlen(ci->summary)) > summarylen) { xmlencode(fp, ci->summary, summarylen - 1); fputs("…", fp); } else { xmlencode(fp, ci->summary, len); } fputs("", fp); } fputs("", fp); if (ci->author) xmlencode(fp, ci->author->name, strlen(ci->author->name)); fputs("", fp); fprintf(fp, "%zu", ci->filecount); fputs("", fp); fprintf(fp, "+%zu", ci->addcount); fputs("", fp); fprintf(fp, "-%zu", ci->delcount); fputs("
", fp); } err: git_reference_free(dref); for (i = 0; i < refcount; i++) git_reference_free(refs[i]); free(refs); return 0; } int main(int argc, char *argv[]) { git_object *obj = NULL; const git_error *e = NULL; FILE *fp, *fpread; char path[PATH_MAX], *p; int status; if (argc != 2) { fprintf(stderr, "%s \n", argv[0]); return 1; } repodir = argv[1]; git_libgit2_init(); if ((status = git_repository_open_ext(&repo, repodir, GIT_REPOSITORY_OPEN_NO_SEARCH, NULL)) < 0) { e = giterr_last(); fprintf(stderr, "error %d/%d: %s\n", status, e->klass, e->message); return status; } /* use directory name as name */ p = xbasename(repodir); snprintf(name, sizeof(name), "%s", p); free(p); /* read description or .git/description */ snprintf(path, sizeof(path), "%s%s%s", repodir, repodir[strlen(repodir)] == '/' ? "" : "/", "description"); if (!(fpread = fopen(path, "r"))) { snprintf(path, sizeof(path), "%s%s%s", repodir, repodir[strlen(repodir)] == '/' ? "" : "/", ".git/description"); fpread = fopen(path, "r"); } if (fpread) { if (!fgets(description, sizeof(description), fpread)) description[0] = '\0'; fclose(fpread); } /* read url or .git/url */ snprintf(path, sizeof(path), "%s%s%s", repodir, repodir[strlen(repodir)] == '/' ? "" : "/", "url"); if (!(fpread = fopen(path, "r"))) { snprintf(path, sizeof(path), "%s%s%s", repodir, repodir[strlen(repodir)] == '/' ? "" : "/", ".git/url"); fpread = fopen(path, "r"); } if (fpread) { if (!fgets(cloneurl, sizeof(cloneurl), fpread)) cloneurl[0] = '\0'; cloneurl[strcspn(cloneurl, "\n")] = '\0'; fclose(fpread); } /* check LICENSE */ haslicense = !git_revparse_single(&obj, repo, "HEAD:LICENSE"); git_object_free(obj); /* check README */ hasreadme = !git_revparse_single(&obj, repo, "HEAD:README"); git_object_free(obj); /* log for HEAD */ mkdir("log", 0755); fp = efopen("log.html", "w"); relpath = ""; writeheader(fp); writelog(fp, "HEAD"); writefooter(fp); fclose(fp); /* files for HEAD */ fp = efopen("files.html", "w"); writeheader(fp); writefiles(fp, "HEAD"); writefooter(fp); fclose(fp); /* summary page with branches and tags */ fp = efopen("refs.html", "w"); writeheader(fp); writerefs(fp); writefooter(fp); fclose(fp); /* Atom feed */ fp = efopen("atom.xml", "w"); writeatom(fp); fclose(fp); /* cleanup */ git_repository_free(repo); git_libgit2_shutdown(); return 0; }