#include <git2.h>
#include "compat.h"
-#include "config.h"
struct deltainfo {
git_patch *patch;
char parentoid[GIT_OID_HEXSZ + 1];
const git_signature *author;
+ const git_signature *committer;
const char *summary;
const char *msg;
size_t ndeltas;
};
+/* summary length (bytes) in the log */
+static const unsigned summarylen = 70;
+/* display line count or file size in file tree index */
+static const int showlinecount = 1;
+
static git_repository *repo;
static const char *relpath = "";
static const char *repodir;
static char *name = "";
-static char *stripped_name;
+static char *strippedname;
static char description[255];
static char cloneurl[1024];
static int haslicense, hasreadme, hassubmodules;
+/* cache */
+static git_oid lastoid;
+static char lastoidstr[GIT_OID_HEXSZ + 2]; /* id + newline + nul byte */
+static FILE *rcachefp, *wcachefp;
+static const char *cachefile;
+
+#ifndef USE_PLEDGE
+int
+pledge(const char *promises, const char *paths[])
+{
+ return 0;
+}
+#endif
+
+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 == -1 || (size_t)r >= bufsiz)
+ errx(1, "path truncated: '%s%s%s'",
+ path, path[0] && path[strlen(path) - 1] != '/' ? "/" : "", path2);
+}
+
void
deltainfo_free(struct deltainfo *di)
{
if (!ci)
return;
-
if (ci->deltas)
for (i = 0; i < ci->ndeltas; i++)
deltainfo_free(ci->deltas[i]);
{
struct commitinfo *ci;
git_diff_options opts;
- const git_oid *oid;
- 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;
+ ci->id = id;
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);
- oid = git_commit_tree_id(ci->commit);
- if ((error = git_tree_lookup(&(ci->commit_tree), repo, oid)))
+ if (git_tree_lookup(&(ci->commit_tree), repo, git_commit_tree_id(ci->commit)))
goto err;
- if (!(error = git_commit_parent(&(ci->parent), ci->commit, 0))) {
- oid = git_commit_tree_id(ci->parent);
- if ((error = git_tree_lookup(&(ci->parent_tree), repo, oid))) {
+ if (!git_commit_parent(&(ci->parent), ci->commit, 0)) {
+ if (git_tree_lookup(&(ci->parent_tree), repo, git_commit_tree_id(ci->parent))) {
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)))
+ if (git_diff_tree_to_tree(&(ci->diff), repo, ci->parent_tree, ci->commit_tree, &opts))
goto err;
-
if (commitinfo_getstats(ci) == -1)
goto err;
}
}
-/* 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, "dirname");
- if (!(b = strdup(b)))
- err(1, "strdup");
- free(p);
-
- return b;
-}
-
int
mkdirp(const char *path)
{
}
void
-printtimeformat(FILE *fp, const git_time *intime, const char *fmt)
+printtimez(FILE *fp, const git_time *intime)
{
struct tm *intm;
time_t t;
char out[32];
- t = (time_t) intime->time + (intime->offset * 60);
+ t = (time_t)intime->time;
if (!(intm = gmtime(&t)))
return;
- strftime(out, sizeof(out), fmt, intm);
+ strftime(out, sizeof(out), "%Y-%m-%dT%H:%M:%SZ", 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");
+ struct tm *intm;
+ time_t t;
+ char out[32];
+
+ t = (time_t)intime->time + (intime->offset * 60);
+ if (!(intm = gmtime(&t)))
+ return;
+ strftime(out, sizeof(out), "%a %b %e %H:%M:%S", intm);
+ if (intime->offset < 0)
+ fprintf(fp, "%s -%02d%02d", out,
+ -(intime->offset) / 60, -(intime->offset) % 60);
+ else
+ fprintf(fp, "%s +%02d%02d", out,
+ intime->offset / 60, intime->offset % 60);
}
void
printtimeshort(FILE *fp, const git_time *intime)
{
- printtimeformat(fp, intime, "%Y-%m-%d %H:%M");
+ 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);
}
-int
+void
writeheader(FILE *fp, const char *title)
{
fputs("<!DOCTYPE html>\n"
"<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n"
"<meta http-equiv=\"Content-Language\" content=\"en\" />\n<title>", fp);
xmlencode(fp, title, strlen(title));
- if (title[0] && stripped_name[0])
+ if (title[0] && strippedname[0])
fputs(" - ", fp);
- xmlencode(fp, stripped_name, strlen(stripped_name));
+ xmlencode(fp, strippedname, strlen(strippedname));
if (description[0])
fputs(" - ", fp);
xmlencode(fp, description, strlen(description));
fprintf(fp, "<a href=\"../%s\"><img src=\"%slogo.png\" alt=\"\" width=\"32\" height=\"32\" /></a>",
relpath, relpath);
fputs("</td><td><h1>", fp);
- xmlencode(fp, stripped_name, strlen(stripped_name));
+ xmlencode(fp, strippedname, strlen(strippedname));
fputs("</h1><span class=\"desc\">", fp);
xmlencode(fp, description, strlen(description));
fputs("</span></td></tr>", fp);
if (haslicense)
fprintf(fp, " | <a href=\"%sfile/LICENSE.html\">LICENSE</a>", relpath);
fputs("</td></tr></table>\n<hr/>\n<div id=\"content\">\n", fp);
-
- return 0;
}
-int
+void
writefooter(FILE *fp)
{
- return !fputs("</div>\n</body>\n</html>\n", fp);
+ fputs("</div>\n</body>\n</html>\n", fp);
}
int
const git_diff_hunk *hunk;
const git_diff_line *line;
git_patch *patch;
- size_t nhunks, nhunklines, changed, add, del, total;
- size_t i, j, k;
+ size_t nhunks, nhunklines, changed, add, del, total, i, j, k;
char linestr[80];
printcommit(fp, ci);
if (!ci->deltas)
return;
+ if (ci->filecount > 1000 ||
+ ci->ndeltas > 1000 ||
+ ci->addcount > 100000 ||
+ ci->delcount > 100000) {
+ fprintf(fp, "(diff is too large, output suppressed)");
+ return;
+ }
+
/* diff stat */
fputs("<b>Diffstat:</b>\n<table>", fp);
for (i = 0; i < ci->ndeltas; i++) {
delta = git_patch_get_delta(ci->deltas[i]->patch);
- fputs("<tr><td>", fp);
+ fprintf(fp, "<tr><td><a href=\"#h%zu\">", i);
xmlencode(fp, delta->old_file.path, strlen(delta->old_file.path));
if (strcmp(delta->old_file.path, delta->new_file.path)) {
fputs(" -> ", fp);
memset(&linestr, '+', add);
memset(&linestr[add], '-', del);
- fprintf(fp, "</td><td> | %zu <span class=\"i\">",
+ fprintf(fp, "</a></td><td> | </td><td class=\"num\">%zu</td><td><span class=\"i\">",
ci->deltas[i]->addcount + ci->deltas[i]->delcount);
fwrite(&linestr, 1, add, fp);
fputs("</span><span class=\"d\">", fp);
for (i = 0; i < ci->ndeltas; i++) {
patch = ci->deltas[i]->patch;
delta = git_patch_get_delta(patch);
- fprintf(fp, "<b>diff --git a/<a href=\"%sfile/%s.html\">%s</a> b/<a href=\"%sfile/%s.html\">%s</a></b>\n",
- relpath, delta->old_file.path, delta->old_file.path,
+ fprintf(fp, "<b>diff --git a/<a id=\"h%zu\" href=\"%sfile/%s.html\">%s</a> b/<a href=\"%sfile/%s.html\">%s</a></b>\n",
+ i, relpath, delta->old_file.path, delta->old_file.path,
relpath, delta->new_file.path, delta->new_file.path);
/* check binary data */
}
}
+void
+writelogline(FILE *fp, struct commitinfo *ci)
+{
+ size_t len;
+
+ fputs("<tr><td>", fp);
+ if (ci->author)
+ printtimeshort(fp, &(ci->author->when));
+ fputs("</td><td>", fp);
+ if (ci->summary) {
+ fprintf(fp, "<a href=\"%scommit/%s.html\">", relpath, ci->oid);
+ if ((len = strlen(ci->summary)) > summarylen) {
+ xmlencode(fp, ci->summary, summarylen - 1);
+ fputs("…", fp);
+ } else {
+ xmlencode(fp, ci->summary, len);
+ }
+ fputs("</a>", fp);
+ }
+ fputs("</td><td>", fp);
+ if (ci->author)
+ xmlencode(fp, ci->author->name, strlen(ci->author->name));
+ fputs("</td><td class=\"num\">", fp);
+ fprintf(fp, "%zu", ci->filecount);
+ fputs("</td><td class=\"num\">", fp);
+ fprintf(fp, "+%zu", ci->addcount);
+ fputs("</td><td class=\"num\">", fp);
+ fprintf(fp, "-%zu", ci->delcount);
+ fputs("</td></tr>\n", fp);
+}
+
int
writelog(FILE *fp, const git_oid *oid)
{
struct commitinfo *ci;
git_revwalk *w = NULL;
git_oid id;
- size_t len;
char path[PATH_MAX];
FILE *fpfile;
int r;
git_revwalk_sorting(w, GIT_SORT_TIME);
git_revwalk_simplify_first_parent(w);
- fputs("<table id=\"log\"><thead>\n<tr><td>Date</td><td>Commit message</td>"
- "<td>Author</td><td class=\"num\">Files</td><td class=\"num\">+</td>"
- "<td class=\"num\">-</td></tr>\n</thead><tbody>\n", fp);
-
while (!git_revwalk_next(&id, w)) {
relpath = "";
+ if (cachefile && !memcmp(&id, &lastoid, sizeof(id)))
+ break;
if (!(ci = commitinfo_getbyoid(&id)))
break;
- fputs("<tr><td>", fp);
- if (ci->author)
- printtimeshort(fp, &(ci->author->when));
- fputs("</td><td>", fp);
- if (ci->summary) {
- fprintf(fp, "<a href=\"%scommit/%s.html\">", relpath, ci->oid);
- if ((len = strlen(ci->summary)) > summarylen) {
- xmlencode(fp, ci->summary, summarylen - 1);
- fputs("…", fp);
- } else {
- xmlencode(fp, ci->summary, len);
- }
- fputs("</a>", fp);
- }
- fputs("</td><td>", fp);
- if (ci->author)
- xmlencode(fp, ci->author->name, strlen(ci->author->name));
- fputs("</td><td class=\"num\">", fp);
- fprintf(fp, "%zu", ci->filecount);
- fputs("</td><td class=\"num\">", fp);
- fprintf(fp, "+%zu", ci->addcount);
- fputs("</td><td class=\"num\">", fp);
- fprintf(fp, "-%zu", ci->delcount);
- fputs("</td></tr>\n", fp);
+ writelogline(fp, ci);
+ if (cachefile)
+ writelogline(wcachefp, ci);
relpath = "../";
}
commitinfo_free(ci);
}
- fputs("</tbody></table>", fp);
-
git_revwalk_free(w);
relpath = "";
fprintf(fp, "<id>%s</id>\n", ci->oid);
if (ci->author) {
- fputs("<updated>", fp);
+ fputs("<published>", fp);
printtimez(fp, &(ci->author->when));
+ fputs("</published>\n", fp);
+ }
+ if (ci->committer) {
+ fputs("<updated>", fp);
+ printtimez(fp, &(ci->committer->when));
fputs("</updated>\n", fp);
}
if (ci->summary) {
fputc('\n', fp);
xmlencode(fp, ci->msg, strlen(ci->msg));
}
- fputs("\n</content>\n", fp);
-
- fputs("</entry>\n", fp);
+ fputs("\n</content>\n</entry>\n", fp);
}
int
struct commitinfo *ci;
git_revwalk *w = NULL;
git_oid id;
- size_t i, m = 100; /* max */
+ size_t i, m = 100; /* last 'm' commits */
fputs("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
"<feed xmlns=\"http://www.w3.org/2005/Atom\">\n<title>", fp);
- xmlencode(fp, stripped_name, strlen(stripped_name));
+ xmlencode(fp, strippedname, strlen(strippedname));
fputs(", branch HEAD</title>\n<subtitle>", fp);
xmlencode(fp, description, strlen(description));
fputs("</subtitle>\n", fp);
int
writeblob(git_object *obj, const char *fpath, const char *filename, git_off_t filesize)
{
- char tmp[PATH_MAX] = "";
- char *d;
+ char tmp[PATH_MAX] = "", *d;
const char *p;
int lc = 0;
FILE *fp;
- d = xdirname(fpath);
- if (mkdirp(d)) {
- free(d);
+ 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 -1;
- }
- free(d);
- p = fpath;
- while (*p) {
+ for (p = fpath, tmp[0] = '\0'; *p; p++) {
if (*p == '/' && strlcat(tmp, "../", sizeof(tmp)) >= sizeof(tmp))
errx(1, "path truncated: '../%s'", tmp);
- p++;
}
relpath = tmp;
{
const git_tree_entry *entry = NULL;
git_submodule *module = NULL;
- const char *entryname;
- char filepath[PATH_MAX], entrypath[PATH_MAX];
git_object *obj = NULL;
git_off_t filesize;
+ const char *entryname;
+ char filepath[PATH_MAX], entrypath[PATH_MAX];
size_t count, i;
int lc, r, ret;
if (!(entry = git_tree_entry_byindex(tree, i)) ||
!(entryname = git_tree_entry_name(entry)))
return -1;
- r = snprintf(entrypath, sizeof(entrypath), "%s%s%s",
- path, path[0] ? "/" : "", entryname);
- if (r == -1 || (size_t)r >= sizeof(entrypath))
- errx(1, "path truncated: '%s%s%s'",
- path, path[0] ? "/" : "", entryname);
-
- r = snprintf(filepath, sizeof(filepath), "file/%s%s%s.html",
- path, path[0] ? "/" : "", entryname);
+ joinpath(entrypath, sizeof(entrypath), path, entryname);
+
+ r = snprintf(filepath, sizeof(filepath), "file/%s.html",
+ entrypath);
if (r == -1 || (size_t)r >= sizeof(filepath))
- errx(1, "path truncated: 'file/%s%s%s.html'",
- path, path[0] ? "/" : "", entryname);
+ errx(1, "path truncated: 'file/%s.html'", entrypath);
if (!git_tree_entry_to_object(&obj, repo, entry)) {
switch (git_object_type(obj)) {
git_reference *dref = NULL, *r, *ref = NULL;
git_reference_iterator *it = NULL;
git_reference **refs = NULL;
- size_t count, i, j, refcount = 0;
+ size_t count, i, j, refcount;
const char *titles[] = { "Branches", "Tags" };
const char *ids[] = { "branches", "tags" };
const char *name;
return 0;
}
+void
+usage(char *argv0)
+{
+ fprintf(stderr, "%s [-c cachefile] repodir\n", argv0);
+ exit(1);
+}
+
int
main(int argc, char *argv[])
{
const git_error *e = NULL;
FILE *fp, *fpread;
char path[PATH_MAX], repodirabs[PATH_MAX + 1], *p;
- int r, status;
-
- if (argc != 2) {
- fprintf(stderr, "%s <repodir>\n", argv[0]);
- return 1;
+ char tmppath[64] = "cache.XXXXXXXXXXXX", buf[BUFSIZ];
+ size_t n;
+ int i, fd;
+
+ if (pledge("stdio rpath wpath cpath", NULL) == -1)
+ err(1, "pledge");
+
+ for (i = 1; i < argc; i++) {
+ if (argv[i][0] != '-') {
+ if (repodir)
+ usage(argv[0]);
+ repodir = argv[i];
+ } else if (argv[i][1] == 'c') {
+ if (i + 1 >= argc)
+ usage(argv[0]);
+ cachefile = argv[++i];
+ }
}
- repodir = argv[1];
+ if (!repodir)
+ usage(argv[0]);
+
if (!realpath(repodir, repodirabs))
err(1, "realpath");
git_libgit2_init();
- if ((status = git_repository_open_ext(&repo, repodir,
- GIT_REPOSITORY_OPEN_NO_SEARCH, NULL)) < 0) {
+ if (git_repository_open_ext(&repo, repodir,
+ GIT_REPOSITORY_OPEN_NO_SEARCH, NULL) < 0) {
e = giterr_last();
fprintf(stderr, "%s: %s\n", argv[0], e->message);
- return status;
+ return 1;
}
/* find HEAD */
name = "";
/* strip .git suffix */
- if (!(stripped_name = strdup(name)))
+ if (!(strippedname = strdup(name)))
err(1, "strdup");
- if ((p = strrchr(stripped_name, '.')))
+ if ((p = strrchr(strippedname, '.')))
if (!strcmp(p, ".git"))
*p = '\0';
/* read description or .git/description */
- r = snprintf(path, sizeof(path), "%s%s%s",
- repodir, repodir[strlen(repodir)] == '/' ? "" : "/", "description");
- if (r == -1 || (size_t)r >= sizeof(path))
- errx(1, "path truncated: '%s%s%s'",
- repodir, repodir[strlen(repodir)] == '/' ? "" : "/", "description");
+ joinpath(path, sizeof(path), repodir, "description");
if (!(fpread = fopen(path, "r"))) {
- r = snprintf(path, sizeof(path), "%s%s%s",
- repodir, repodir[strlen(repodir)] == '/' ? "" : "/", ".git/description");
- if (r == -1 || (size_t)r >= sizeof(path))
- errx(1, "path truncated: '%s%s%s'",
- repodir, repodir[strlen(repodir)] == '/' ? "" : "/", ".git/description");
+ joinpath(path, sizeof(path), repodir, ".git/description");
fpread = fopen(path, "r");
}
if (fpread) {
}
/* read url or .git/url */
- r = snprintf(path, sizeof(path), "%s%s%s",
- repodir, repodir[strlen(repodir)] == '/' ? "" : "/", "url");
- if (r == -1 || (size_t)r >= sizeof(path))
- errx(1, "path truncated: '%s%s%s'",
- repodir, repodir[strlen(repodir)] == '/' ? "" : "/", "url");
+ joinpath(path, sizeof(path), repodir, "url");
if (!(fpread = fopen(path, "r"))) {
- r = snprintf(path, sizeof(path), "%s%s%s",
- repodir, repodir[strlen(repodir)] == '/' ? "" : "/", ".git/url");
- if (r == -1 || (size_t)r >= sizeof(path))
- errx(1, "path truncated: '%s%s%s'",
- repodir, repodir[strlen(repodir)] == '/' ? "" : "/", ".git/url");
+ joinpath(path, sizeof(path), repodir, ".git/url");
fpread = fopen(path, "r");
}
if (fpread) {
/* log for HEAD */
fp = efopen("log.html", "w");
relpath = "";
- writeheader(fp, "Log");
mkdir("commit", 0755);
- writelog(fp, head);
+ writeheader(fp, "Log");
+ fputs("<table id=\"log\"><thead>\n<tr><td>Date</td><td>Commit message</td>"
+ "<td>Author</td><td class=\"num\">Files</td><td class=\"num\">+</td>"
+ "<td class=\"num\">-</td></tr>\n</thead><tbody>\n", fp);
+
+ if (cachefile) {
+ /* read from cache file (does not need to exist) */
+ if ((rcachefp = fopen(cachefile, "r"))) {
+ if (!fgets(lastoidstr, sizeof(lastoidstr), rcachefp))
+ errx(1, "%s: no object id", cachefile);
+ if (git_oid_fromstr(&lastoid, lastoidstr))
+ errx(1, "%s: invalid object id", cachefile);
+ }
+
+ /* write log to (temporary) cache */
+ if ((fd = mkstemp(tmppath)) == -1)
+ err(1, "mkstemp");
+ if (!(wcachefp = fdopen(fd, "w")))
+ err(1, "fdopen");
+ /* write last commit id (HEAD) */
+ git_oid_tostr(buf, sizeof(buf), head);
+ fprintf(wcachefp, "%s\n", buf);
+
+ writelog(fp, head);
+
+ if (rcachefp) {
+ /* append previous log to log.html and the new cache */
+ while (!feof(rcachefp)) {
+ n = fread(buf, 1, sizeof(buf), rcachefp);
+ if (ferror(rcachefp))
+ err(1, "fread");
+ if (fwrite(buf, 1, n, fp) != n ||
+ fwrite(buf, 1, n, wcachefp) != n)
+ err(1, "fwrite");
+ }
+ fclose(rcachefp);
+ }
+ fclose(wcachefp);
+ } else {
+ writelog(fp, head);
+ }
+
+ fputs("</tbody></table>", fp);
writefooter(fp);
fclose(fp);
writeatom(fp);
fclose(fp);
+ /* rename new cache file on success */
+ if (cachefile && rename(tmppath, cachefile))
+ err(1, "rename: '%s' to '%s'", tmppath, cachefile);
+
/* cleanup */
git_repository_free(repo);
git_libgit2_shutdown();