stagit

static git repository generator

git clone https://9o.is/git/stagit.git

stagit-index.c

(8487B)


      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] = "Git Repositories";
     16 static char *name = "";
     17 static char owner[255];
     18 
     19 /* Handle read or write errors for a FILE * stream */
     20 void
     21 checkfileerror(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 
     29 void
     30 joinpath(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. */
     42 void
     43 percentencode(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. */
     64 void
     65 xmlencode(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 
     81 void
     82 printtimeshort(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 
     95 void
     96 writeheader(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>Git Repositories - qhis</title>\n", fp);
    103 	fputs("<meta name=\"darkreader-lock\">\n", fp);
    104 	fputs("<link rel=\"stylesheet\" type=\"text/css\" href=\"/style.css\" />\n", fp);
    105 	fputs("<link rel=\"apple-touch-icon\" sizes=\"180x180\" href=\"/apple-touch-icon.png\" />\n", fp);
    106 	fputs("<link rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/favicon-32x32.png\" />\n", fp);
    107 	fputs("<link rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"/favicon-16x16.png\" />\n", fp);
    108 	fputs("<link rel=\"icon\" type=\"image/x-icon\" href=\"/favicon.ico\" />\n", fp);
    109 	fputs("<link rel=\"manifest\" href=\"/site.webmanifest\" />\n", fp);
    110 	fputs("</head>\n<body>\n", fp);
    111 	fputs("<nav aria-label=\"Main\"><div class=\"nav-inner\"><ul>\n", fp);
    112 	fputs("<li><a href=\"/\">\n", fp);
    113 	fputs("<svg aria-hidden=\"true\" focusable=\"false\" viewBox=\"60 140 395 235\" width=\"1.65em\" height=\"1em\">\n", fp);
    114 	fputs("<path fill=\"#89B4F9\" d=\"M64.835068,227.999908 C64.835060,201.044281 64.835060,174.588654 64.835060,147.827759 C119.952759,147.827759 174.637543,147.827759 229.576569,147.827759 C229.576569,221.044449 229.576569,293.936920 229.576569,367.162964 C211.413635,367.162964 193.487869,367.162964 174.971115,367.162964 C174.971115,349.200623 174.971115,331.143860 174.971115,312.670959 C138.115814,312.670959 101.845200,312.670959 64.835068,312.670959 C64.835068,284.334381 64.835068,256.417145 64.835068,227.999908 M119.817665,228.499924 C119.817665,237.981033 119.817665,247.462143 119.817665,257.209167 C138.278030,257.209167 156.353180,257.209167 174.638031,257.209167 C174.638031,238.965073 174.638031,221.035233 174.638031,202.790985 C156.353516,202.790985 138.278290,202.790985 119.817665,202.790985 C119.817665,211.203720 119.817665,219.351807 119.817665,228.499924 z\"/>\n", fp);
    115 	fputs("<path fill=\"#89B4F9\" d=\"M430.999786,202.503693 C437.444763,202.503662 443.389832,202.503662 449.575439,202.503662 C449.575439,257.714081 449.575439,312.279022 449.575439,367.170105 C394.745087,367.170105 340.163940,367.170105 285.208710,367.170105 C285.208710,312.545074 285.208710,257.842651 285.208710,202.503723 C333.674988,202.503723 382.087433,202.503723 430.999786,202.503693 M380.489105,257.476349 C367.047089,257.476349 353.605072,257.476349 340.041931,257.476349 C340.041931,276.088287 340.041931,294.037903 340.041931,312.185791 C358.345459,312.185791 376.423187,312.185791 394.602081,312.185791 C394.602081,293.916534 394.602081,275.986145 394.602081,257.476410 C390.074432,257.476410 385.778351,257.476410 380.489105,257.476349 z\"/>\n", fp);
    116 	fputs("</svg>\n", fp);
    117 	fputs("Blog</a></li>\n", fp);
    118 	fputs("<li><a href=\"/git\">Git</a></li>\n", fp);
    119 	fputs("</ul></div></nav>\n", fp);
    120 	fputs("<main class=\"git-repos\">\n", fp);
    121 	fputs("<header class=\"main\">\n<h1>", fp);
    122 	xmlencode(fp, description, strlen(description));
    123 	fputs("</h1>\n<p>Browse and clone my public git projects</p>\n", fp);
    124 	fputs("</header>\n<ol>\n", fp);
    125 }
    126 
    127 void
    128 writefooter(FILE *fp)
    129 {
    130 	fputs("</ol>\n</main>\n</body>\n</html>\n", fp);
    131 }
    132 
    133 int
    134 writelog(FILE *fp)
    135 {
    136 	git_commit *commit = NULL;
    137 	const git_signature *author;
    138 	git_revwalk *w = NULL;
    139 	git_oid id;
    140 	char *stripped_name = NULL, *p;
    141 	int ret = 0;
    142 
    143 	git_revwalk_new(&w, repo);
    144 	git_revwalk_push_head(w);
    145 
    146 	if (git_revwalk_next(&id, w) ||
    147 	    git_commit_lookup(&commit, repo, &id)) {
    148 		ret = -1;
    149 		goto err;
    150 	}
    151 
    152 	author = git_commit_author(commit);
    153 
    154 	/* strip .git suffix */
    155 	if (!(stripped_name = strdup(name)))
    156 		err(1, "strdup");
    157 	if ((p = strrchr(stripped_name, '.')))
    158 		if (!strcmp(p, ".git"))
    159 			*p = '\0';
    160 
    161 	fputs("<li><article>\n", fp);
    162 	fputs("<h2><a href=\"", fp);
    163 	percentencode(fp, stripped_name, strlen(stripped_name));
    164 	fputs("/log.html\">", fp);
    165 	xmlencode(fp, stripped_name, strlen(stripped_name));
    166 	fputs("</a></h2>\n", fp);
    167 	if (author) {
    168 		fputs("<time>", fp);
    169 		printtimeshort(fp, &(author->when));
    170 		fputs("</time>\n", fp);
    171 	}
    172 	fputs("<p>", fp);
    173 	xmlencode(fp, description, strlen(description));
    174 	fputs("</p>\n", fp);
    175 	fputs("</article></li>\n", fp);
    176 
    177 	git_commit_free(commit);
    178 err:
    179 	git_revwalk_free(w);
    180 	free(stripped_name);
    181 
    182 	return ret;
    183 }
    184 
    185 int
    186 main(int argc, char *argv[])
    187 {
    188 	FILE *fp;
    189 	char path[PATH_MAX], repodirabs[PATH_MAX + 1];
    190 	const char *repodir;
    191 	int i, ret = 0;
    192 
    193 	if (argc < 2) {
    194 		fprintf(stderr, "usage: %s [repodir...]\n", argv[0]);
    195 		return 1;
    196 	}
    197 
    198 	/* do not search outside the git repository:
    199 	   GIT_CONFIG_LEVEL_APP is the highest level currently */
    200 	git_libgit2_init();
    201 	for (i = 1; i <= GIT_CONFIG_LEVEL_APP; i++)
    202 		git_libgit2_opts(GIT_OPT_SET_SEARCH_PATH, i, "");
    203 	/* do not require the git repository to be owned by the current user */
    204 	git_libgit2_opts(GIT_OPT_SET_OWNER_VALIDATION, 0);
    205 
    206 #ifdef __OpenBSD__
    207 	if (pledge("stdio rpath", NULL) == -1)
    208 		err(1, "pledge");
    209 #endif
    210 
    211 	writeheader(stdout);
    212 
    213 	for (i = 1; i < argc; i++) {
    214 		repodir = argv[i];
    215 		if (!realpath(repodir, repodirabs))
    216 			err(1, "realpath");
    217 
    218 		if (git_repository_open_ext(&repo, repodir,
    219 		    GIT_REPOSITORY_OPEN_NO_SEARCH, NULL)) {
    220 			fprintf(stderr, "%s: cannot open repository\n", argv[0]);
    221 			ret = 1;
    222 			continue;
    223 		}
    224 
    225 		/* use directory name as name */
    226 		if ((name = strrchr(repodirabs, '/')))
    227 			name++;
    228 		else
    229 			name = "";
    230 
    231 		/* read description or .git/description */
    232 		joinpath(path, sizeof(path), repodir, "description");
    233 		if (!(fp = fopen(path, "r"))) {
    234 			joinpath(path, sizeof(path), repodir, ".git/description");
    235 			fp = fopen(path, "r");
    236 		}
    237 		description[0] = '\0';
    238 		if (fp) {
    239 			if (!fgets(description, sizeof(description), fp))
    240 				description[0] = '\0';
    241 			checkfileerror(fp, "description", 'r');
    242 			fclose(fp);
    243 		}
    244 
    245 		/* read owner or .git/owner */
    246 		joinpath(path, sizeof(path), repodir, "owner");
    247 		if (!(fp = fopen(path, "r"))) {
    248 			joinpath(path, sizeof(path), repodir, ".git/owner");
    249 			fp = fopen(path, "r");
    250 		}
    251 		owner[0] = '\0';
    252 		if (fp) {
    253 			if (!fgets(owner, sizeof(owner), fp))
    254 				owner[0] = '\0';
    255 			checkfileerror(fp, "owner", 'r');
    256 			fclose(fp);
    257 			owner[strcspn(owner, "\n")] = '\0';
    258 		}
    259 		writelog(stdout);
    260 	}
    261 	writefooter(stdout);
    262 
    263 	/* cleanup */
    264 	git_repository_free(repo);
    265 	git_libgit2_shutdown();
    266 
    267 	checkfileerror(stdout, "<stdout>", 'w');
    268 
    269 	return ret;
    270 }