stagit

My personal fork of stagit https://codemadness.org/stagit.html
git clone https://git.grace.moe/stagit
Log | Files | Refs | README | LICENSE

stagit-index.c (10356B)


      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 
     11 static git_repository *repo;
     12 
     13 static const char *relpath = "";
     14 
     15 static char description[255] = "Repositories";
     16 static char *name = "";
     17 static char owner[255];
     18 
     19 static char * const default_style =
     20 	"body {\n"
     21 	"	color: #000;\n"
     22 	"	background-color: #fff;\n"
     23 	"	font-family: monospace;\n"
     24 	"}\n"
     25 	"\n"
     26 	"h1, h2, h3, h4, h5, h6 {\n"
     27 	"	font-size: 1em;\n"
     28 	"	margin: 0;\n"
     29 	"}\n"
     30 	"\n"
     31 	"img, svg, h1, h2 {\n"
     32 	"	vertical-align: middle;\n"
     33 	"}\n"
     34 	"\n"
     35 	"img {\n"
     36 	"	border: 0;\n"
     37 	"}\n"
     38 	"\n"
     39 	"a:target {\n"
     40 	"	background-color: #ccc;\n"
     41 	"}\n"
     42 	"\n"
     43 	"a.d,\n"
     44 	"a.h,\n"
     45 	"a.i,\n"
     46 	"a.line {\n"
     47 	"	text-decoration: none;\n"
     48 	"}\n"
     49 	"\n"
     50 	"#blob a {\n"
     51 	"	color: #555;\n"
     52 	"}\n"
     53 	"	\n"
     54 	"#blob a:hover {\n"
     55 	"	color: blue;\n"
     56 	"	text-decoration: none;\n"
     57 	"}\n"
     58 	"	\n"
     59 	"table thead td {\n"
     60 	"	font-weight: bold;\n"
     61 	"}\n"
     62 	"\n"
     63 	"table td {\n"
     64 	"	padding: 0 0.4em;\n"
     65 	"}\n"
     66 	"\n"
     67 	"#content table td {\n"
     68 	"	vertical-align: top;\n"
     69 	"	white-space: nowrap;\n"
     70 	"}\n"
     71 	"	\n"
     72 	"#branches tr:hover td,\n"
     73 	"#tags tr:hover td,\n"
     74 	"#index tr:hover td,\n"
     75 	"#log tr:hover td,\n"
     76 	"#files tr:hover td {\n"
     77 	"	background-color: #eee;\n"
     78 	"}\n"
     79 	"	\n"
     80 	"#index tr td:nth-child(2),\n"
     81 	"#tags tr td:nth-child(3),\n"
     82 	"#branches tr td:nth-child(3),\n"
     83 	"#log tr td:nth-child(2) {\n"
     84 	"	white-space: normal;\n"
     85 	"}\n"
     86 	"	\n"
     87 	"td.num {\n"
     88 	"	text-align: right;\n"
     89 	"}\n"
     90 	"	\n"
     91 	".desc {\n"
     92 	"	color: #555;\n"
     93 	"}\n"
     94 	"\n"
     95 	"hr {\n"
     96 	"	border: 0;\n"
     97 	"	border-top: 1px solid #555;\n"
     98 	"	height: 1px;\n"
     99 	"}\n"
    100 	"\n"
    101 	"pre {\n"
    102 	"	font-family: monospace;\n"
    103 	"}\n"
    104 	"\n"
    105 	"pre a.h {\n"
    106 	"	color: #00a;\n"
    107 	"}\n"
    108 	"\n"
    109 	".A,\n"
    110 	"span.i,\n"
    111 	"pre a.i {\n"
    112 	"	color: #070;\n"
    113 	"}\n"
    114 	"\n"
    115 	".D,\n"
    116 	"span.d,\n"
    117 	"pre a.d {\n"
    118 	"	color: #e00;\n"
    119 	"}\n"
    120 	"\n"
    121 	"pre a.h:hover,\n"
    122 	"pre a.i:hover,\n"
    123 	"pre a.d:hover {\n"
    124 	"	text-decoration: none;\n"
    125 	"}\n"
    126 	"\n"
    127 	"@media (prefers-color-scheme: dark) {\n"
    128 	"	body {\n"
    129 	"		background-color: #000;\n"
    130 	"		color: #bdbdbd;\n"
    131 	"	}\n"
    132 	"	hr {\n"
    133 	"		border-color: #222;\n"
    134 	"	}\n"
    135 	"	a {\n"
    136 	"		color: #56c8ff;\n"
    137 	"	}\n"
    138 	"	a:target {\n"
    139 	"		background-color: #222;\n"
    140 	"	}\n"
    141 	"	.desc {\n"
    142 	"		color: #aaa;\n"
    143 	"	}\n"
    144 	"	#blob a {\n"
    145 	"		color: #555;\n"
    146 	"	}\n"
    147 	"	#blob a:target {\n"
    148 	"		color: #eee;\n"
    149 	"	}\n"
    150 	"	#blob a:hover {\n"
    151 	"		color: #56c8ff;\n"
    152 	"	}\n"
    153 	"	pre a.h {\n"
    154 	"		color: #00cdcd;\n"
    155 	"	}\n"
    156 	"	.A,\n"
    157 	"	span.i,\n"
    158 	"	pre a.i {\n"
    159 	"		color: #00cd00;\n"
    160 	"	}\n"
    161 	"	.D,\n"
    162 	"	span.d,\n"
    163 	"	pre a.d {\n"
    164 	"		color: #cd0000;\n"
    165 	"	}\n"
    166 	"	#branches tr:hover td,\n"
    167 	"	#tags tr:hover td,\n"
    168 	"	#index tr:hover td,\n"
    169 	"	#log tr:hover td,\n"
    170 	"	#files tr:hover td {\n"
    171 	"		background-color: #111;\n"
    172 	"	}\n"
    173 	"}\n";
    174 
    175 static char *stylefile;
    176 static char *style = default_style;
    177 
    178 /* Handle read or write errors for a FILE * stream */
    179 void
    180 checkfileerror(FILE *fp, const char *name, int mode)
    181 {
    182 	if (mode == 'r' && ferror(fp))
    183 		errx(1, "read error: %s", name);
    184 	else if (mode == 'w' && (fflush(fp) || ferror(fp)))
    185 		errx(1, "write error: %s", name);
    186 }
    187 
    188 void
    189 joinpath(char *buf, size_t bufsiz, const char *path, const char *path2)
    190 {
    191 	int r;
    192 
    193 	r = snprintf(buf, bufsiz, "%s%s%s",
    194 		path, path[0] && path[strlen(path) - 1] != '/' ? "/" : "", path2);
    195 	if (r < 0 || (size_t)r >= bufsiz)
    196 		errx(1, "path truncated: '%s%s%s'",
    197 			path, path[0] && path[strlen(path) - 1] != '/' ? "/" : "", path2);
    198 }
    199 
    200 /* Percent-encode, see RFC3986 section 2.1. */
    201 void
    202 percentencode(FILE *fp, const char *s, size_t len)
    203 {
    204 	static char tab[] = "0123456789ABCDEF";
    205 	unsigned char uc;
    206 	size_t i;
    207 
    208 	for (i = 0; *s && i < len; s++, i++) {
    209 		uc = *s;
    210 		/* NOTE: do not encode '/' for paths or ",-." */
    211 		if (uc < ',' || uc >= 127 || (uc >= ':' && uc <= '@') ||
    212 		    uc == '[' || uc == ']') {
    213 			putc('%', fp);
    214 			putc(tab[(uc >> 4) & 0x0f], fp);
    215 			putc(tab[uc & 0x0f], fp);
    216 		} else {
    217 			putc(uc, fp);
    218 		}
    219 	}
    220 }
    221 
    222 /* Escape characters below as HTML 2.0 / XML 1.0. */
    223 void
    224 xmlencode(FILE *fp, const char *s, size_t len)
    225 {
    226 	size_t i;
    227 
    228 	for (i = 0; *s && i < len; s++, i++) {
    229 		switch(*s) {
    230 		case '<':  fputs("&lt;",   fp); break;
    231 		case '>':  fputs("&gt;",   fp); break;
    232 		case '\'': fputs("&#39;" , fp); break;
    233 		case '&':  fputs("&amp;",  fp); break;
    234 		case '"':  fputs("&quot;", fp); break;
    235 		default:   putc(*s, fp);
    236 		}
    237 	}
    238 }
    239 
    240 void
    241 printtimeshort(FILE *fp, const git_time *intime)
    242 {
    243 	struct tm *intm;
    244 	time_t t;
    245 	char out[32];
    246 
    247 	t = (time_t)intime->time;
    248 	if (!(intm = gmtime(&t)))
    249 		return;
    250 	strftime(out, sizeof(out), "%Y-%m-%d %H:%M", intm);
    251 	fputs(out, fp);
    252 }
    253 
    254 void
    255 writeheader(FILE *fp)
    256 {
    257 	fputs("<!DOCTYPE html>\n"
    258 		"<html>\n<head>\n"
    259 		"<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n"
    260 		"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n"
    261 		"<title>", fp);
    262 	xmlencode(fp, description, strlen(description));
    263 	fprintf(fp,
    264 		"</title>\n"
    265 		"<link rel=\"apple-touch-icon\" sizes=\"180x180\" href=\"/icons/apple-touch-icon.png\">\n"
    266 		"<link rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/icons/favicon-32x32.png\">\n"
    267 		"<link rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"/icons/favicon-16x16.png\">\n"
    268 		"<link rel=\"manifest\" href=\"/icons/site.webmanifest\">\n"
    269 		"<link rel=\"mask-icon\" href=\"/icons/safari-pinned-tab.svg\" color=\"#ffb6c1\">\n"
    270 		"<link rel=\"shortcut icon\" href=\"/icons/favicon.ico\">\n"
    271 		"<meta name=\"msapplication-TileColor\" content=\"#603cba\">\n"
    272 		"<meta name=\"msapplication-config\" content=\"/icons/browserconfig.xml\">\n"
    273 		"<meta name=\"theme-color\" content=\"#ffffff\">\n");
    274 	fputs("<style>\n", fp);
    275 	fputs(style, fp);
    276 	fputs("</style>\n", fp);
    277 	fputs("</head>\n<body>\n", fp);
    278 	fprintf(fp, "<table>\n<tr><td><a href=\"../\"><svg height=\"32px\" width=\"32px\" xmlns=\"http://www.w3.org/2000/svg\"><circle r=\"16\" cx=\"16\" cy=\"16\" fill=\"lightpink\"></circle></svg></a></td>\n"
    279 	        "<td><span class=\"desc\">");
    280 	xmlencode(fp, description, strlen(description));
    281 	fputs("</span></td></tr><tr><td></td><td>\n"
    282 		"</td></tr>\n</table>\n<hr/>\n<div id=\"content\">\n"
    283 		"<table id=\"index\"><thead>\n"
    284 		"<tr><td><b>Name</b></td><td><b>Description</b></td><td><b>Owner</b></td>"
    285 		"<td><b>Last commit</b></td></tr>"
    286 		"</thead><tbody>\n", fp);
    287 }
    288 
    289 void
    290 writefooter(FILE *fp)
    291 {
    292 	fputs("</tbody>\n</table>\n</div>\n</body>\n</html>\n", fp);
    293 }
    294 
    295 int
    296 writelog(FILE *fp)
    297 {
    298 	git_commit *commit = NULL;
    299 	const git_signature *author;
    300 	git_revwalk *w = NULL;
    301 	git_oid id;
    302 	char *stripped_name = NULL, *p;
    303 	int ret = 0;
    304 
    305 	git_revwalk_new(&w, repo);
    306 	git_revwalk_push_head(w);
    307 
    308 	if (git_revwalk_next(&id, w) ||
    309 	    git_commit_lookup(&commit, repo, &id)) {
    310 		ret = -1;
    311 		goto err;
    312 	}
    313 
    314 	author = git_commit_author(commit);
    315 
    316 	/* strip .git suffix */
    317 	if (!(stripped_name = strdup(name)))
    318 		err(1, "strdup");
    319 	if ((p = strrchr(stripped_name, '.')))
    320 		if (!strcmp(p, ".git"))
    321 			*p = '\0';
    322 
    323 	fputs("<tr><td><a href=\"", fp);
    324 	percentencode(fp, stripped_name, strlen(stripped_name));
    325 	fputs("/log.html\">", fp);
    326 	xmlencode(fp, stripped_name, strlen(stripped_name));
    327 	fputs("</a></td><td>", fp);
    328 	xmlencode(fp, description, strlen(description));
    329 	fputs("</td><td>", fp);
    330 	xmlencode(fp, owner, strlen(owner));
    331 	fputs("</td><td>", fp);
    332 	if (author)
    333 		printtimeshort(fp, &(author->when));
    334 	fputs("</td></tr>", fp);
    335 
    336 	git_commit_free(commit);
    337 err:
    338 	git_revwalk_free(w);
    339 	free(stripped_name);
    340 
    341 	return ret;
    342 }
    343 
    344 int
    345 main(int argc, char *argv[])
    346 {
    347 	FILE *fp, *fpread;
    348 	char path[PATH_MAX], repodirabs[PATH_MAX + 1];
    349 	const char *repodir;
    350 	int i, ret = 0;
    351 
    352 	if (argc < 2) {
    353 		fprintf(stderr, "usage: %s [-s stylefile] [repodir...]\n", argv[0]);
    354 		return 1;
    355 	}
    356 
    357 	/* do not search outside the git repository:
    358 	   GIT_CONFIG_LEVEL_APP is the highest level currently */
    359 	git_libgit2_init();
    360 	for (i = 1; i <= GIT_CONFIG_LEVEL_APP; i++)
    361 		git_libgit2_opts(GIT_OPT_SET_SEARCH_PATH, i, "");
    362 	/* do not require the git repository to be owned by the current user */
    363 	git_libgit2_opts(GIT_OPT_SET_OWNER_VALIDATION, 0);
    364 
    365 #ifdef __OpenBSD__
    366 	if (pledge("stdio rpath", NULL) == -1)
    367 		err(1, "pledge");
    368 #endif
    369 
    370 	for (i = 1; i < argc; i++) {
    371 		if (argv[i][0] != '-') {
    372 			continue;
    373 		}
    374 		if (argv[i][1] == 's') {
    375 			if (i + 1 >= argc)
    376 				fprintf(stderr, "usage: %s [-s stylefile] [repodir...]\n", argv[0]);
    377 			stylefile = argv[++i];
    378 			continue;
    379 		}
    380 		err(1, "usage: %s [-s stylefile] [repodir...]\n", argv[0]);
    381 	}
    382 
    383 	if (stylefile) {
    384 		if (!(fpread = fopen(stylefile, "r")))
    385 			err(1, "fopen: '%s'", stylefile);
    386 
    387 		if (fpread) {
    388 			fseek(fpread, 0, SEEK_END);
    389 			long int len = ftell(fpread);
    390 			rewind(fpread);
    391 			if (len >= 0) {
    392 				style = calloc(1, (size_t)len + 1);
    393 				if (!fread(style, (size_t)len, 1, fpread)) {
    394 					free(style);
    395 					style = default_style;
    396 				}
    397 			}
    398 			checkfileerror(fpread, stylefile, 'r');
    399 			fclose(fpread);
    400 		}
    401 	}
    402 
    403 	writeheader(stdout);
    404 
    405 	for (i = 1; i < argc; i++) {
    406 		if (argv[i][0] == '-') {
    407 			++i;
    408 			continue;
    409 		}
    410 
    411 		repodir = argv[i];
    412 		if (!realpath(repodir, repodirabs))
    413 			err(1, "realpath");
    414 
    415 		if (git_repository_open_ext(&repo, repodir,
    416 		    GIT_REPOSITORY_OPEN_NO_SEARCH, NULL)) {
    417 			fprintf(stderr, "%s: cannot open repository\n", argv[0]);
    418 			ret = 1;
    419 			continue;
    420 		}
    421 
    422 		/* use directory name as name */
    423 		if ((name = strrchr(repodirabs, '/')))
    424 			name++;
    425 		else
    426 			name = "";
    427 
    428 		/* read description or .git/description */
    429 		joinpath(path, sizeof(path), repodir, "description");
    430 		if (!(fp = fopen(path, "r"))) {
    431 			joinpath(path, sizeof(path), repodir, ".git/description");
    432 			fp = fopen(path, "r");
    433 		}
    434 		description[0] = '\0';
    435 		if (fp) {
    436 			if (!fgets(description, sizeof(description), fp))
    437 				description[0] = '\0';
    438 			checkfileerror(fp, "description", 'r');
    439 			fclose(fp);
    440 		}
    441 
    442 		/* read owner or .git/owner */
    443 		joinpath(path, sizeof(path), repodir, "owner");
    444 		if (!(fp = fopen(path, "r"))) {
    445 			joinpath(path, sizeof(path), repodir, ".git/owner");
    446 			fp = fopen(path, "r");
    447 		}
    448 		owner[0] = '\0';
    449 		if (fp) {
    450 			if (!fgets(owner, sizeof(owner), fp))
    451 				owner[0] = '\0';
    452 			checkfileerror(fp, "owner", 'r');
    453 			fclose(fp);
    454 			owner[strcspn(owner, "\n")] = '\0';
    455 		}
    456 		writelog(stdout);
    457 	}
    458 	writefooter(stdout);
    459 
    460 	/* cleanup */
    461 	git_repository_free(repo);
    462 	git_libgit2_shutdown();
    463 
    464 	checkfileerror(stdout, "<stdout>", 'w');
    465 
    466 	return ret;
    467 }