#include #include #include #include #include #include #include #include #include #include #include "config.h" #include "git2.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 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; /* TODO: show tags when commit has it */ 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; /* TODO: handle error */ 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); /* TODO: show tag when commit has it */ 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 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)); /* TODO: bring in libutil? */ 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\n
", fp); fprintf(fp, "\"\"", relpath, relpath); fputs("

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

", fp); xmlencode(fp, description, strlen(description)); fputs("
\n", fp); fprintf(fp, "Log | ", relpath); fprintf(fp, "Files", 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 = 0; 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);
		while (i < len - 1) {
			if (s[i] == '\n') {
				n++;
				fprintf(fp, nfmt, n, n, n);
			}
			i++;
		}
	}

	fputs("
\n", fp);
	xmlencode(fp, s, (size_t)len);
	fputs("
\n", fp); } void printcommit(FILE *fp, struct commitinfo *ci) { /* TODO: show tag when commit has it */ 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) { fprintf(fp, "Merge:"); 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) { fprintf(fp, "Author: "); xmlencode(fp, ci->author->name, strlen(ci->author->name)); fprintf(fp, " <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]) {
				fprintf(fp, "Diffstat:\n");
				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; /* TODO: handle error */ } 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; /* TODO: handle error ? */ fprintf(fp, "%s\n", hunk->header); 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; } void writelog(FILE *fp) { struct commitinfo *ci; git_revwalk *w = NULL; git_oid id; size_t len; mkdir("commit", 0755); git_revwalk_new(&w, repo); git_revwalk_push_head(w); git_revwalk_sorting(w, GIT_SORT_TIME); git_revwalk_simplify_first_parent(w); /* TODO: also make "expanded" log ? (with message body) */ 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); } fprintf(fp, "
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("
"); git_revwalk_free(w); relpath = ""; } 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) { fprintf(fp, "Merge:"); 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) { fprintf(fp, "Author: "); xmlencode(fp, ci->author->name, strlen(ci->author->name)); fprintf(fp, " <"); xmlencode(fp, ci->author->email, strlen(ci->author->email)); fprintf(fp, ">\nDate: "); 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", fp); fputs("\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(const git_index_entry *entry) { char fpath[PATH_MAX]; char ref[PATH_MAX]; char tmp[PATH_MAX] = ""; char *p; git_object *obj = NULL; FILE *fp; snprintf(fpath, sizeof(fpath), "file/%s.html", entry->path); snprintf(ref, sizeof(ref), "HEAD:%s", entry->path); if (git_revparse_single(&obj, repo, ref)) return 1; if (mkdirp(dirname(fpath))) return 1; p = fpath; while (*p) { if (*p == '/') strlcat(tmp, "../", sizeof(tmp)); p++; } relpath = tmp; fp = efopen(fpath, "w"); writeheader(fp); fprintf(fp, "

%s (%" PRIu32 "b)


", entry->path, entry->file_size); if (git_blob_is_binary((git_blob *)obj)) { fprintf(fp, "

Binary file

\n"); } else { writeblobhtml(fp, (git_blob *)obj); if (ferror(fp)) err(1, "fwrite"); } git_object_free(obj); writefooter(fp); fclose(fp); relpath = ""; return 0; } int writefiles(FILE *fp) { const git_index_entry *entry; git_index *index; size_t count, i; fputs("\n" "\n" "\n", fp); git_repository_index(&index, repo); count = git_index_entrycount(index); for (i = 0; i < count; i++) { entry = git_index_get_byindex(index, i); fputs("\n", fp); writeblob(entry); } fputs("
ModeNameSize
", fp); fprintf(fp, "%u", entry->mode); /* TODO: fancy print, like: "-rw-r--r--" */ fprintf(fp, "path, strlen(entry->path)); fputs(".html\">", fp); xmlencode(fp, entry->path, strlen(entry->path)); fputs("", fp); fprintf(fp, "%" PRIu32, entry->file_size); fputs("
", fp); 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); } /* 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); fp = efopen("log.html", "w"); writeheader(fp); writelog(fp); writefooter(fp); fclose(fp); fp = efopen("files.html", "w"); writeheader(fp); writefiles(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; }