stagit
static git repository generator
git clone https://9o.is/git/stagit.git
stagit.c
(38756B)
1 #include <sys/stat.h>
2 #include <sys/types.h>
3
4 #include <err.h>
5 #include <errno.h>
6 #include <libgen.h>
7 #include <limits.h>
8 #include <stdint.h>
9 #include <stdio.h>
10 #include <stdlib.h>
11 #include <string.h>
12 #include <time.h>
13 #include <unistd.h>
14
15 #include <git2.h>
16
17 #include "compat.h"
18
19 #define LEN(s) (sizeof(s)/sizeof(*s))
20
21 struct deltainfo {
22 git_patch *patch;
23
24 size_t addcount;
25 size_t delcount;
26 };
27
28 struct commitinfo {
29 const git_oid *id;
30
31 char oid[GIT_OID_HEXSZ + 1];
32 char parentoid[GIT_OID_HEXSZ + 1];
33
34 const git_signature *author;
35 const git_signature *committer;
36 const char *summary;
37 const char *msg;
38
39 git_diff *diff;
40 git_commit *commit;
41 git_commit *parent;
42 git_tree *commit_tree;
43 git_tree *parent_tree;
44
45 size_t addcount;
46 size_t delcount;
47 size_t filecount;
48
49 struct deltainfo **deltas;
50 size_t ndeltas;
51 };
52
53 /* reference and associated data for sorting */
54 struct referenceinfo {
55 struct git_reference *ref;
56 struct commitinfo *ci;
57 };
58
59 static git_repository *repo;
60
61 static const char *baseurl = ""; /* base URL to make absolute RSS/Atom URI */
62 static const char *relpath = "";
63 static const char *repodir;
64
65 static char *name = "";
66 static char *strippedname = "";
67 static char description[255];
68 static char cloneurl[1024];
69 static char *submodules;
70 static char *licensefiles[] = { "HEAD:LICENSE", "HEAD:LICENSE.md", "HEAD:COPYING" };
71 static char *license;
72 static char *readmefiles[] = { "HEAD:README", "HEAD:README.md" };
73 static char *readme;
74 static long long nlogcommits = -1; /* -1 indicates not used */
75
76 /* cache */
77 static git_oid lastoid;
78 static char lastoidstr[GIT_OID_HEXSZ + 2]; /* id + newline + NUL byte */
79 static FILE *rcachefp, *wcachefp;
80 static const char *cachefile;
81
82 /* Handle read or write errors for a FILE * stream */
83 void
84 checkfileerror(FILE *fp, const char *name, int mode)
85 {
86 if (mode == 'r' && ferror(fp))
87 errx(1, "read error: %s", name);
88 else if (mode == 'w' && (fflush(fp) || ferror(fp)))
89 errx(1, "write error: %s", name);
90 }
91
92 void
93 joinpath(char *buf, size_t bufsiz, const char *path, const char *path2)
94 {
95 int r;
96
97 r = snprintf(buf, bufsiz, "%s%s%s",
98 path, path[0] && path[strlen(path) - 1] != '/' ? "/" : "", path2);
99 if (r < 0 || (size_t)r >= bufsiz)
100 errx(1, "path truncated: '%s%s%s'",
101 path, path[0] && path[strlen(path) - 1] != '/' ? "/" : "", path2);
102 }
103
104 void
105 deltainfo_free(struct deltainfo *di)
106 {
107 if (!di)
108 return;
109 git_patch_free(di->patch);
110 memset(di, 0, sizeof(*di));
111 free(di);
112 }
113
114 int
115 commitinfo_getstats(struct commitinfo *ci)
116 {
117 struct deltainfo *di;
118 git_diff_options opts;
119 git_diff_find_options fopts;
120 const git_diff_delta *delta;
121 const git_diff_hunk *hunk;
122 const git_diff_line *line;
123 git_patch *patch = NULL;
124 size_t ndeltas, nhunks, nhunklines;
125 size_t i, j, k;
126
127 if (git_tree_lookup(&(ci->commit_tree), repo, git_commit_tree_id(ci->commit)))
128 goto err;
129 if (!git_commit_parent(&(ci->parent), ci->commit, 0)) {
130 if (git_tree_lookup(&(ci->parent_tree), repo, git_commit_tree_id(ci->parent))) {
131 ci->parent = NULL;
132 ci->parent_tree = NULL;
133 }
134 }
135
136 git_diff_init_options(&opts, GIT_DIFF_OPTIONS_VERSION);
137 opts.flags |= GIT_DIFF_DISABLE_PATHSPEC_MATCH |
138 GIT_DIFF_IGNORE_SUBMODULES |
139 GIT_DIFF_INCLUDE_TYPECHANGE;
140 if (git_diff_tree_to_tree(&(ci->diff), repo, ci->parent_tree, ci->commit_tree, &opts))
141 goto err;
142
143 if (git_diff_find_init_options(&fopts, GIT_DIFF_FIND_OPTIONS_VERSION))
144 goto err;
145 /* find renames and copies, exact matches (no heuristic) for renames. */
146 fopts.flags |= GIT_DIFF_FIND_RENAMES | GIT_DIFF_FIND_COPIES |
147 GIT_DIFF_FIND_EXACT_MATCH_ONLY;
148 if (git_diff_find_similar(ci->diff, &fopts))
149 goto err;
150
151 ndeltas = git_diff_num_deltas(ci->diff);
152 if (ndeltas && !(ci->deltas = calloc(ndeltas, sizeof(struct deltainfo *))))
153 err(1, "calloc");
154
155 for (i = 0; i < ndeltas; i++) {
156 if (git_patch_from_diff(&patch, ci->diff, i))
157 goto err;
158
159 if (!(di = calloc(1, sizeof(struct deltainfo))))
160 err(1, "calloc");
161 di->patch = patch;
162 ci->deltas[i] = di;
163
164 delta = git_patch_get_delta(patch);
165
166 /* skip stats for binary data */
167 if (delta->flags & GIT_DIFF_FLAG_BINARY)
168 continue;
169
170 nhunks = git_patch_num_hunks(patch);
171 for (j = 0; j < nhunks; j++) {
172 if (git_patch_get_hunk(&hunk, &nhunklines, patch, j))
173 break;
174 for (k = 0; ; k++) {
175 if (git_patch_get_line_in_hunk(&line, patch, j, k))
176 break;
177 if (line->old_lineno == -1) {
178 di->addcount++;
179 ci->addcount++;
180 } else if (line->new_lineno == -1) {
181 di->delcount++;
182 ci->delcount++;
183 }
184 }
185 }
186 }
187 ci->ndeltas = i;
188 ci->filecount = i;
189
190 return 0;
191
192 err:
193 git_diff_free(ci->diff);
194 ci->diff = NULL;
195 git_tree_free(ci->commit_tree);
196 ci->commit_tree = NULL;
197 git_tree_free(ci->parent_tree);
198 ci->parent_tree = NULL;
199 git_commit_free(ci->parent);
200 ci->parent = NULL;
201
202 if (ci->deltas)
203 for (i = 0; i < ci->ndeltas; i++)
204 deltainfo_free(ci->deltas[i]);
205 free(ci->deltas);
206 ci->deltas = NULL;
207 ci->ndeltas = 0;
208 ci->addcount = 0;
209 ci->delcount = 0;
210 ci->filecount = 0;
211
212 return -1;
213 }
214
215 void
216 commitinfo_free(struct commitinfo *ci)
217 {
218 size_t i;
219
220 if (!ci)
221 return;
222 if (ci->deltas)
223 for (i = 0; i < ci->ndeltas; i++)
224 deltainfo_free(ci->deltas[i]);
225
226 free(ci->deltas);
227 git_diff_free(ci->diff);
228 git_tree_free(ci->commit_tree);
229 git_tree_free(ci->parent_tree);
230 git_commit_free(ci->commit);
231 git_commit_free(ci->parent);
232 memset(ci, 0, sizeof(*ci));
233 free(ci);
234 }
235
236 struct commitinfo *
237 commitinfo_getbyoid(const git_oid *id)
238 {
239 struct commitinfo *ci;
240
241 if (!(ci = calloc(1, sizeof(struct commitinfo))))
242 err(1, "calloc");
243
244 if (git_commit_lookup(&(ci->commit), repo, id))
245 goto err;
246 ci->id = id;
247
248 git_oid_tostr(ci->oid, sizeof(ci->oid), git_commit_id(ci->commit));
249 git_oid_tostr(ci->parentoid, sizeof(ci->parentoid), git_commit_parent_id(ci->commit, 0));
250
251 ci->author = git_commit_author(ci->commit);
252 ci->committer = git_commit_committer(ci->commit);
253 ci->summary = git_commit_summary(ci->commit);
254 ci->msg = git_commit_message(ci->commit);
255
256 return ci;
257
258 err:
259 commitinfo_free(ci);
260
261 return NULL;
262 }
263
264 int
265 refs_cmp(const void *v1, const void *v2)
266 {
267 const struct referenceinfo *r1 = v1, *r2 = v2;
268 time_t t1, t2;
269 int r;
270
271 if ((r = git_reference_is_tag(r1->ref) - git_reference_is_tag(r2->ref)))
272 return r;
273
274 t1 = r1->ci->author ? r1->ci->author->when.time : 0;
275 t2 = r2->ci->author ? r2->ci->author->when.time : 0;
276 if ((r = t1 > t2 ? -1 : (t1 == t2 ? 0 : 1)))
277 return r;
278
279 return strcmp(git_reference_shorthand(r1->ref),
280 git_reference_shorthand(r2->ref));
281 }
282
283 int
284 getrefs(struct referenceinfo **pris, size_t *prefcount)
285 {
286 struct referenceinfo *ris = NULL;
287 struct commitinfo *ci = NULL;
288 git_reference_iterator *it = NULL;
289 const git_oid *id = NULL;
290 git_object *obj = NULL;
291 git_reference *dref = NULL, *r, *ref = NULL;
292 size_t i, refcount;
293
294 *pris = NULL;
295 *prefcount = 0;
296
297 if (git_reference_iterator_new(&it, repo))
298 return -1;
299
300 for (refcount = 0; !git_reference_next(&ref, it); ) {
301 if (!git_reference_is_branch(ref) && !git_reference_is_tag(ref)) {
302 git_reference_free(ref);
303 ref = NULL;
304 continue;
305 }
306
307 switch (git_reference_type(ref)) {
308 case GIT_REF_SYMBOLIC:
309 if (git_reference_resolve(&dref, ref))
310 goto err;
311 r = dref;
312 break;
313 case GIT_REF_OID:
314 r = ref;
315 break;
316 default:
317 continue;
318 }
319 if (!git_reference_target(r) ||
320 git_reference_peel(&obj, r, GIT_OBJ_ANY))
321 goto err;
322 if (!(id = git_object_id(obj)))
323 goto err;
324 if (!(ci = commitinfo_getbyoid(id)))
325 break;
326
327 if (!(ris = reallocarray(ris, refcount + 1, sizeof(*ris))))
328 err(1, "realloc");
329 ris[refcount].ci = ci;
330 ris[refcount].ref = r;
331 refcount++;
332
333 git_object_free(obj);
334 obj = NULL;
335 git_reference_free(dref);
336 dref = NULL;
337 }
338 git_reference_iterator_free(it);
339
340 /* sort by type, date then shorthand name */
341 qsort(ris, refcount, sizeof(*ris), refs_cmp);
342
343 *pris = ris;
344 *prefcount = refcount;
345
346 return 0;
347
348 err:
349 git_object_free(obj);
350 git_reference_free(dref);
351 commitinfo_free(ci);
352 for (i = 0; i < refcount; i++) {
353 commitinfo_free(ris[i].ci);
354 git_reference_free(ris[i].ref);
355 }
356 free(ris);
357
358 return -1;
359 }
360
361 FILE *
362 efopen(const char *filename, const char *flags)
363 {
364 FILE *fp;
365
366 if (!(fp = fopen(filename, flags)))
367 err(1, "fopen: '%s'", filename);
368
369 return fp;
370 }
371
372 /* Percent-encode, see RFC3986 section 2.1. */
373 void
374 percentencode(FILE *fp, const char *s, size_t len)
375 {
376 static char tab[] = "0123456789ABCDEF";
377 unsigned char uc;
378 size_t i;
379
380 for (i = 0; *s && i < len; s++, i++) {
381 uc = *s;
382 /* NOTE: do not encode '/' for paths or ",-." */
383 if (uc < ',' || uc >= 127 || (uc >= ':' && uc <= '@') ||
384 uc == '[' || uc == ']') {
385 putc('%', fp);
386 putc(tab[(uc >> 4) & 0x0f], fp);
387 putc(tab[uc & 0x0f], fp);
388 } else {
389 putc(uc, fp);
390 }
391 }
392 }
393
394 /* Escape characters below as HTML 2.0 / XML 1.0. */
395 void
396 xmlencode(FILE *fp, const char *s, size_t len)
397 {
398 size_t i;
399
400 for (i = 0; *s && i < len; s++, i++) {
401 switch(*s) {
402 case '<': fputs("<", fp); break;
403 case '>': fputs(">", fp); break;
404 case '\'': fputs("'", fp); break;
405 case '&': fputs("&", fp); break;
406 case '"': fputs(""", fp); break;
407 default: putc(*s, fp);
408 }
409 }
410 }
411
412 /* Escape characters below as HTML 2.0 / XML 1.0, ignore printing '\r', '\n' */
413 void
414 xmlencodeline(FILE *fp, const char *s, size_t len)
415 {
416 size_t i;
417
418 for (i = 0; *s && i < len; s++, i++) {
419 switch(*s) {
420 case '<': fputs("<", fp); break;
421 case '>': fputs(">", fp); break;
422 case '\'': fputs("'", fp); break;
423 case '&': fputs("&", fp); break;
424 case '"': fputs(""", fp); break;
425 case '\r': break; /* ignore CR */
426 case '\n': break; /* ignore LF */
427 default: putc(*s, fp);
428 }
429 }
430 }
431
432 int
433 mkdirp(const char *path)
434 {
435 char tmp[PATH_MAX], *p;
436
437 if (strlcpy(tmp, path, sizeof(tmp)) >= sizeof(tmp))
438 errx(1, "path truncated: '%s'", path);
439 for (p = tmp + (tmp[0] == '/'); *p; p++) {
440 if (*p != '/')
441 continue;
442 *p = '\0';
443 if (mkdir(tmp, S_IRWXU | S_IRWXG | S_IRWXO) < 0 && errno != EEXIST)
444 return -1;
445 *p = '/';
446 }
447 if (mkdir(tmp, S_IRWXU | S_IRWXG | S_IRWXO) < 0 && errno != EEXIST)
448 return -1;
449 return 0;
450 }
451
452 void
453 printtimez(FILE *fp, const git_time *intime)
454 {
455 struct tm *intm;
456 time_t t;
457 char out[32];
458
459 t = (time_t)intime->time;
460 if (!(intm = gmtime(&t)))
461 return;
462 strftime(out, sizeof(out), "%Y-%m-%dT%H:%M:%SZ", intm);
463 fputs(out, fp);
464 }
465
466 void
467 printtime(FILE *fp, const git_time *intime)
468 {
469 struct tm *intm;
470 time_t t;
471 char out[32];
472
473 t = (time_t)intime->time + (intime->offset * 60);
474 if (!(intm = gmtime(&t)))
475 return;
476 strftime(out, sizeof(out), "%a, %e %b %Y %H:%M:%S", intm);
477 if (intime->offset < 0)
478 fprintf(fp, "%s -%02d%02d", out,
479 -(intime->offset) / 60, -(intime->offset) % 60);
480 else
481 fprintf(fp, "%s +%02d%02d", out,
482 intime->offset / 60, intime->offset % 60);
483 }
484
485 void
486 printtimeshort(FILE *fp, const git_time *intime)
487 {
488 struct tm *intm;
489 time_t t;
490 char out[32];
491
492 t = (time_t)intime->time;
493 if (!(intm = gmtime(&t)))
494 return;
495 strftime(out, sizeof(out), "%Y-%m-%d %H:%M", intm);
496 fputs(out, fp);
497 }
498
499 void
500 writeheader(FILE *fp, const char *title)
501 {
502 fputs("<!DOCTYPE html>\n"
503 "<html>\n<head>\n"
504 "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n"
505 "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n"
506 "<title>", fp);
507 xmlencode(fp, title, strlen(title));
508 if (title[0] && strippedname[0])
509 fputs(" - ", fp);
510 xmlencode(fp, strippedname, strlen(strippedname));
511 if (description[0])
512 fputs(" - ", fp);
513 xmlencode(fp, description, strlen(description));
514 fputs("</title>\n<link rel=\"icon\" type=\"image/png\" href=\"/favicon.png\" />\n", fp);
515 fputs("<link rel=\"alternate\" type=\"application/atom+xml\" title=\"", fp);
516 xmlencode(fp, name, strlen(name));
517 fprintf(fp, " Atom Feed\" href=\"%satom.xml\" />\n", relpath);
518 fputs("<link rel=\"alternate\" type=\"application/atom+xml\" title=\"", fp);
519 xmlencode(fp, name, strlen(name));
520 fprintf(fp, " Atom Feed (tags)\" href=\"%stags.xml\" />\n", relpath);
521 fputs("<meta name=\"darkreader-lock\">\n", fp);
522 fputs("<link rel=\"stylesheet\" type=\"text/css\" href=\"/style.css\" />\n", fp);
523 fputs("<link rel=\"apple-touch-icon\" sizes=\"180x180\" href=\"/apple-touch-icon.png\" />\n", fp);
524 fputs("<link rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/favicon-32x32.png\" />\n", fp);
525 fputs("<link rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"/favicon-16x16.png\" />\n", fp);
526 fputs("<link rel=\"icon\" type=\"image/x-icon\" href=\"/favicon.ico\" />\n", fp);
527 fputs("<link rel=\"manifest\" href=\"/site.webmanifest\" />\n", fp);
528 fputs("</head>\n<body>\n", fp);
529 fputs("<nav aria-label=\"Main\"><div class=\"nav-inner\"><ul>\n", fp);
530 fputs("<li><a href=\"/\">\n", fp);
531 fputs("<svg aria-hidden=\"true\" focusable=\"false\" viewBox=\"60 140 395 235\" width=\"1.65em\" height=\"1em\">\n", fp);
532 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);
533 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);
534 fputs("</svg>\n", fp);
535 fputs("Blog</a></li>\n", fp);
536 fputs("<li><a href=\"/git\">Git</a></li>\n", fp);
537 fputs("</ul></div></nav>\n", fp);
538 fputs("<main class=\"git\">\n", fp);
539 fputs("<header class=\"main\">\n<h1>", fp);
540 xmlencode(fp, strippedname, strlen(strippedname));
541 fputs("</h1><p>", fp);
542 xmlencode(fp, description, strlen(description));
543 fputs("</p>", fp);
544 if (cloneurl[0]) {
545 fputs("<p>git clone <a href=\"", fp);
546 xmlencode(fp, cloneurl, strlen(cloneurl)); /* not percent-encoded */
547 fputs("\">", fp);
548 xmlencode(fp, cloneurl, strlen(cloneurl));
549 fputs("</a></p>", fp);
550 }
551 fputs("<nav aria-label=\"Repository\"><ul>\n", fp);
552 fprintf(fp, "<li><a href=\"%slog.html\">Log</a></li>", relpath);
553 fprintf(fp, "<li><a href=\"%sfiles.html\">Files</a></li>", relpath);
554 fprintf(fp, "<li><a href=\"%srefs.html\">Refs</a></li>", relpath);
555 if (submodules)
556 fprintf(fp, "<li><a href=\"%sfile/%s.html\">Submodules</a></li>",
557 relpath, submodules);
558 if (readme)
559 fprintf(fp, "<li><a href=\"%sfile/%s.html\">README</a></li>",
560 relpath, readme);
561 if (license)
562 fprintf(fp, "<li><a href=\"%sfile/%s.html\">LICENSE</a></li>",
563 relpath, license);
564 fputs("</ul></nav>\n</header>\n\n", fp);
565 }
566
567 void
568 writefooter(FILE *fp)
569 {
570 fputs("</main>\n</body>\n</html>\n", fp);
571 }
572
573 size_t
574 writeblobhtml(FILE *fp, const git_blob *blob)
575 {
576 size_t n = 0, i, len, prev;
577 const char *nfmt = "<a href=\"#l%zu\" class=\"line\" id=\"l%zu\">%7zu</a> ";
578 const char *s = git_blob_rawcontent(blob);
579
580 len = git_blob_rawsize(blob);
581 fputs("<pre id=\"blob\">\n", fp);
582
583 if (len > 0) {
584 for (i = 0, prev = 0; i < len; i++) {
585 if (s[i] != '\n')
586 continue;
587 n++;
588 fprintf(fp, nfmt, n, n, n);
589 xmlencodeline(fp, &s[prev], i - prev + 1);
590 putc('\n', fp);
591 prev = i + 1;
592 }
593 /* trailing data */
594 if ((len - prev) > 0) {
595 n++;
596 fprintf(fp, nfmt, n, n, n);
597 xmlencodeline(fp, &s[prev], len - prev);
598 }
599 }
600
601 fputs("</pre>\n", fp);
602
603 return n;
604 }
605
606 void
607 printcommit(FILE *fp, struct commitinfo *ci)
608 {
609 fprintf(fp, "<b>commit</b> <a href=\"%scommit/%s.html\">%s</a>\n",
610 relpath, ci->oid, ci->oid);
611
612 if (ci->parentoid[0])
613 fprintf(fp, "<b>parent</b> <a href=\"%scommit/%s.html\">%s</a>\n",
614 relpath, ci->parentoid, ci->parentoid);
615
616 if (ci->author) {
617 fputs("<b>Author:</b> ", fp);
618 xmlencode(fp, ci->author->name, strlen(ci->author->name));
619 fputs(" <<a href=\"mailto:", fp);
620 xmlencode(fp, ci->author->email, strlen(ci->author->email)); /* not percent-encoded */
621 fputs("\">", fp);
622 xmlencode(fp, ci->author->email, strlen(ci->author->email));
623 fputs("</a>>\n<b>Date:</b> ", fp);
624 printtime(fp, &(ci->author->when));
625 putc('\n', fp);
626 }
627 if (ci->msg) {
628 putc('\n', fp);
629 xmlencode(fp, ci->msg, strlen(ci->msg));
630 putc('\n', fp);
631 }
632 }
633
634 void
635 printshowfile(FILE *fp, struct commitinfo *ci)
636 {
637 const git_diff_delta *delta;
638 const git_diff_hunk *hunk;
639 const git_diff_line *line;
640 git_patch *patch;
641 size_t nhunks, nhunklines, changed, add, del, total, i, j, k;
642 char linestr[80];
643 int c;
644
645 printcommit(fp, ci);
646
647 if (!ci->deltas)
648 return;
649
650 if (ci->filecount > 1000 ||
651 ci->ndeltas > 1000 ||
652 ci->addcount > 100000 ||
653 ci->delcount > 100000) {
654 fputs("Diff is too large, output suppressed.\n", fp);
655 return;
656 }
657
658 /* diff stat */
659 fputs("<b>Diffstat:</b>\n<table>", fp);
660 for (i = 0; i < ci->ndeltas; i++) {
661 delta = git_patch_get_delta(ci->deltas[i]->patch);
662
663 switch (delta->status) {
664 case GIT_DELTA_ADDED: c = 'A'; break;
665 case GIT_DELTA_COPIED: c = 'C'; break;
666 case GIT_DELTA_DELETED: c = 'D'; break;
667 case GIT_DELTA_MODIFIED: c = 'M'; break;
668 case GIT_DELTA_RENAMED: c = 'R'; break;
669 case GIT_DELTA_TYPECHANGE: c = 'T'; break;
670 default: c = ' '; break;
671 }
672 if (c == ' ')
673 fprintf(fp, "<tr><td>%c", c);
674 else
675 fprintf(fp, "<tr><td class=\"%c\">%c", c, c);
676
677 fprintf(fp, "</td><td><a href=\"#h%zu\">", i);
678 xmlencode(fp, delta->old_file.path, strlen(delta->old_file.path));
679 if (strcmp(delta->old_file.path, delta->new_file.path)) {
680 fputs(" -> ", fp);
681 xmlencode(fp, delta->new_file.path, strlen(delta->new_file.path));
682 }
683
684 add = ci->deltas[i]->addcount;
685 del = ci->deltas[i]->delcount;
686 changed = add + del;
687 total = sizeof(linestr) - 2;
688 if (changed > total) {
689 if (add)
690 add = ((float)total / changed * add) + 1;
691 if (del)
692 del = ((float)total / changed * del) + 1;
693 }
694 memset(&linestr, '+', add);
695 memset(&linestr[add], '-', del);
696
697 fprintf(fp, "</a></td><td> | </td><td class=\"num\">%zu</td><td><span class=\"i\">",
698 ci->deltas[i]->addcount + ci->deltas[i]->delcount);
699 fwrite(&linestr, 1, add, fp);
700 fputs("</span><span class=\"d\">", fp);
701 fwrite(&linestr[add], 1, del, fp);
702 fputs("</span></td></tr>\n", fp);
703 }
704 fprintf(fp, "</table></pre><pre>%zu file%s changed, %zu insertion%s(+), %zu deletion%s(-)\n",
705 ci->filecount, ci->filecount == 1 ? "" : "s",
706 ci->addcount, ci->addcount == 1 ? "" : "s",
707 ci->delcount, ci->delcount == 1 ? "" : "s");
708
709 fputs("<hr/>", fp);
710
711 for (i = 0; i < ci->ndeltas; i++) {
712 patch = ci->deltas[i]->patch;
713 delta = git_patch_get_delta(patch);
714 fprintf(fp, "<b>diff --git a/<a id=\"h%zu\" href=\"%sfile/", i, relpath);
715 percentencode(fp, delta->old_file.path, strlen(delta->old_file.path));
716 fputs(".html\">", fp);
717 xmlencode(fp, delta->old_file.path, strlen(delta->old_file.path));
718 fprintf(fp, "</a> b/<a href=\"%sfile/", relpath);
719 percentencode(fp, delta->new_file.path, strlen(delta->new_file.path));
720 fprintf(fp, ".html\">");
721 xmlencode(fp, delta->new_file.path, strlen(delta->new_file.path));
722 fprintf(fp, "</a></b>\n");
723
724 /* check binary data */
725 if (delta->flags & GIT_DIFF_FLAG_BINARY) {
726 fputs("Binary files differ.\n", fp);
727 continue;
728 }
729
730 nhunks = git_patch_num_hunks(patch);
731 for (j = 0; j < nhunks; j++) {
732 if (git_patch_get_hunk(&hunk, &nhunklines, patch, j))
733 break;
734
735 fprintf(fp, "<a href=\"#h%zu-%zu\" id=\"h%zu-%zu\" class=\"h\">", i, j, i, j);
736 xmlencode(fp, hunk->header, hunk->header_len);
737 fputs("</a>", fp);
738
739 for (k = 0; ; k++) {
740 if (git_patch_get_line_in_hunk(&line, patch, j, k))
741 break;
742 if (line->old_lineno == -1)
743 fprintf(fp, "<a href=\"#h%zu-%zu-%zu\" id=\"h%zu-%zu-%zu\" class=\"i\">+",
744 i, j, k, i, j, k);
745 else if (line->new_lineno == -1)
746 fprintf(fp, "<a href=\"#h%zu-%zu-%zu\" id=\"h%zu-%zu-%zu\" class=\"d\">-",
747 i, j, k, i, j, k);
748 else
749 putc(' ', fp);
750 xmlencodeline(fp, line->content, line->content_len);
751 putc('\n', fp);
752 if (line->old_lineno == -1 || line->new_lineno == -1)
753 fputs("</a>", fp);
754 }
755 }
756 }
757 }
758
759 void
760 writelogline(FILE *fp, struct commitinfo *ci)
761 {
762 fputs("<tr><td>", fp);
763 if (ci->author)
764 printtimeshort(fp, &(ci->author->when));
765 fputs("</td><td>", fp);
766 if (ci->summary) {
767 fprintf(fp, "<a href=\"%scommit/%s.html\">", relpath, ci->oid);
768 xmlencode(fp, ci->summary, strlen(ci->summary));
769 fputs("</a>", fp);
770 }
771 fputs("</td><td>", fp);
772 if (ci->author)
773 xmlencode(fp, ci->author->name, strlen(ci->author->name));
774 fputs("</td><td class=\"num\" align=\"right\">", fp);
775 fprintf(fp, "%zu", ci->filecount);
776 fputs("</td><td class=\"num\" align=\"right\">", fp);
777 fprintf(fp, "+%zu", ci->addcount);
778 fputs("</td><td class=\"num\" align=\"right\">", fp);
779 fprintf(fp, "-%zu", ci->delcount);
780 fputs("</td></tr>\n", fp);
781 }
782
783 int
784 writelog(FILE *fp, const git_oid *oid)
785 {
786 struct commitinfo *ci;
787 git_revwalk *w = NULL;
788 git_oid id;
789 char path[PATH_MAX], oidstr[GIT_OID_HEXSZ + 1];
790 FILE *fpfile;
791 size_t remcommits = 0;
792 int r;
793
794 git_revwalk_new(&w, repo);
795 git_revwalk_push(w, oid);
796
797 while (!git_revwalk_next(&id, w)) {
798 relpath = "";
799
800 if (cachefile && !memcmp(&id, &lastoid, sizeof(id)))
801 break;
802
803 git_oid_tostr(oidstr, sizeof(oidstr), &id);
804 r = snprintf(path, sizeof(path), "commit/%s.html", oidstr);
805 if (r < 0 || (size_t)r >= sizeof(path))
806 errx(1, "path truncated: 'commit/%s.html'", oidstr);
807 r = access(path, F_OK);
808
809 /* optimization: if there are no log lines to write and
810 the commit file already exists: skip the diffstat */
811 if (!nlogcommits) {
812 remcommits++;
813 if (!r)
814 continue;
815 }
816
817 if (!(ci = commitinfo_getbyoid(&id)))
818 break;
819 /* diffstat: for stagit HTML required for the log.html line */
820 if (commitinfo_getstats(ci) == -1)
821 goto err;
822
823 if (nlogcommits != 0) {
824 writelogline(fp, ci);
825 if (nlogcommits > 0)
826 nlogcommits--;
827 }
828
829 if (cachefile)
830 writelogline(wcachefp, ci);
831
832 /* check if file exists if so skip it */
833 if (r) {
834 relpath = "../";
835 fpfile = efopen(path, "w");
836 writeheader(fpfile, ci->summary);
837 fputs("<pre>", fpfile);
838 printshowfile(fpfile, ci);
839 fputs("</pre>\n", fpfile);
840 writefooter(fpfile);
841 checkfileerror(fpfile, path, 'w');
842 fclose(fpfile);
843 }
844 err:
845 commitinfo_free(ci);
846 }
847 git_revwalk_free(w);
848
849 if (nlogcommits == 0 && remcommits != 0) {
850 fprintf(fp, "<tr><td></td><td colspan=\"5\">"
851 "%zu more commits remaining, fetch the repository"
852 "</td></tr>\n", remcommits);
853 }
854
855 relpath = "";
856
857 return 0;
858 }
859
860 void
861 printcommitatom(FILE *fp, struct commitinfo *ci, const char *tag)
862 {
863 fputs("<entry>\n", fp);
864
865 fprintf(fp, "<id>%s</id>\n", ci->oid);
866 if (ci->author) {
867 fputs("<published>", fp);
868 printtimez(fp, &(ci->author->when));
869 fputs("</published>\n", fp);
870 }
871 if (ci->committer) {
872 fputs("<updated>", fp);
873 printtimez(fp, &(ci->committer->when));
874 fputs("</updated>\n", fp);
875 }
876 if (ci->summary) {
877 fputs("<title>", fp);
878 if (tag && tag[0]) {
879 fputs("[", fp);
880 xmlencode(fp, tag, strlen(tag));
881 fputs("] ", fp);
882 }
883 xmlencode(fp, ci->summary, strlen(ci->summary));
884 fputs("</title>\n", fp);
885 }
886 fprintf(fp, "<link rel=\"alternate\" type=\"text/html\" href=\"%scommit/%s.html\" />\n",
887 baseurl, ci->oid);
888
889 if (ci->author) {
890 fputs("<author>\n<name>", fp);
891 xmlencode(fp, ci->author->name, strlen(ci->author->name));
892 fputs("</name>\n<email>", fp);
893 xmlencode(fp, ci->author->email, strlen(ci->author->email));
894 fputs("</email>\n</author>\n", fp);
895 }
896
897 fputs("<content>", fp);
898 fprintf(fp, "commit %s\n", ci->oid);
899 if (ci->parentoid[0])
900 fprintf(fp, "parent %s\n", ci->parentoid);
901 if (ci->author) {
902 fputs("Author: ", fp);
903 xmlencode(fp, ci->author->name, strlen(ci->author->name));
904 fputs(" <", fp);
905 xmlencode(fp, ci->author->email, strlen(ci->author->email));
906 fputs(">\nDate: ", fp);
907 printtime(fp, &(ci->author->when));
908 putc('\n', fp);
909 }
910 if (ci->msg) {
911 putc('\n', fp);
912 xmlencode(fp, ci->msg, strlen(ci->msg));
913 }
914 fputs("\n</content>\n</entry>\n", fp);
915 }
916
917 int
918 writeatom(FILE *fp, int all)
919 {
920 struct referenceinfo *ris = NULL;
921 size_t refcount = 0;
922 struct commitinfo *ci;
923 git_revwalk *w = NULL;
924 git_oid id;
925 size_t i, m = 100; /* last 'm' commits */
926
927 fputs("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
928 "<feed xmlns=\"http://www.w3.org/2005/Atom\">\n<title>", fp);
929 xmlencode(fp, strippedname, strlen(strippedname));
930 fputs(", branch HEAD</title>\n<subtitle>", fp);
931 xmlencode(fp, description, strlen(description));
932 fputs("</subtitle>\n", fp);
933
934 /* all commits or only tags? */
935 if (all) {
936 git_revwalk_new(&w, repo);
937 git_revwalk_push_head(w);
938 for (i = 0; i < m && !git_revwalk_next(&id, w); i++) {
939 if (!(ci = commitinfo_getbyoid(&id)))
940 break;
941 printcommitatom(fp, ci, "");
942 commitinfo_free(ci);
943 }
944 git_revwalk_free(w);
945 } else if (getrefs(&ris, &refcount) != -1) {
946 /* references: tags */
947 for (i = 0; i < refcount; i++) {
948 if (git_reference_is_tag(ris[i].ref))
949 printcommitatom(fp, ris[i].ci,
950 git_reference_shorthand(ris[i].ref));
951
952 commitinfo_free(ris[i].ci);
953 git_reference_free(ris[i].ref);
954 }
955 free(ris);
956 }
957
958 fputs("</feed>\n", fp);
959
960 return 0;
961 }
962
963 size_t
964 writeblob(git_object *obj, const char *fpath, const char *filename, size_t filesize)
965 {
966 char tmp[PATH_MAX] = "", *d;
967 const char *p;
968 size_t lc = 0;
969 FILE *fp;
970
971 if (strlcpy(tmp, fpath, sizeof(tmp)) >= sizeof(tmp))
972 errx(1, "path truncated: '%s'", fpath);
973 if (!(d = dirname(tmp)))
974 err(1, "dirname");
975 if (mkdirp(d))
976 return -1;
977
978 for (p = fpath, tmp[0] = '\0'; *p; p++) {
979 if (*p == '/' && strlcat(tmp, "../", sizeof(tmp)) >= sizeof(tmp))
980 errx(1, "path truncated: '../%s'", tmp);
981 }
982 relpath = tmp;
983
984 fp = efopen(fpath, "w");
985 writeheader(fp, filename);
986 fputs("<section class=\"git-file\">\n", fp);
987 fputs("<h2>", fp);
988 xmlencode(fp, filename, strlen(filename));
989 fputs("</h2>\n", fp);
990 fputs("<p>", fp);
991 fprintf(fp, " (%zuB)", filesize);
992 fputs("</p><hr/>\n", fp);
993
994 if (git_blob_is_binary((git_blob *)obj))
995 fputs("<p>Binary file.</p>\n", fp);
996 else
997 lc = writeblobhtml(fp, (git_blob *)obj);
998
999 fputs("</section>\n", fp);
1000 writefooter(fp);
1001 checkfileerror(fp, fpath, 'w');
1002 fclose(fp);
1003
1004 relpath = "";
1005
1006 return lc;
1007 }
1008
1009 const char *
1010 filemode(git_filemode_t m)
1011 {
1012 static char mode[11];
1013
1014 memset(mode, '-', sizeof(mode) - 1);
1015 mode[10] = '\0';
1016
1017 if (S_ISREG(m))
1018 mode[0] = '-';
1019 else if (S_ISBLK(m))
1020 mode[0] = 'b';
1021 else if (S_ISCHR(m))
1022 mode[0] = 'c';
1023 else if (S_ISDIR(m))
1024 mode[0] = 'd';
1025 else if (S_ISFIFO(m))
1026 mode[0] = 'p';
1027 else if (S_ISLNK(m))
1028 mode[0] = 'l';
1029 else if (S_ISSOCK(m))
1030 mode[0] = 's';
1031 else
1032 mode[0] = '?';
1033
1034 if (m & S_IRUSR) mode[1] = 'r';
1035 if (m & S_IWUSR) mode[2] = 'w';
1036 if (m & S_IXUSR) mode[3] = 'x';
1037 if (m & S_IRGRP) mode[4] = 'r';
1038 if (m & S_IWGRP) mode[5] = 'w';
1039 if (m & S_IXGRP) mode[6] = 'x';
1040 if (m & S_IROTH) mode[7] = 'r';
1041 if (m & S_IWOTH) mode[8] = 'w';
1042 if (m & S_IXOTH) mode[9] = 'x';
1043
1044 if (m & S_ISUID) mode[3] = (mode[3] == 'x') ? 's' : 'S';
1045 if (m & S_ISGID) mode[6] = (mode[6] == 'x') ? 's' : 'S';
1046 if (m & S_ISVTX) mode[9] = (mode[9] == 'x') ? 't' : 'T';
1047
1048 return mode;
1049 }
1050
1051 int
1052 writefilestree(FILE *fp, git_tree *tree, const char *path)
1053 {
1054 const git_tree_entry *entry = NULL;
1055 git_object *obj = NULL;
1056 const char *entryname;
1057 char filepath[PATH_MAX], entrypath[PATH_MAX], oid[8];
1058 size_t count, i, lc, filesize;
1059 int r, ret;
1060
1061 count = git_tree_entrycount(tree);
1062 for (i = 0; i < count; i++) {
1063 if (!(entry = git_tree_entry_byindex(tree, i)) ||
1064 !(entryname = git_tree_entry_name(entry)))
1065 return -1;
1066 joinpath(entrypath, sizeof(entrypath), path, entryname);
1067
1068 r = snprintf(filepath, sizeof(filepath), "file/%s.html",
1069 entrypath);
1070 if (r < 0 || (size_t)r >= sizeof(filepath))
1071 errx(1, "path truncated: 'file/%s.html'", entrypath);
1072
1073 if (!git_tree_entry_to_object(&obj, repo, entry)) {
1074 switch (git_object_type(obj)) {
1075 case GIT_OBJ_BLOB:
1076 break;
1077 case GIT_OBJ_TREE:
1078 /* NOTE: recurses */
1079 ret = writefilestree(fp, (git_tree *)obj,
1080 entrypath);
1081 git_object_free(obj);
1082 if (ret)
1083 return ret;
1084 continue;
1085 default:
1086 git_object_free(obj);
1087 continue;
1088 }
1089
1090 filesize = git_blob_rawsize((git_blob *)obj);
1091 lc = writeblob(obj, filepath, entryname, filesize);
1092
1093 fputs("<tr><td>", fp);
1094 fputs(filemode(git_tree_entry_filemode(entry)), fp);
1095 fprintf(fp, "</td><td><a href=\"%s", relpath);
1096 percentencode(fp, filepath, strlen(filepath));
1097 fputs("\">", fp);
1098 xmlencode(fp, entrypath, strlen(entrypath));
1099 fputs("</a></td><td class=\"num\" align=\"right\">", fp);
1100 if (lc > 0)
1101 fprintf(fp, "%zuL", lc);
1102 else
1103 fprintf(fp, "%zuB", filesize);
1104 fputs("</td></tr>\n", fp);
1105 git_object_free(obj);
1106 } else if (git_tree_entry_type(entry) == GIT_OBJ_COMMIT) {
1107 /* commit object in tree is a submodule */
1108 fprintf(fp, "<tr><td>m---------</td><td><a href=\"%sfile/.gitmodules.html\">",
1109 relpath);
1110 xmlencode(fp, entrypath, strlen(entrypath));
1111 fputs("</a> @ ", fp);
1112 git_oid_tostr(oid, sizeof(oid), git_tree_entry_id(entry));
1113 xmlencode(fp, oid, strlen(oid));
1114 fputs("</td><td class=\"num\" align=\"right\"></td></tr>\n", fp);
1115 }
1116 }
1117
1118 return 0;
1119 }
1120
1121 int
1122 writefiles(FILE *fp, const git_oid *id)
1123 {
1124 git_tree *tree = NULL;
1125 git_commit *commit = NULL;
1126 int ret = -1;
1127
1128 fputs("<table id=\"files\"><thead>\n<tr>"
1129 "<td><b>Mode</b></td><td><b>Name</b></td>"
1130 "<td class=\"num\" align=\"right\"><b>Size</b></td>"
1131 "</tr>\n</thead><tbody>\n", fp);
1132
1133 if (!git_commit_lookup(&commit, repo, id) &&
1134 !git_commit_tree(&tree, commit))
1135 ret = writefilestree(fp, tree, "");
1136
1137 fputs("</tbody></table>", fp);
1138
1139 git_commit_free(commit);
1140 git_tree_free(tree);
1141
1142 return ret;
1143 }
1144
1145 int
1146 writerefs(FILE *fp)
1147 {
1148 struct referenceinfo *ris = NULL;
1149 struct commitinfo *ci;
1150 size_t count, i, j, refcount;
1151 const char *titles[] = { "Branches", "Tags" };
1152 const char *ids[] = { "branches", "tags" };
1153 const char *s;
1154
1155 if (getrefs(&ris, &refcount) == -1)
1156 return -1;
1157
1158 for (i = 0, j = 0, count = 0; i < refcount; i++) {
1159 if (j == 0 && git_reference_is_tag(ris[i].ref)) {
1160 if (count)
1161 fputs("</tbody></table><br/>\n", fp);
1162 count = 0;
1163 j = 1;
1164 }
1165
1166 /* print header if it has an entry (first). */
1167 if (++count == 1) {
1168 fprintf(fp, "<h2>%s</h2><table id=\"%s\">"
1169 "<thead>\n<tr><td><b>Name</b></td>"
1170 "<td><b>Last commit date</b></td>"
1171 "<td><b>Author</b></td>\n</tr>\n"
1172 "</thead><tbody>\n",
1173 titles[j], ids[j]);
1174 }
1175
1176 ci = ris[i].ci;
1177 s = git_reference_shorthand(ris[i].ref);
1178
1179 fputs("<tr><td>", fp);
1180 xmlencode(fp, s, strlen(s));
1181 fputs("</td><td>", fp);
1182 if (ci->author)
1183 printtimeshort(fp, &(ci->author->when));
1184 fputs("</td><td>", fp);
1185 if (ci->author)
1186 xmlencode(fp, ci->author->name, strlen(ci->author->name));
1187 fputs("</td></tr>\n", fp);
1188 }
1189 /* table footer */
1190 if (count)
1191 fputs("</tbody></table><br/>\n", fp);
1192
1193 for (i = 0; i < refcount; i++) {
1194 commitinfo_free(ris[i].ci);
1195 git_reference_free(ris[i].ref);
1196 }
1197 free(ris);
1198
1199 return 0;
1200 }
1201
1202 void
1203 usage(char *argv0)
1204 {
1205 fprintf(stderr, "usage: %s [-c cachefile | -l commits] "
1206 "[-u baseurl] repodir\n", argv0);
1207 exit(1);
1208 }
1209
1210 int
1211 main(int argc, char *argv[])
1212 {
1213 git_object *obj = NULL;
1214 const git_oid *head = NULL;
1215 mode_t mask;
1216 FILE *fp, *fpread;
1217 char path[PATH_MAX], repodirabs[PATH_MAX + 1], *p;
1218 char tmppath[64] = "cache.XXXXXXXXXXXX", buf[BUFSIZ];
1219 size_t n;
1220 int i, fd;
1221
1222 for (i = 1; i < argc; i++) {
1223 if (argv[i][0] != '-') {
1224 if (repodir)
1225 usage(argv[0]);
1226 repodir = argv[i];
1227 } else if (argv[i][1] == 'c') {
1228 if (nlogcommits > 0 || i + 1 >= argc)
1229 usage(argv[0]);
1230 cachefile = argv[++i];
1231 } else if (argv[i][1] == 'l') {
1232 if (cachefile || i + 1 >= argc)
1233 usage(argv[0]);
1234 errno = 0;
1235 nlogcommits = strtoll(argv[++i], &p, 10);
1236 if (argv[i][0] == '\0' || *p != '\0' ||
1237 nlogcommits <= 0 || errno)
1238 usage(argv[0]);
1239 } else if (argv[i][1] == 'u') {
1240 if (i + 1 >= argc)
1241 usage(argv[0]);
1242 baseurl = argv[++i];
1243 }
1244 }
1245 if (!repodir)
1246 usage(argv[0]);
1247
1248 if (!realpath(repodir, repodirabs))
1249 err(1, "realpath");
1250
1251 /* do not search outside the git repository:
1252 GIT_CONFIG_LEVEL_APP is the highest level currently */
1253 git_libgit2_init();
1254 for (i = 1; i <= GIT_CONFIG_LEVEL_APP; i++)
1255 git_libgit2_opts(GIT_OPT_SET_SEARCH_PATH, i, "");
1256 /* do not require the git repository to be owned by the current user */
1257 git_libgit2_opts(GIT_OPT_SET_OWNER_VALIDATION, 0);
1258
1259 #ifdef __OpenBSD__
1260 if (unveil(repodir, "r") == -1)
1261 err(1, "unveil: %s", repodir);
1262 if (unveil(".", "rwc") == -1)
1263 err(1, "unveil: .");
1264 if (cachefile && unveil(cachefile, "rwc") == -1)
1265 err(1, "unveil: %s", cachefile);
1266
1267 if (cachefile) {
1268 if (pledge("stdio rpath wpath cpath fattr", NULL) == -1)
1269 err(1, "pledge");
1270 } else {
1271 if (pledge("stdio rpath wpath cpath", NULL) == -1)
1272 err(1, "pledge");
1273 }
1274 #endif
1275
1276 if (git_repository_open_ext(&repo, repodir,
1277 GIT_REPOSITORY_OPEN_NO_SEARCH, NULL) < 0) {
1278 fprintf(stderr, "%s: cannot open repository\n", argv[0]);
1279 return 1;
1280 }
1281
1282 /* find HEAD */
1283 if (!git_revparse_single(&obj, repo, "HEAD"))
1284 head = git_object_id(obj);
1285 git_object_free(obj);
1286
1287 /* use directory name as name */
1288 if ((name = strrchr(repodirabs, '/')))
1289 name++;
1290 else
1291 name = "";
1292
1293 /* strip .git suffix */
1294 if (!(strippedname = strdup(name)))
1295 err(1, "strdup");
1296 if ((p = strrchr(strippedname, '.')))
1297 if (!strcmp(p, ".git"))
1298 *p = '\0';
1299
1300 /* read description or .git/description */
1301 joinpath(path, sizeof(path), repodir, "description");
1302 if (!(fpread = fopen(path, "r"))) {
1303 joinpath(path, sizeof(path), repodir, ".git/description");
1304 fpread = fopen(path, "r");
1305 }
1306 if (fpread) {
1307 if (!fgets(description, sizeof(description), fpread))
1308 description[0] = '\0';
1309 checkfileerror(fpread, path, 'r');
1310 fclose(fpread);
1311 }
1312
1313 /* read url or .git/url */
1314 joinpath(path, sizeof(path), repodir, "url");
1315 if (!(fpread = fopen(path, "r"))) {
1316 joinpath(path, sizeof(path), repodir, ".git/url");
1317 fpread = fopen(path, "r");
1318 }
1319 if (fpread) {
1320 if (!fgets(cloneurl, sizeof(cloneurl), fpread))
1321 cloneurl[0] = '\0';
1322 checkfileerror(fpread, path, 'r');
1323 fclose(fpread);
1324 cloneurl[strcspn(cloneurl, "\n")] = '\0';
1325 }
1326
1327 /* check LICENSE */
1328 for (i = 0; i < LEN(licensefiles) && !license; i++) {
1329 if (!git_revparse_single(&obj, repo, licensefiles[i]) &&
1330 git_object_type(obj) == GIT_OBJ_BLOB)
1331 license = licensefiles[i] + strlen("HEAD:");
1332 git_object_free(obj);
1333 }
1334
1335 /* check README */
1336 for (i = 0; i < LEN(readmefiles) && !readme; i++) {
1337 if (!git_revparse_single(&obj, repo, readmefiles[i]) &&
1338 git_object_type(obj) == GIT_OBJ_BLOB)
1339 readme = readmefiles[i] + strlen("HEAD:");
1340 git_object_free(obj);
1341 }
1342
1343 if (!git_revparse_single(&obj, repo, "HEAD:.gitmodules") &&
1344 git_object_type(obj) == GIT_OBJ_BLOB)
1345 submodules = ".gitmodules";
1346 git_object_free(obj);
1347
1348 /* log for HEAD */
1349 fp = efopen("log.html", "w");
1350 relpath = "";
1351 mkdir("commit", S_IRWXU | S_IRWXG | S_IRWXO);
1352 writeheader(fp, "Log");
1353 fputs("<table id=\"log\"><thead>\n<tr><td><b>Date</b></td>"
1354 "<td><b>Commit message</b></td>"
1355 "<td><b>Author</b></td><td class=\"num\" align=\"right\"><b>Files</b></td>"
1356 "<td class=\"num\" align=\"right\"><b>+</b></td>"
1357 "<td class=\"num\" align=\"right\"><b>-</b></td></tr>\n</thead><tbody>\n", fp);
1358
1359 if (cachefile && head) {
1360 /* read from cache file (does not need to exist) */
1361 if ((rcachefp = fopen(cachefile, "r"))) {
1362 if (!fgets(lastoidstr, sizeof(lastoidstr), rcachefp))
1363 errx(1, "%s: no object id", cachefile);
1364 if (git_oid_fromstr(&lastoid, lastoidstr))
1365 errx(1, "%s: invalid object id", cachefile);
1366 }
1367
1368 /* write log to (temporary) cache */
1369 if ((fd = mkstemp(tmppath)) == -1)
1370 err(1, "mkstemp");
1371 if (!(wcachefp = fdopen(fd, "w")))
1372 err(1, "fdopen: '%s'", tmppath);
1373 /* write last commit id (HEAD) */
1374 git_oid_tostr(buf, sizeof(buf), head);
1375 fprintf(wcachefp, "%s\n", buf);
1376
1377 writelog(fp, head);
1378
1379 if (rcachefp) {
1380 /* append previous log to log.html and the new cache */
1381 while (!feof(rcachefp)) {
1382 n = fread(buf, 1, sizeof(buf), rcachefp);
1383 if (ferror(rcachefp))
1384 break;
1385 if (fwrite(buf, 1, n, fp) != n ||
1386 fwrite(buf, 1, n, wcachefp) != n)
1387 break;
1388 }
1389 checkfileerror(rcachefp, cachefile, 'r');
1390 fclose(rcachefp);
1391 }
1392 checkfileerror(wcachefp, tmppath, 'w');
1393 fclose(wcachefp);
1394 } else {
1395 if (head)
1396 writelog(fp, head);
1397 }
1398
1399 fputs("</tbody></table>", fp);
1400 writefooter(fp);
1401 checkfileerror(fp, "log.html", 'w');
1402 fclose(fp);
1403
1404 /* files for HEAD */
1405 fp = efopen("files.html", "w");
1406 writeheader(fp, "Files");
1407 if (head)
1408 writefiles(fp, head);
1409 writefooter(fp);
1410 checkfileerror(fp, "files.html", 'w');
1411 fclose(fp);
1412
1413 /* summary page with branches and tags */
1414 fp = efopen("refs.html", "w");
1415 writeheader(fp, "Refs");
1416 writerefs(fp);
1417 writefooter(fp);
1418 checkfileerror(fp, "refs.html", 'w');
1419 fclose(fp);
1420
1421 /* Atom feed */
1422 fp = efopen("atom.xml", "w");
1423 writeatom(fp, 1);
1424 checkfileerror(fp, "atom.xml", 'w');
1425 fclose(fp);
1426
1427 /* Atom feed for tags / releases */
1428 fp = efopen("tags.xml", "w");
1429 writeatom(fp, 0);
1430 checkfileerror(fp, "tags.xml", 'w');
1431 fclose(fp);
1432
1433 /* rename new cache file on success */
1434 if (cachefile && head) {
1435 if (rename(tmppath, cachefile))
1436 err(1, "rename: '%s' to '%s'", tmppath, cachefile);
1437 umask((mask = umask(0)));
1438 if (chmod(cachefile,
1439 (S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH) & ~mask))
1440 err(1, "chmod: '%s'", cachefile);
1441 }
1442
1443 /* cleanup */
1444 git_repository_free(repo);
1445 git_libgit2_shutdown();
1446
1447 return 0;
1448 }