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("<", fp); break;
72 case '>': fputs(">", fp); break;
73 case '\'': fputs("'" , fp); break;
74 case '&': fputs("&", fp); break;
75 case '"': fputs(""", 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 }