stagit

add stagit.c

Hiltjo Posthuma contact@arjunchoudhary.com

commit: b52a6b0 parent: b54b47a
1 files changed, 852 insertions(+), 0 deletions(-)
Astagit.c+852-0
A · stagit.c +852, -0
  1@@ -0,0 +1,852 @@
  2+#include <sys/stat.h>
  3+
  4+#include <err.h>
  5+#include <errno.h>
  6+#include <inttypes.h>
  7+#include <libgen.h>
  8+#include <limits.h>
  9+#include <stdio.h>
 10+#include <stdlib.h>
 11+#include <string.h>
 12+#include <unistd.h>
 13+
 14+#include <git2.h>
 15+
 16+#include "compat.h"
 17+#include "config.h"
 18+
 19+struct commitinfo {
 20+	const git_oid *id;
 21+
 22+	char oid[GIT_OID_HEXSZ + 1];
 23+	char parentoid[GIT_OID_HEXSZ + 1];
 24+
 25+	const git_signature *author;
 26+	const char *summary;
 27+	const char *msg;
 28+
 29+	git_diff_stats *stats;
 30+	git_diff       *diff;
 31+	git_commit     *commit;
 32+	git_commit     *parent;
 33+	git_tree       *commit_tree;
 34+	git_tree       *parent_tree;
 35+
 36+	size_t addcount;
 37+	size_t delcount;
 38+	size_t filecount;
 39+};
 40+
 41+static git_repository *repo;
 42+
 43+static const char *relpath = "";
 44+static const char *repodir;
 45+
 46+static char name[255];
 47+static char description[255];
 48+static char cloneurl[1024];
 49+static int hasreadme, haslicense;
 50+
 51+void
 52+commitinfo_free(struct commitinfo *ci)
 53+{
 54+	if (!ci)
 55+		return;
 56+
 57+	git_diff_stats_free(ci->stats);
 58+	git_diff_free(ci->diff);
 59+	git_tree_free(ci->commit_tree);
 60+	git_tree_free(ci->parent_tree);
 61+	git_commit_free(ci->commit);
 62+}
 63+
 64+struct commitinfo *
 65+commitinfo_getbyoid(const git_oid *id)
 66+{
 67+	struct commitinfo *ci;
 68+	git_diff_options opts;
 69+	int error;
 70+
 71+	if (!(ci = calloc(1, sizeof(struct commitinfo))))
 72+		err(1, "calloc");
 73+
 74+	ci->id = id;
 75+	if (git_commit_lookup(&(ci->commit), repo, id))
 76+		goto err;
 77+
 78+	git_oid_tostr(ci->oid, sizeof(ci->oid), git_commit_id(ci->commit));
 79+	git_oid_tostr(ci->parentoid, sizeof(ci->parentoid), git_commit_parent_id(ci->commit, 0));
 80+
 81+	ci->author = git_commit_author(ci->commit);
 82+	ci->summary = git_commit_summary(ci->commit);
 83+	ci->msg = git_commit_message(ci->commit);
 84+
 85+	if ((error = git_commit_tree(&(ci->commit_tree), ci->commit)))
 86+		goto err;
 87+	if (!(error = git_commit_parent(&(ci->parent), ci->commit, 0))) {
 88+		if ((error = git_commit_tree(&(ci->parent_tree), ci->parent)))
 89+			goto err;
 90+	} else {
 91+		ci->parent = NULL;
 92+		ci->parent_tree = NULL;
 93+	}
 94+
 95+	git_diff_init_options(&opts, GIT_DIFF_OPTIONS_VERSION);
 96+	opts.flags |= GIT_DIFF_DISABLE_PATHSPEC_MATCH;
 97+	if ((error = git_diff_tree_to_tree(&(ci->diff), repo, ci->parent_tree, ci->commit_tree, &opts)))
 98+		goto err;
 99+	if (git_diff_get_stats(&(ci->stats), ci->diff))
100+		goto err;
101+
102+	ci->addcount = git_diff_stats_insertions(ci->stats);
103+	ci->delcount = git_diff_stats_deletions(ci->stats);
104+	ci->filecount = git_diff_stats_files_changed(ci->stats);
105+
106+	return ci;
107+
108+err:
109+	commitinfo_free(ci);
110+	free(ci);
111+
112+	return NULL;
113+}
114+
115+FILE *
116+efopen(const char *name, const char *flags)
117+{
118+	FILE *fp;
119+
120+	if (!(fp = fopen(name, flags)))
121+		err(1, "fopen");
122+
123+	return fp;
124+}
125+
126+/* Escape characters below as HTML 2.0 / XML 1.0. */
127+void
128+xmlencode(FILE *fp, const char *s, size_t len)
129+{
130+	size_t i;
131+
132+	for (i = 0; *s && i < len; s++, i++) {
133+		switch(*s) {
134+		case '<':  fputs("&lt;",   fp); break;
135+		case '>':  fputs("&gt;",   fp); break;
136+		case '\'': fputs("&apos;", fp); break;
137+		case '&':  fputs("&amp;",  fp); break;
138+		case '"':  fputs("&quot;", fp); break;
139+		default:   fputc(*s, fp);
140+		}
141+	}
142+}
143+
144+/* Some implementations of dirname(3) return a pointer to a static
145+ * internal buffer (OpenBSD). Others modify the contents of `path` (POSIX).
146+ * This is a wrapper function that is compatible with both versions.
147+ * The program will error out if dirname(3) failed, this can only happen
148+ * with the OpenBSD version. */
149+char *
150+xdirname(const char *path)
151+{
152+	char *p, *b;
153+
154+	if (!(p = strdup(path)))
155+		err(1, "strdup");
156+	if (!(b = dirname(p)))
157+		err(1, "basename");
158+	if (!(b = strdup(b)))
159+		err(1, "strdup");
160+	free(p);
161+
162+	return b;
163+}
164+
165+/* Some implementations of basename(3) return a pointer to a static
166+ * internal buffer (OpenBSD). Others modify the contents of `path` (POSIX).
167+ * This is a wrapper function that is compatible with both versions.
168+ * The program will error out if basename(3) failed, this can only happen
169+ * with the OpenBSD version. */
170+char *
171+xbasename(const char *path)
172+{
173+	char *p, *b;
174+
175+	if (!(p = strdup(path)))
176+		err(1, "strdup");
177+	if (!(b = basename(p)))
178+		err(1, "basename");
179+	if (!(b = strdup(b)))
180+		err(1, "strdup");
181+	free(p);
182+
183+	return b;
184+}
185+
186+int
187+mkdirp(const char *path)
188+{
189+	char tmp[PATH_MAX], *p;
190+
191+	strlcpy(tmp, path, sizeof(tmp));
192+	for (p = tmp + (tmp[0] == '/'); *p; p++) {
193+		if (*p != '/')
194+			continue;
195+		*p = '\0';
196+		if (mkdir(tmp, S_IRWXU | S_IRWXG | S_IRWXO) < 0 && errno != EEXIST)
197+			return -1;
198+		*p = '/';
199+	}
200+	if (mkdir(tmp, S_IRWXU | S_IRWXG | S_IRWXO) < 0 && errno != EEXIST)
201+		return -1;
202+	return 0;
203+}
204+
205+void
206+printtimeformat(FILE *fp, const git_time *intime, const char *fmt)
207+{
208+	struct tm *intm;
209+	time_t t;
210+	char out[32];
211+
212+	t = (time_t) intime->time + (intime->offset * 60);
213+	intm = gmtime(&t);
214+	strftime(out, sizeof(out), fmt, intm);
215+	fputs(out, fp);
216+}
217+
218+void
219+printtimez(FILE *fp, const git_time *intime)
220+{
221+	printtimeformat(fp, intime, "%Y-%m-%dT%H:%M:%SZ");
222+}
223+
224+void
225+printtime(FILE *fp, const git_time *intime)
226+{
227+	printtimeformat(fp, intime, "%a %b %e %T %Y");
228+}
229+
230+void
231+printtimeshort(FILE *fp, const git_time *intime)
232+{
233+	printtimeformat(fp, intime, "%Y-%m-%d %H:%M");
234+}
235+
236+int
237+writeheader(FILE *fp)
238+{
239+	fputs("<!DOCTYPE HTML>"
240+		"<html dir=\"ltr\" lang=\"en\">\n<head>\n"
241+		"<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n"
242+		"<meta http-equiv=\"Content-Language\" content=\"en\" />\n<title>", fp);
243+	xmlencode(fp, name, strlen(name));
244+	if (description[0])
245+		fputs(" - ", fp);
246+	xmlencode(fp, description, strlen(description));
247+	fprintf(fp, "</title>\n<link rel=\"icon\" type=\"image/png\" href=\"%sfavicon.png\" />\n", relpath);
248+	fprintf(fp, "<link rel=\"alternate\" type=\"application/atom+xml\" title=\"%s Atom Feed\" href=\"%satom.xml\" />\n",
249+		name, relpath);
250+	fprintf(fp, "<link rel=\"stylesheet\" type=\"text/css\" href=\"%sstyle.css\" />\n", relpath);
251+	fputs("</head>\n<body>\n<table><tr><td>", fp);
252+	fprintf(fp, "<a href=\"../%s\"><img src=\"%slogo.png\" alt=\"\" width=\"32\" height=\"32\" /></a>",
253+	        relpath, relpath);
254+	fputs("</td><td><h1>", fp);
255+	xmlencode(fp, name, strlen(name));
256+	fputs("</h1><span class=\"desc\">", fp);
257+	xmlencode(fp, description, strlen(description));
258+	fputs("</span></td></tr>", fp);
259+	if (cloneurl[0]) {
260+		fputs("<tr class=\"url\"><td></td><td>git clone <a href=\"", fp);
261+		xmlencode(fp, cloneurl, strlen(cloneurl));
262+		fputs("\">", fp);
263+		xmlencode(fp, cloneurl, strlen(cloneurl));
264+		fputs("</a></td></tr>", fp);
265+	}
266+	fputs("<tr><td></td><td>\n", fp);
267+	fprintf(fp, "<a href=\"%slog.html\">Log</a> | ", relpath);
268+	fprintf(fp, "<a href=\"%sfiles.html\">Files</a>", relpath);
269+	if (hasreadme)
270+		fprintf(fp, " | <a href=\"%sfile/README.html\">README</a>", relpath);
271+	if (haslicense)
272+		fprintf(fp, " | <a href=\"%sfile/LICENSE.html\">LICENSE</a>", relpath);
273+	fputs("</td></tr></table>\n<hr/><div id=\"content\">\n", fp);
274+
275+	return 0;
276+}
277+
278+int
279+writefooter(FILE *fp)
280+{
281+	return !fputs("</div></body>\n</html>", fp);
282+}
283+
284+void
285+writeblobhtml(FILE *fp, const git_blob *blob)
286+{
287+	off_t i = 0;
288+	size_t n = 1;
289+	char *nfmt = "<a href=\"#l%d\" id=\"l%d\">%d</a>\n";
290+	const char *s = git_blob_rawcontent(blob);
291+	git_off_t len = git_blob_rawsize(blob);
292+
293+	fputs("<table id=\"blob\"><tr><td class=\"num\"><pre>\n", fp);
294+
295+	if (len) {
296+		fprintf(fp, nfmt, n, n, n);
297+		while (i < len - 1) {
298+			if (s[i] == '\n') {
299+				n++;
300+				fprintf(fp, nfmt, n, n, n);
301+			}
302+			i++;
303+		}
304+	}
305+
306+	fputs("</pre></td><td><pre>\n", fp);
307+	xmlencode(fp, s, (size_t)len);
308+	fputs("</pre></td></tr></table>\n", fp);
309+}
310+
311+void
312+printcommit(FILE *fp, struct commitinfo *ci)
313+{
314+	fprintf(fp, "<b>commit</b> <a href=\"%scommit/%s.html\">%s</a>\n",
315+		relpath, ci->oid, ci->oid);
316+
317+	if (ci->parentoid[0])
318+		fprintf(fp, "<b>parent</b> <a href=\"%scommit/%s.html\">%s</a>\n",
319+			relpath, ci->parentoid, ci->parentoid);
320+
321+#if 0
322+	if ((count = (int)git_commit_parentcount(commit)) > 1) {
323+		fprintf(fp, "<b>Merge:</b>");
324+		for (i = 0; i < count; i++) {
325+			git_oid_tostr(buf, 8, git_commit_parent_id(commit, i));
326+			fprintf(fp, " <a href=\"%scommit/%s.html\">%s</a>",
327+				relpath, buf, buf);
328+		}
329+		fputc('\n', fp);
330+	}
331+#endif
332+	if (ci->author) {
333+		fprintf(fp, "<b>Author:</b> ");
334+		xmlencode(fp, ci->author->name, strlen(ci->author->name));
335+		fprintf(fp, " &lt;<a href=\"mailto:");
336+		xmlencode(fp, ci->author->email, strlen(ci->author->email));
337+		fputs("\">", fp);
338+		xmlencode(fp, ci->author->email, strlen(ci->author->email));
339+		fputs("</a>&gt;\n<b>Date:</b>   ", fp);
340+		printtime(fp, &(ci->author->when));
341+		fputc('\n', fp);
342+	}
343+	fputc('\n', fp);
344+
345+	if (ci->msg)
346+		xmlencode(fp, ci->msg, strlen(ci->msg));
347+
348+	fputc('\n', fp);
349+}
350+
351+void
352+printshowfile(struct commitinfo *ci)
353+{
354+	const git_diff_delta *delta;
355+	const git_diff_hunk *hunk;
356+	const git_diff_line *line;
357+	git_patch *patch;
358+	git_buf statsbuf;
359+	size_t ndeltas, nhunks, nhunklines;
360+	FILE *fp;
361+	size_t i, j, k;
362+	char path[PATH_MAX];
363+
364+	snprintf(path, sizeof(path), "commit/%s.html", ci->oid);
365+	/* check if file exists if so skip it */
366+	if (!access(path, F_OK))
367+		return;
368+
369+	fp = efopen(path, "w");
370+	writeheader(fp);
371+	fputs("<pre>\n", fp);
372+	printcommit(fp, ci);
373+
374+	memset(&statsbuf, 0, sizeof(statsbuf));
375+
376+	/* diff stat */
377+	if (ci->stats) {
378+		if (!git_diff_stats_to_buf(&statsbuf, ci->stats,
379+		    GIT_DIFF_STATS_FULL | GIT_DIFF_STATS_SHORT, 80)) {
380+			if (statsbuf.ptr && statsbuf.ptr[0]) {
381+				fprintf(fp, "<b>Diffstat:</b>\n");
382+				fputs(statsbuf.ptr, fp);
383+			}
384+		}
385+	}
386+
387+	fputs("<hr/>", fp);
388+
389+	ndeltas = git_diff_num_deltas(ci->diff);
390+	for (i = 0; i < ndeltas; i++) {
391+		if (git_patch_from_diff(&patch, ci->diff, i)) {
392+			git_patch_free(patch);
393+			break;
394+		}
395+
396+		delta = git_patch_get_delta(patch);
397+		fprintf(fp, "<b>diff --git a/<a href=\"%sfile/%s.html\">%s</a> b/<a href=\"%sfile/%s.html\">%s</a></b>\n",
398+			relpath, delta->old_file.path, delta->old_file.path,
399+			relpath, delta->new_file.path, delta->new_file.path);
400+
401+		/* check binary data */
402+		if (delta->flags & GIT_DIFF_FLAG_BINARY) {
403+			fputs("Binary files differ\n", fp);
404+			git_patch_free(patch);
405+			continue;
406+		}
407+
408+		nhunks = git_patch_num_hunks(patch);
409+		for (j = 0; j < nhunks; j++) {
410+			if (git_patch_get_hunk(&hunk, &nhunklines, patch, j))
411+				break;
412+
413+			fprintf(fp, "<span class=\"h\">%s</span>\n", hunk->header);
414+
415+			for (k = 0; ; k++) {
416+				if (git_patch_get_line_in_hunk(&line, patch, j, k))
417+					break;
418+				if (line->old_lineno == -1)
419+					fprintf(fp, "<a href=\"#h%zu-%zu\" id=\"h%zu-%zu\" class=\"i\">+",
420+						j, k, j, k);
421+				else if (line->new_lineno == -1)
422+					fprintf(fp, "<a href=\"#h%zu-%zu\" id=\"h%zu-%zu\" class=\"d\">-",
423+						j, k, j, k);
424+				else
425+					fputc(' ', fp);
426+				xmlencode(fp, line->content, line->content_len);
427+				if (line->old_lineno == -1 || line->new_lineno == -1)
428+					fputs("</a>", fp);
429+			}
430+		}
431+		git_patch_free(patch);
432+	}
433+	git_buf_free(&statsbuf);
434+
435+	fputs( "</pre>\n", fp);
436+	writefooter(fp);
437+	fclose(fp);
438+	return;
439+}
440+
441+void
442+writelog(FILE *fp)
443+{
444+	struct commitinfo *ci;
445+	git_revwalk *w = NULL;
446+	git_oid id;
447+	size_t len;
448+
449+	mkdir("commit", 0755);
450+
451+	git_revwalk_new(&w, repo);
452+	git_revwalk_push_head(w);
453+	git_revwalk_sorting(w, GIT_SORT_TIME);
454+	git_revwalk_simplify_first_parent(w);
455+
456+	fputs("<table id=\"log\"><thead>\n<tr><td>Age</td><td>Commit message</td>"
457+		  "<td>Author</td><td>Files</td><td class=\"num\">+</td>"
458+		  "<td class=\"num\">-</td></tr>\n</thead><tbody>\n", fp);
459+	while (!git_revwalk_next(&id, w)) {
460+		relpath = "";
461+
462+		if (!(ci = commitinfo_getbyoid(&id)))
463+			break;
464+
465+		fputs("<tr><td>", fp);
466+		if (ci->author)
467+			printtimeshort(fp, &(ci->author->when));
468+		fputs("</td><td>", fp);
469+		if (ci->summary) {
470+			fprintf(fp, "<a href=\"%scommit/%s.html\">", relpath, ci->oid);
471+			if ((len = strlen(ci->summary)) > summarylen) {
472+				xmlencode(fp, ci->summary, summarylen - 1);
473+				fputs("…", fp);
474+			} else {
475+				xmlencode(fp, ci->summary, len);
476+			}
477+			fputs("</a>", fp);
478+		}
479+		fputs("</td><td>", fp);
480+		if (ci->author)
481+			xmlencode(fp, ci->author->name, strlen(ci->author->name));
482+		fputs("</td><td class=\"num\">", fp);
483+		fprintf(fp, "%zu", ci->filecount);
484+		fputs("</td><td class=\"num\">", fp);
485+		fprintf(fp, "+%zu", ci->addcount);
486+		fputs("</td><td class=\"num\">", fp);
487+		fprintf(fp, "-%zu", ci->delcount);
488+		fputs("</td></tr>\n", fp);
489+
490+		relpath = "../";
491+		printshowfile(ci);
492+
493+		commitinfo_free(ci);
494+	}
495+	fprintf(fp, "</tbody></table>");
496+
497+	git_revwalk_free(w);
498+	relpath = "";
499+}
500+
501+void
502+printcommitatom(FILE *fp, struct commitinfo *ci)
503+{
504+	fputs("<entry>\n", fp);
505+
506+	fprintf(fp, "<id>%s</id>\n", ci->oid);
507+	if (ci->author) {
508+		fputs("<updated>", fp);
509+		printtimez(fp, &(ci->author->when));
510+		fputs("</updated>\n", fp);
511+	}
512+	if (ci->summary) {
513+		fputs("<title type=\"text\">", fp);
514+		xmlencode(fp, ci->summary, strlen(ci->summary));
515+		fputs("</title>\n", fp);
516+	}
517+
518+	fputs("<content type=\"text\">", fp);
519+	fprintf(fp, "commit %s\n", ci->oid);
520+	if (ci->parentoid[0])
521+		fprintf(fp, "parent %s\n", ci->parentoid);
522+
523+#if 0
524+	if ((count = (int)git_commit_parentcount(commit)) > 1) {
525+		fprintf(fp, "Merge:");
526+		for (i = 0; i < count; i++) {
527+			git_oid_tostr(buf, 8, git_commit_parent_id(commit, i));
528+			fprintf(fp, " %s", buf);
529+		}
530+		fputc('\n', fp);
531+	}
532+#endif
533+
534+	if (ci->author) {
535+		fprintf(fp, "Author: ");
536+		xmlencode(fp, ci->author->name, strlen(ci->author->name));
537+		fprintf(fp, " &lt;");
538+		xmlencode(fp, ci->author->email, strlen(ci->author->email));
539+		fprintf(fp, "&gt;\nDate:   ");
540+		printtime(fp, &(ci->author->when));
541+	}
542+	fputc('\n', fp);
543+
544+	if (ci->msg)
545+		xmlencode(fp, ci->msg, strlen(ci->msg));
546+	fputs("\n</content>\n", fp);
547+	if (ci->author) {
548+		fputs("<author><name>", fp);
549+		xmlencode(fp, ci->author->name, strlen(ci->author->name));
550+		fputs("</name>\n<email>", fp);
551+		xmlencode(fp, ci->author->email, strlen(ci->author->email));
552+		fputs("</email>\n</author>\n", fp);
553+	}
554+	fputs("</entry>\n", fp);
555+}
556+
557+int
558+writeatom(FILE *fp)
559+{
560+	struct commitinfo *ci;
561+	git_revwalk *w = NULL;
562+	git_oid id;
563+	size_t i, m = 100; /* max */
564+
565+	fputs("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
566+	      "<feed xmlns=\"http://www.w3.org/2005/Atom\">\n<title>", fp);
567+	xmlencode(fp, name, strlen(name));
568+	fputs(", branch master</title>\n<subtitle>", fp);
569+
570+	xmlencode(fp, description, strlen(description));
571+	fputs("</subtitle>\n", fp);
572+
573+	git_revwalk_new(&w, repo);
574+	git_revwalk_push_head(w);
575+	git_revwalk_sorting(w, GIT_SORT_TIME);
576+	git_revwalk_simplify_first_parent(w);
577+
578+	for (i = 0; i < m && !git_revwalk_next(&id, w); i++) {
579+		if (!(ci = commitinfo_getbyoid(&id)))
580+			break;
581+		printcommitatom(fp, ci);
582+		commitinfo_free(ci);
583+	}
584+	git_revwalk_free(w);
585+
586+	fputs("</feed>", fp);
587+
588+	return 0;
589+}
590+
591+int
592+writeblob(git_object *obj, const char *filename, git_off_t filesize)
593+{
594+	char fpath[PATH_MAX];
595+	char tmp[PATH_MAX] = "";
596+	char *d, *p;
597+	FILE *fp;
598+
599+	snprintf(fpath, sizeof(fpath), "file/%s.html", filename);
600+	d = xdirname(fpath);
601+	if (mkdirp(d)) {
602+		free(d);
603+		return 1;
604+	}
605+	free(d);
606+
607+	p = fpath;
608+	while (*p) {
609+		if (*p == '/')
610+			strlcat(tmp, "../", sizeof(tmp));
611+		p++;
612+	}
613+	relpath = tmp;
614+
615+	fp = efopen(fpath, "w");
616+	writeheader(fp);
617+	fputs("<p> ", fp);
618+	xmlencode(fp, filename, strlen(filename));
619+	fprintf(fp, " (%" PRIu32 "b)", filesize);
620+	fputs("</p><hr/>", fp);
621+
622+	if (git_blob_is_binary((git_blob *)obj)) {
623+		fprintf(fp, "<p>Binary file</p>\n");
624+	} else {
625+		writeblobhtml(fp, (git_blob *)obj);
626+		if (ferror(fp))
627+			err(1, "fwrite");
628+	}
629+	writefooter(fp);
630+	fclose(fp);
631+
632+	relpath = "";
633+
634+	return 0;
635+}
636+
637+const char *
638+filemode(git_filemode_t m)
639+{
640+	static char mode[11];
641+
642+	memset(mode, '-', sizeof(mode) - 1);
643+	mode[10] = '\0';
644+
645+	if (S_ISREG(m))
646+		mode[0] = '-';
647+	else if (S_ISBLK(m))
648+		mode[0] = 'b';
649+	else if (S_ISCHR(m))
650+		mode[0] = 'c';
651+	else if (S_ISDIR(m))
652+		mode[0] = 'd';
653+	else if (S_ISFIFO(m))
654+		mode[0] = 'p';
655+	else if (S_ISLNK(m))
656+		mode[0] = 'l';
657+	else if (S_ISSOCK(m))
658+		mode[0] = 's';
659+	else
660+		mode[0] = '?';
661+
662+	if (m & S_IRUSR) mode[1] = 'r';
663+	if (m & S_IWUSR) mode[2] = 'w';
664+	if (m & S_IXUSR) mode[3] = 'x';
665+	if (m & S_IRGRP) mode[4] = 'r';
666+	if (m & S_IWGRP) mode[5] = 'w';
667+	if (m & S_IXGRP) mode[6] = 'x';
668+	if (m & S_IROTH) mode[7] = 'r';
669+	if (m & S_IWOTH) mode[8] = 'w';
670+	if (m & S_IXOTH) mode[9] = 'x';
671+
672+	if (m & S_ISUID) mode[3] = (mode[3] == 'x') ? 's' : 'S';
673+	if (m & S_ISGID) mode[6] = (mode[6] == 'x') ? 's' : 'S';
674+	if (m & S_ISVTX) mode[9] = (mode[9] == 'x') ? 't' : 'T';
675+
676+	return mode;
677+}
678+
679+int
680+writefilestree(FILE *fp, git_tree *tree, const char *path)
681+{
682+	const git_tree_entry *entry = NULL;
683+	const char *filename;
684+	char filepath[PATH_MAX];
685+	git_object *obj = NULL;
686+	git_off_t filesize;
687+	size_t count, i;
688+	int ret;
689+
690+	count = git_tree_entrycount(tree);
691+	for (i = 0; i < count; i++) {
692+		if (!(entry = git_tree_entry_byindex(tree, i)))
693+			return -1;
694+
695+		filename = git_tree_entry_name(entry);
696+		if (git_tree_entry_to_object(&obj, repo, entry))
697+			return -1;
698+		switch (git_object_type(obj)) {
699+		case GIT_OBJ_BLOB:
700+			break;
701+		case GIT_OBJ_TREE:
702+			ret = writefilestree(fp, (git_tree *)obj, filename);
703+			git_object_free(obj);
704+			if (ret)
705+				return ret;
706+			continue;
707+		default:
708+			git_object_free(obj);
709+			continue;
710+		}
711+		if (path[0]) {
712+			snprintf(filepath, sizeof(filepath), "%s/%s", path, filename);
713+			filename = filepath;
714+		}
715+
716+		filesize = git_blob_rawsize((git_blob *)obj);
717+
718+		fputs("<tr><td>", fp);
719+		fprintf(fp, "%s", filemode(git_tree_entry_filemode(entry)));
720+		fprintf(fp, "</td><td><a href=\"%sfile/", relpath);
721+		xmlencode(fp, filename, strlen(filename));
722+		fputs(".html\">", fp);
723+		xmlencode(fp, filename, strlen(filename));
724+		fputs("</a></td><td class=\"num\">", fp);
725+		fprintf(fp, "%" PRIu32, filesize);
726+		fputs("</td></tr>\n", fp);
727+
728+		writeblob(obj, filename, filesize);
729+	}
730+
731+	return 0;
732+}
733+
734+int
735+writefiles(FILE *fp)
736+{
737+	const git_oid *id;
738+	git_tree *tree = NULL;
739+	git_object *obj = NULL;
740+	git_commit *commit = NULL;
741+
742+	fputs("<table id=\"files\"><thead>\n<tr>"
743+	      "<td>Mode</td><td>Name</td><td class=\"num\">Size</td>"
744+	      "</tr>\n</thead><tbody>\n", fp);
745+
746+	if (git_revparse_single(&obj, repo, "HEAD"))
747+		return -1;
748+	id = git_object_id(obj);
749+	if (git_commit_lookup(&commit, repo, id))
750+		return -1;
751+	if (git_commit_tree(&tree, commit)) {
752+		git_commit_free(commit);
753+		return -1;
754+	}
755+	git_commit_free(commit);
756+
757+	writefilestree(fp, tree, "");
758+
759+	git_commit_free(commit);
760+	git_tree_free(tree);
761+
762+	fputs("</tbody></table>", fp);
763+
764+	return 0;
765+}
766+
767+int
768+main(int argc, char *argv[])
769+{
770+	git_object *obj = NULL;
771+	const git_error *e = NULL;
772+	FILE *fp, *fpread;
773+	char path[PATH_MAX], *p;
774+	int status;
775+
776+	if (argc != 2) {
777+		fprintf(stderr, "%s <repodir>\n", argv[0]);
778+		return 1;
779+	}
780+	repodir = argv[1];
781+
782+	git_libgit2_init();
783+
784+	if ((status = git_repository_open_ext(&repo, repodir,
785+		GIT_REPOSITORY_OPEN_NO_SEARCH, NULL)) < 0) {
786+		e = giterr_last();
787+		fprintf(stderr, "error %d/%d: %s\n", status, e->klass, e->message);
788+		return status;
789+	}
790+
791+	/* use directory name as name */
792+	p = xbasename(repodir);
793+	snprintf(name, sizeof(name), "%s", p);
794+	free(p);
795+
796+	/* read description or .git/description */
797+	snprintf(path, sizeof(path), "%s%s%s",
798+		repodir, repodir[strlen(repodir)] == '/' ? "" : "/", "description");
799+	if (!(fpread = fopen(path, "r"))) {
800+		snprintf(path, sizeof(path), "%s%s%s",
801+			repodir, repodir[strlen(repodir)] == '/' ? "" : "/", ".git/description");
802+		fpread = fopen(path, "r");
803+	}
804+	if (fpread) {
805+		if (!fgets(description, sizeof(description), fpread))
806+			description[0] = '\0';
807+		fclose(fpread);
808+	}
809+
810+	/* read url or .git/url */
811+	snprintf(path, sizeof(path), "%s%s%s",
812+		repodir, repodir[strlen(repodir)] == '/' ? "" : "/", "url");
813+	if (!(fpread = fopen(path, "r"))) {
814+		snprintf(path, sizeof(path), "%s%s%s",
815+			repodir, repodir[strlen(repodir)] == '/' ? "" : "/", ".git/url");
816+		fpread = fopen(path, "r");
817+	}
818+	if (fpread) {
819+		if (!fgets(cloneurl, sizeof(cloneurl), fpread))
820+			cloneurl[0] = '\0';
821+		fclose(fpread);
822+	}
823+
824+	/* check LICENSE */
825+	haslicense = !git_revparse_single(&obj, repo, "HEAD:LICENSE");
826+	git_object_free(obj);
827+	/* check README */
828+	hasreadme = !git_revparse_single(&obj, repo, "HEAD:README");
829+	git_object_free(obj);
830+
831+	fp = efopen("log.html", "w");
832+	writeheader(fp);
833+	writelog(fp);
834+	writefooter(fp);
835+	fclose(fp);
836+
837+	fp = efopen("files.html", "w");
838+	writeheader(fp);
839+	writefiles(fp);
840+	writefooter(fp);
841+	fclose(fp);
842+
843+	/* Atom feed */
844+	fp = efopen("atom.xml", "w");
845+	writeatom(fp);
846+	fclose(fp);
847+
848+	/* cleanup */
849+	git_repository_free(repo);
850+	git_libgit2_shutdown();
851+
852+	return 0;
853+}