stagit

git site generator
Contents

stagit-index.c

6.4 kB
  1#include <err.h>
  2#include <limits.h>
  3#include <stdio.h>
  4#include <stdlib.h>
  5#include <string.h>
  6#include <time.h>
  7#include <unistd.h>
  8
  9#include <git2.h>
 10
 11static git_repository *repo;
 12
 13static const char *relpath = "";
 14
 15static char description[255] = "Repositories";
 16static char *name = "";
 17static char owner[255];
 18
 19/* Handle read or write errors for a FILE * stream */
 20void
 21checkfileerror(FILE *fp, const char *name, int mode)
 22{
 23	if (mode == 'r' && ferror(fp))
 24		errx(1, "read error: %s", name);
 25	else if (mode == 'w' && (fflush(fp) || ferror(fp)))
 26		errx(1, "write error: %s", name);
 27}
 28
 29void
 30joinpath(char *buf, size_t bufsiz, const char *path, const char *path2)
 31{
 32	int r;
 33
 34	r = snprintf(buf, bufsiz, "%s%s%s",
 35		path, path[0] && path[strlen(path) - 1] != '/' ? "/" : "", path2);
 36	if (r < 0 || (size_t)r >= bufsiz)
 37		errx(1, "path truncated: '%s%s%s'",
 38			path, path[0] && path[strlen(path) - 1] != '/' ? "/" : "", path2);
 39}
 40
 41/* Percent-encode, see RFC3986 section 2.1. */
 42void
 43percentencode(FILE *fp, const char *s, size_t len)
 44{
 45	static char tab[] = "0123456789ABCDEF";
 46	unsigned char uc;
 47	size_t i;
 48
 49	for (i = 0; *s && i < len; s++, i++) {
 50		uc = *s;
 51		/* NOTE: do not encode '/' for paths or ",-." */
 52		if (uc < ',' || uc >= 127 || (uc >= ':' && uc <= '@') ||
 53		    uc == '[' || uc == ']') {
 54			putc('%', fp);
 55			putc(tab[(uc >> 4) & 0x0f], fp);
 56			putc(tab[uc & 0x0f], fp);
 57		} else {
 58			putc(uc, fp);
 59		}
 60	}
 61}
 62
 63/* Escape characters below as HTML 2.0 / XML 1.0. */
 64void
 65xmlencode(FILE *fp, const char *s, size_t len)
 66{
 67	size_t i;
 68
 69	for (i = 0; *s && i < len; s++, i++) {
 70		switch(*s) {
 71		case '<':  fputs("&lt;",   fp); break;
 72		case '>':  fputs("&gt;",   fp); break;
 73		case '\'': fputs("&#39;" , fp); break;
 74		case '&':  fputs("&amp;",  fp); break;
 75		case '"':  fputs("&quot;", fp); break;
 76		default:   putc(*s, fp);
 77		}
 78	}
 79}
 80
 81void
 82printtimeshort(FILE *fp, const git_time *intime)
 83{
 84	struct tm *intm;
 85	time_t t;
 86	char out[32];
 87
 88	t = (time_t)intime->time;
 89	if (!(intm = gmtime(&t)))
 90		return;
 91	strftime(out, sizeof(out), "%Y-%m-%d %H:%M", intm);
 92	fputs(out, fp);
 93}
 94
 95void
 96writeheader(FILE *fp)
 97{
 98	fputs("<!DOCTYPE html>\n"
 99		"<html>\n<head>\n"
100		"<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n"
101		"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n"
102		"<title>", fp);
103	xmlencode(fp, description, strlen(description));
104	fprintf(fp, "</title>\n<link rel=\"icon\" type=\"image/png\" href=\"%sfavicon.png\" />\n", relpath);
105	fprintf(fp, "<link rel=\"stylesheet\" type=\"text/css\" href=\"%sstyle.css\" />\n", relpath);
106	fputs("</head>\n<body>\n<div id=\"container\">\n<div id=\"sidebar\">\n<a id=\"logo\" href=\"https://git.arjunchoudhary.com\">\n<div id=\"typing\">ARJUN</div>\n<hr id=\"cursor\"/>\n</a>\n<img id=\"profile_img\" src=\"profile_img.png\" alt=\"my_profile_image\"/>\n<a id=\"contact\" href=\"mailto:contact@arjunchoudhary.com\">Contact</a>\n</div>\n<div id=\"main-view\">\n", fp);
107	fprintf(fp, "<h1 id=\"repositories\">");
108	xmlencode(fp, description, strlen(description));
109	fputs("</h1></td></tr>\n</table>\n<div id=\"content\">\n"
110		"<table id=\"index\"><thead>\n"
111		"<tr><td><b>Name</b></td><td><b>Description</b></td><td><b>Owner</b></td>"
112		"<td><b>Last commit</b></td></tr>"
113		"</thead><tbody>\n", fp);
114}
115
116void
117writefooter(FILE *fp)
118{
119	fputs("</tbody>\n</table>\n</div>\n</div>\n</div>\n</body>\n</html>\n", fp);
120}
121
122int
123writelog(FILE *fp)
124{
125	git_commit *commit = NULL;
126	const git_signature *author;
127	git_revwalk *w = NULL;
128	git_oid id;
129	char *stripped_name = NULL, *p;
130	int ret = 0;
131
132	git_revwalk_new(&w, repo);
133	git_revwalk_push_head(w);
134
135	if (git_revwalk_next(&id, w) ||
136	    git_commit_lookup(&commit, repo, &id)) {
137		ret = -1;
138		goto err;
139	}
140
141	author = git_commit_author(commit);
142
143	/* strip .git suffix */
144	if (!(stripped_name = strdup(name)))
145		err(1, "strdup");
146	if ((p = strrchr(stripped_name, '.')))
147		if (!strcmp(p, ".git"))
148			*p = '\0';
149
150	fputs("<tr><td><a href=\"", fp);
151	percentencode(fp, stripped_name, strlen(stripped_name));
152	fputs("/file/README.md.html\">", fp);
153	xmlencode(fp, stripped_name, strlen(stripped_name));
154	fputs("</a></td><td>", fp);
155	xmlencode(fp, description, strlen(description));
156	fputs("</td><td>", fp);
157	xmlencode(fp, owner, strlen(owner));
158	fputs("</td><td>", fp);
159	if (author)
160		printtimeshort(fp, &(author->when));
161	fputs("</td></tr>", fp);
162
163	git_commit_free(commit);
164err:
165	git_revwalk_free(w);
166	free(stripped_name);
167
168	return ret;
169}
170
171int
172main(int argc, char *argv[])
173{
174	FILE *fp;
175	char path[PATH_MAX], repodirabs[PATH_MAX + 1];
176	const char *repodir;
177	int i, ret = 0;
178
179	if (argc < 2) {
180		fprintf(stderr, "%s [repodir...]\n", argv[0]);
181		return 1;
182	}
183
184	/* do not search outside the git repository:
185	   GIT_CONFIG_LEVEL_APP is the highest level currently */
186	git_libgit2_init();
187	for (i = 1; i <= GIT_CONFIG_LEVEL_APP; i++)
188		git_libgit2_opts(GIT_OPT_SET_SEARCH_PATH, i, "");
189
190#ifdef __OpenBSD__
191	if (pledge("stdio rpath", NULL) == -1)
192		err(1, "pledge");
193#endif
194
195	writeheader(stdout);
196
197	for (i = 1; i < argc; i++) {
198		repodir = argv[i];
199		if (!realpath(repodir, repodirabs))
200			err(1, "realpath");
201
202		if (git_repository_open_ext(&repo, repodir,
203		    GIT_REPOSITORY_OPEN_NO_SEARCH, NULL)) {
204			fprintf(stderr, "%s: cannot open repository\n", argv[0]);
205			ret = 1;
206			continue;
207		}
208
209		/* use directory name as name */
210		if ((name = strrchr(repodirabs, '/')))
211			name++;
212		else
213			name = "";
214
215		/* read description or .git/description */
216		joinpath(path, sizeof(path), repodir, "description");
217		if (!(fp = fopen(path, "r"))) {
218			joinpath(path, sizeof(path), repodir, ".git/description");
219			fp = fopen(path, "r");
220		}
221		description[0] = '\0';
222		if (fp) {
223			if (!fgets(description, sizeof(description), fp))
224				description[0] = '\0';
225			checkfileerror(fp, "description", 'r');
226			fclose(fp);
227		}
228
229		/* read owner or .git/owner */
230		joinpath(path, sizeof(path), repodir, "owner");
231		if (!(fp = fopen(path, "r"))) {
232			joinpath(path, sizeof(path), repodir, ".git/owner");
233			fp = fopen(path, "r");
234		}
235		owner[0] = '\0';
236		if (fp) {
237			if (!fgets(owner, sizeof(owner), fp))
238				owner[0] = '\0';
239			checkfileerror(fp, "owner", 'r');
240			fclose(fp);
241			owner[strcspn(owner, "\n")] = '\0';
242		}
243		writelog(stdout);
244	}
245	writefooter(stdout);
246
247	/* cleanup */
248	git_repository_free(repo);
249	git_libgit2_shutdown();
250
251	checkfileerror(stdout, "<stdout>", 'w');
252
253	return ret;
254}