stagit.c (39334B)
1#include <sys/stat.h>
2#include <sys/types.h>
34
#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>
1415
#include <git2.h>
1617
#include "compat.h"
18#include "get_lang.h"
1920
#define LEN(s) (sizeof(s)/sizeof(*s))
2122
struct deltainfo {
23git_patch *patch;
2425
size_t addcount;
26size_t delcount;
27};
2829
struct commitinfo {
30const git_oid *id;
3132
char oid[GIT_OID_HEXSZ + 1];
33char parentoid[GIT_OID_HEXSZ + 1];
3435
const git_signature *author;
36const git_signature *committer;
37const char *summary;
38const char *msg;
3940
git_diff *diff;
41git_commit *commit;
42git_commit *parent;
43git_tree *commit_tree;
44git_tree *parent_tree;
4546
size_t addcount;
47size_t delcount;
48size_t filecount;
4950
struct deltainfo **deltas;
51size_t ndeltas;
52};
5354
/* reference and associated data for sorting */
55struct referenceinfo {
56struct git_reference *ref;
57struct commitinfo *ci;
58};
5960
static git_repository *repo;
6162
static const char *baseurl = ""; /* base URL to make absolute RSS/Atom URI */
63static const char *relpath = "";
64static const char *repodir;
6566
static char *name = "";
67static char *strippedname = "";
68static char description[255];
69static char cloneurl[1024];
70static char *submodules;
71static char *licensefiles[] = { "HEAD:LICENSE", "HEAD:LICENSE.md", "HEAD:COPYING" };
72static char *license;
73static char *readmefiles[] = { "HEAD:README", "HEAD:README.md" };
74static char *readme;
75static long long nlogcommits = -1; /* -1 indicates not used */
7677
/* cache */
78static git_oid lastoid;
79static char lastoidstr[GIT_OID_HEXSZ + 2]; /* id + newline + NUL byte */
80static FILE *rcachefp, *wcachefp;
81static const char *cachefile;
8283
/* Handle read or write errors for a FILE * stream */
84void
85checkfileerror(FILE *fp, const char *name, int mode)
86{
87if (mode == 'r' && ferror(fp))
88errx(1, "read error: %s", name);
89else if (mode == 'w' && (fflush(fp) || ferror(fp)))
90errx(1, "write error: %s", name);
91}
9293
void
94joinpath(char *buf, size_t bufsiz, const char *path, const char *path2)
95{
96int r;
9798
r = snprintf(buf, bufsiz, "%s%s%s",
99path, path[0] && path[strlen(path) - 1] != '/' ? "/" : "", path2);
100if (r < 0 || (size_t)r >= bufsiz)
101errx(1, "path truncated: '%s%s%s'",
102path, path[0] && path[strlen(path) - 1] != '/' ? "/" : "", path2);
103}
104105
void
106deltainfo_free(struct deltainfo *di)
107{
108if (!di)
109return;
110git_patch_free(di->patch);
111memset(di, 0, sizeof(*di));
112free(di);
113}
114115
int
116commitinfo_getstats(struct commitinfo *ci)
117{
118struct deltainfo *di;
119git_diff_options opts;
120git_diff_find_options fopts;
121const git_diff_delta *delta;
122const git_diff_hunk *hunk;
123const git_diff_line *line;
124git_patch *patch = NULL;
125size_t ndeltas, nhunks, nhunklines;
126size_t i, j, k;
127128
if (git_tree_lookup(&(ci->commit_tree), repo, git_commit_tree_id(ci->commit)))
129goto err;
130if (!git_commit_parent(&(ci->parent), ci->commit, 0)) {
131if (git_tree_lookup(&(ci->parent_tree), repo, git_commit_tree_id(ci->parent))) {
132ci->parent = NULL;
133ci->parent_tree = NULL;
134}
135}
136137
git_diff_init_options(&opts, GIT_DIFF_OPTIONS_VERSION);
138opts.flags |= GIT_DIFF_DISABLE_PATHSPEC_MATCH |
139GIT_DIFF_IGNORE_SUBMODULES |
140GIT_DIFF_INCLUDE_TYPECHANGE;
141if (git_diff_tree_to_tree(&(ci->diff), repo, ci->parent_tree, ci->commit_tree, &opts))
142goto err;
143144
if (git_diff_find_init_options(&fopts, GIT_DIFF_FIND_OPTIONS_VERSION))
145goto err;
146/* find renames and copies, exact matches (no heuristic) for renames. */
147fopts.flags |= GIT_DIFF_FIND_RENAMES | GIT_DIFF_FIND_COPIES |
148GIT_DIFF_FIND_EXACT_MATCH_ONLY;
149if (git_diff_find_similar(ci->diff, &fopts))
150goto err;
151152
ndeltas = git_diff_num_deltas(ci->diff);
153if (ndeltas && !(ci->deltas = calloc(ndeltas, sizeof(struct deltainfo *))))
154err(1, "calloc");
155156
for (i = 0; i < ndeltas; i++) {
157if (git_patch_from_diff(&patch, ci->diff, i))
158goto err;
159160
if (!(di = calloc(1, sizeof(struct deltainfo))))
161err(1, "calloc");
162di->patch = patch;
163ci->deltas[i] = di;
164165
delta = git_patch_get_delta(patch);
166167
/* skip stats for binary data */
168if (delta->flags & GIT_DIFF_FLAG_BINARY)
169continue;
170171
nhunks = git_patch_num_hunks(patch);
172for (j = 0; j < nhunks; j++) {
173if (git_patch_get_hunk(&hunk, &nhunklines, patch, j))
174break;
175for (k = 0; ; k++) {
176if (git_patch_get_line_in_hunk(&line, patch, j, k))
177break;
178if (line->old_lineno == -1) {
179di->addcount++;
180ci->addcount++;
181} else if (line->new_lineno == -1) {
182di->delcount++;
183ci->delcount++;
184}
185}
186}
187}
188ci->ndeltas = i;
189ci->filecount = i;
190191
return 0;
192193
err:
194git_diff_free(ci->diff);
195ci->diff = NULL;
196git_tree_free(ci->commit_tree);
197ci->commit_tree = NULL;
198git_tree_free(ci->parent_tree);
199ci->parent_tree = NULL;
200git_commit_free(ci->parent);
201ci->parent = NULL;
202203
if (ci->deltas)
204for (i = 0; i < ci->ndeltas; i++)
205deltainfo_free(ci->deltas[i]);
206free(ci->deltas);
207ci->deltas = NULL;
208ci->ndeltas = 0;
209ci->addcount = 0;
210ci->delcount = 0;
211ci->filecount = 0;
212213
return -1;
214}
215216
void
217commitinfo_free(struct commitinfo *ci)
218{
219size_t i;
220221
if (!ci)
222return;
223if (ci->deltas)
224for (i = 0; i < ci->ndeltas; i++)
225deltainfo_free(ci->deltas[i]);
226227
free(ci->deltas);
228git_diff_free(ci->diff);
229git_tree_free(ci->commit_tree);
230git_tree_free(ci->parent_tree);
231git_commit_free(ci->commit);
232git_commit_free(ci->parent);
233memset(ci, 0, sizeof(*ci));
234free(ci);
235}
236237
struct commitinfo *
238commitinfo_getbyoid(const git_oid *id)
239{
240struct commitinfo *ci;
241242
if (!(ci = calloc(1, sizeof(struct commitinfo))))
243err(1, "calloc");
244245
if (git_commit_lookup(&(ci->commit), repo, id))
246goto err;
247ci->id = id;
248249
git_oid_tostr(ci->oid, sizeof(ci->oid), git_commit_id(ci->commit));
250git_oid_tostr(ci->parentoid, sizeof(ci->parentoid), git_commit_parent_id(ci->commit, 0));
251252
ci->author = git_commit_author(ci->commit);
253ci->committer = git_commit_committer(ci->commit);
254ci->summary = git_commit_summary(ci->commit);
255ci->msg = git_commit_message(ci->commit);
256257
return ci;
258259
err:
260commitinfo_free(ci);
261262
return NULL;
263}
264265
int
266refs_cmp(const void *v1, const void *v2)
267{
268const struct referenceinfo *r1 = v1, *r2 = v2;
269time_t t1, t2;
270int r;
271272
if ((r = git_reference_is_tag(r1->ref) - git_reference_is_tag(r2->ref)))
273return r;
274275
t1 = r1->ci->author ? r1->ci->author->when.time : 0;
276t2 = r2->ci->author ? r2->ci->author->when.time : 0;
277if ((r = t1 > t2 ? -1 : (t1 == t2 ? 0 : 1)))
278return r;
279280
return strcmp(git_reference_shorthand(r1->ref),
281git_reference_shorthand(r2->ref));
282}
283284
int
285getrefs(struct referenceinfo **pris, size_t *prefcount)
286{
287struct referenceinfo *ris = NULL;
288struct commitinfo *ci = NULL;
289git_reference_iterator *it = NULL;
290const git_oid *id = NULL;
291git_object *obj = NULL;
292git_reference *dref = NULL, *r, *ref = NULL;
293size_t i, refcount;
294295
*pris = NULL;
296*prefcount = 0;
297298
if (git_reference_iterator_new(&it, repo))
299return -1;
300301
for (refcount = 0; !git_reference_next(&ref, it); ) {
302if (!git_reference_is_branch(ref) && !git_reference_is_tag(ref)) {
303git_reference_free(ref);
304ref = NULL;
305continue;
306}
307308
switch (git_reference_type(ref)) {
309case GIT_REF_SYMBOLIC:
310if (git_reference_resolve(&dref, ref))
311goto err;
312r = dref;
313break;
314case GIT_REF_OID:
315r = ref;
316break;
317default:
318continue;
319}
320if (!git_reference_target(r) ||
321git_reference_peel(&obj, r, GIT_OBJ_ANY))
322goto err;
323if (!(id = git_object_id(obj)))
324goto err;
325if (!(ci = commitinfo_getbyoid(id)))
326break;
327328
if (!(ris = reallocarray(ris, refcount + 1, sizeof(*ris))))
329err(1, "realloc");
330ris[refcount].ci = ci;
331ris[refcount].ref = r;
332refcount++;
333334
git_object_free(obj);
335obj = NULL;
336git_reference_free(dref);
337dref = NULL;
338}
339git_reference_iterator_free(it);
340341
/* sort by type, date then shorthand name */
342qsort(ris, refcount, sizeof(*ris), refs_cmp);
343344
*pris = ris;
345*prefcount = refcount;
346347
return 0;
348349
err:
350git_object_free(obj);
351git_reference_free(dref);
352commitinfo_free(ci);
353for (i = 0; i < refcount; i++) {
354commitinfo_free(ris[i].ci);
355git_reference_free(ris[i].ref);
356}
357free(ris);
358359
return -1;
360}
361362
FILE *
363efopen(const char *filename, const char *flags)
364{
365FILE *fp;
366367
if (!(fp = fopen(filename, flags)))
368err(1, "fopen: '%s'", filename);
369370
return fp;
371}
372373
/* Percent-encode, see RFC3986 section 2.1. */
374void
375percentencode(FILE *fp, const char *s, size_t len)
376{
377static char tab[] = "0123456789ABCDEF";
378unsigned char uc;
379size_t i;
380381
for (i = 0; *s && i < len; s++, i++) {
382uc = *s;
383/* NOTE: do not encode '/' for paths or ",-." */
384if (uc < ',' || uc >= 127 || (uc >= ':' && uc <= '@') ||
385uc == '[' || uc == ']') {
386putc('%', fp);
387putc(tab[(uc >> 4) & 0x0f], fp);
388putc(tab[uc & 0x0f], fp);
389} else {
390putc(uc, fp);
391}
392}
393}
394395
/* Escape characters below as HTML 2.0 / XML 1.0. */
396void
397xmlencode(FILE *fp, const char *s, size_t len)
398{
399size_t i;
400401
for (i = 0; *s && i < len; s++, i++) {
402switch(*s) {
403case '<': fputs("<", fp); break;
404case '>': fputs(">", fp); break;
405case '\'': fputs("'", fp); break;
406case '&': fputs("&", fp); break;
407case '"': fputs(""", fp); break;
408default: putc(*s, fp);
409}
410}
411}
412413
/* Escape characters below as HTML 2.0 / XML 1.0, ignore printing '\r', '\n' */
414void
415xmlencodeline(FILE *fp, const char *s, size_t len)
416{
417size_t i;
418419
for (i = 0; *s && i < len; s++, i++) {
420switch(*s) {
421case '<': fputs("<", fp); break;
422case '>': fputs(">", fp); break;
423case '\'': fputs("'", fp); break;
424case '&': fputs("&", fp); break;
425case '"': fputs(""", fp); break;
426case '\r': break; /* ignore CR */
427case '\n': break; /* ignore LF */
428default: putc(*s, fp);
429}
430}
431}
432433
int
434mkdirp(const char *path)
435{
436char tmp[PATH_MAX], *p;
437438
if (strlcpy(tmp, path, sizeof(tmp)) >= sizeof(tmp))
439errx(1, "path truncated: '%s'", path);
440for (p = tmp + (tmp[0] == '/'); *p; p++) {
441if (*p != '/')
442continue;
443*p = '\0';
444if (mkdir(tmp, S_IRWXU | S_IRWXG | S_IRWXO) < 0 && errno != EEXIST)
445return -1;
446*p = '/';
447}
448if (mkdir(tmp, S_IRWXU | S_IRWXG | S_IRWXO) < 0 && errno != EEXIST)
449return -1;
450return 0;
451}
452453
void
454printtimez(FILE *fp, const git_time *intime)
455{
456struct tm *intm;
457time_t t;
458char out[32];
459460
t = (time_t)intime->time;
461if (!(intm = gmtime(&t)))
462return;
463strftime(out, sizeof(out), "%Y-%m-%dT%H:%M:%SZ", intm);
464fputs(out, fp);
465}
466467
void
468printtime(FILE *fp, const git_time *intime)
469{
470struct tm *intm;
471time_t t;
472char out[32];
473474
t = (time_t)intime->time + (intime->offset * 60);
475if (!(intm = gmtime(&t)))
476return;
477strftime(out, sizeof(out), "%a, %e %b %Y %H:%M:%S", intm);
478if (intime->offset < 0)
479fprintf(fp, "%s -%02d%02d", out,
480-(intime->offset) / 60, -(intime->offset) % 60);
481else
482fprintf(fp, "%s +%02d%02d", out,
483intime->offset / 60, intime->offset % 60);
484}
485486
void
487printtimeshort(FILE *fp, const git_time *intime)
488{
489struct tm *intm;
490time_t t;
491char out[32];
492493
t = (time_t)intime->time;
494if (!(intm = gmtime(&t)))
495return;
496strftime(out, sizeof(out), "%Y-%m-%d %H:%M", intm);
497fputs(out, fp);
498}
499500
void
501writeheader(FILE *fp, const char *title)
502{
503fputs("<!DOCTYPE html>\n"
504"<html lang=\"en\">\n<head>\n"
505"<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n"
506"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n"
507"<title>", fp);
508xmlencode(fp, title, strlen(title));
509if (title[0] && strippedname[0])
510fputs(" - ", fp);
511xmlencode(fp, strippedname, strlen(strippedname));
512if (description[0])
513fputs(" - ", fp);
514xmlencode(fp, description, strlen(description));
515fprintf(fp, "</title>\n<link rel=\"icon\" type=\"image/png\" href=\"%sfavicon.png\" />\n", relpath);
516fputs("<link rel=\"alternate\" type=\"application/atom+xml\" title=\"", fp);
517xmlencode(fp, name, strlen(name));
518fprintf(fp, " Atom Feed\" href=\"%satom.xml\" />\n", relpath);
519fputs("<link rel=\"alternate\" type=\"application/atom+xml\" title=\"", fp);
520xmlencode(fp, name, strlen(name));
521fprintf(fp, " Atom Feed (tags)\" href=\"%stags.xml\" />\n", relpath);
522fprintf(fp, "<link rel=\"stylesheet\" type=\"text/css\" href=\"%sstyle.css\" />\n", relpath);
523524
/*
525* add font and script for syntax highlighting
526*/
527528
fputs("<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n", fp);
529530
fputs("<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n", fp);
531532
fputs("<link href=\"https://fonts.googleapis.com/css2?family=Source+Code+Pro:ital,wght@0,200..900;1,200..900&display=swap\" rel=\"stylesheet\">\n",fp);
533534
/*
535* syntax highlight
536*/
537fputs("<script>\n"
538"const prefersLightTheme = window.matchMedia('(prefers-color-scheme: light)');\n"
539"if (prefersLightTheme.matches) {\n"
540"var link = document.createElement('link');\n"
541"link.rel = 'stylesheet';\n"
542"link.href = 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/xcode.css'; // brown-paper, intellij-light.css\n"
543"document.head.appendChild(link);\n"
544"}\n"
545"else {\n"
546"var link = document.createElement('link');\n"
547"link.rel = 'stylesheet';\n"
548"link.href = 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/base16/tender.css';\n"
549"document.head.appendChild(link);\n"
550"}\n"
551"</script>\n",fp);
552553
554
fputs("<script src=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js\"></script>\n", fp);
555fputs("<script src=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/go.min.js\"></script>\n", fp);
556fputs("<script> hljs.highlightAll() ;</script>\n", fp);
557558
559
/*fix nextline code issue*/
560fputs("<style>\n"
561"pre code.hljs {\n"
562"display: inline;\n"
563"padding: 0;\n"
564"font-family: \"Source Code Pro\", monospace;\n"
565"font-optical-sizing: auto;\n"
566"font-weight: 500;\n"
567"font-style: normal;\n"
568"}\n"
569"code.hljs {padding: 0;}\n"
570".hljs {background: initial;}\n"
571"@media (prefers-color-scheme: dark) {\n"
572".hljs-comment{color: rgb(100, 100, 100);}\n"
573"}\n"
574"</style>\n",fp);
575576
577
/*
578* add vim keybindings
579*/
580// fputs("<script src=\"https://cdnjs.cloudflare.com/ajax"
581// "/libs/mousetrap/1.6.3/mousetrap.min.js\"></script>\n", fp);
582//
583//
584// fputs("<script>\n"
585// "Mousetrap.bind('j', function() {\n"
586// "window.scrollBy(0, 100);\n"
587// "return false;\n"
588// "});\n"
589// "Mousetrap.bind('k', function() {\n"
590// "window.scrollBy(0, -100);\n"
591// "return false;\n"
592// "});\n"
593// "</script>\n", fp);
594//
595596
fputs("</head>\n<body>\n<table><tr><td>", fp);
597fprintf(fp, "<a href=\"../%s\"><img src=\"%slogo.png\" alt=\"\" width=\"32\" height=\"32\" /></a>",
598relpath, relpath);
599fputs("</td><td><h1>", fp);
600xmlencode(fp, strippedname, strlen(strippedname));
601fputs("</h1><span class=\"desc\">", fp);
602xmlencode(fp, description, strlen(description));
603fputs("</span></td></tr>", fp);
604if (cloneurl[0]) {
605fputs("<tr class=\"url\"><td></td><td>git clone <a href=\"", fp);
606xmlencode(fp, cloneurl, strlen(cloneurl)); /* not percent-encoded */
607fputs("\">", fp);
608xmlencode(fp, cloneurl, strlen(cloneurl));
609fputs("</a></td></tr>", fp);
610}
611fputs("<tr><td></td><td>\n", fp);
612/*
613* more space before files, log etc.
614*/
615fputs("<br>\n", fp);
616fprintf(fp, "<a href=\"%slog.html\">Log</a> | ", relpath);
617fprintf(fp, "<a href=\"%sfiles.html\">Files</a> | ", relpath);
618fprintf(fp, "<a href=\"%srefs.html\">Refs</a>", relpath);
619if (submodules)
620fprintf(fp, " | <a href=\"%sfile/%s.html\">Submodules</a>",
621relpath, submodules);
622if (readme)
623fprintf(fp, " | <a href=\"%sfile/%s.html\">README</a>",
624relpath, readme);
625if (license)
626fprintf(fp, " | <a href=\"%sfile/%s.html\">LICENSE</a>",
627relpath, license);
628fputs("</td></tr></table>\n<hr/>\n<div id=\"content\">\n", fp);
629}
630631
void
632writefooter(FILE *fp)
633{
634fputs("</div>\n</body>\n</html>\n", fp);
635}
636637
/*
638* added parameter filename, so the <code class=?> can be determined
639*/
640size_t
641writeblobhtml(FILE *fp, const git_blob *blob, const char* filename)
642{
643size_t n = 0, i, len, prev;
644const char *nfmt_old = "<a href=\"#l%zu\" class=\"line\" id=\"l%zu\">%7zu</a>"; //removed <code> here
645const char *s = git_blob_rawcontent(blob);
646647
len = git_blob_rawsize(blob);
648fputs("<pre id=\"blob\">\n", fp);
649650
char code_opening[250];
651652
char this_lang[200];
653const char *ret = get_lang(filename);
654if (!ret)
655strcpy(this_lang, "plaintext");
656else
657strcpy(this_lang, ret);
658659
sprintf(code_opening, "<code class=\"language-%s\"> ", this_lang);
660char *nfmt = (char *) malloc( strlen(nfmt_old) + strlen(code_opening) + 1);
661strcpy(nfmt, nfmt_old);
662strcat(nfmt, code_opening);
663664
if (len > 0) {
665for (i = 0, prev = 0; i < len; i++) {
666if (s[i] != '\n')
667continue;
668n++;
669fprintf(fp, nfmt, n, n, n);
670xmlencodeline(fp, &s[prev], i - prev + 1);
671/*
672* odd but maybe highlight.js inserts its own
673* newline, so we are not putting ours
674* that did'nt fix it
675*/
676fputs("</code>\n", fp);
677prev = i + 1;
678}
679/* trailing data */
680if ((len - prev) > 0) {
681n++;
682fprintf(fp, nfmt, n, n, n);
683xmlencodeline(fp, &s[prev], len - prev);
684}
685}
686687
fputs("</pre>\n", fp);
688689
return n;
690}
691692
void
693printcommit(FILE *fp, struct commitinfo *ci)
694{
695fprintf(fp, "<b>commit</b> <a href=\"%scommit/%s.html\">%s</a>\n",
696relpath, ci->oid, ci->oid);
697698
if (ci->parentoid[0])
699fprintf(fp, "<b>parent</b> <a href=\"%scommit/%s.html\">%s</a>\n",
700relpath, ci->parentoid, ci->parentoid);
701702
if (ci->author) {
703fputs("<b>Author:</b> ", fp);
704xmlencode(fp, ci->author->name, strlen(ci->author->name));
705fputs(" <<a href=\"mailto:", fp);
706xmlencode(fp, ci->author->email, strlen(ci->author->email)); /* not percent-encoded */
707fputs("\">", fp);
708xmlencode(fp, ci->author->email, strlen(ci->author->email));
709fputs("</a>>\n<b>Date:</b> ", fp);
710printtime(fp, &(ci->author->when));
711putc('\n', fp);
712}
713if (ci->msg) {
714putc('\n', fp);
715xmlencode(fp, ci->msg, strlen(ci->msg));
716putc('\n', fp);
717}
718}
719720
void
721printshowfile(FILE *fp, struct commitinfo *ci)
722{
723const git_diff_delta *delta;
724const git_diff_hunk *hunk;
725const git_diff_line *line;
726git_patch *patch;
727size_t nhunks, nhunklines, changed, add, del, total, i, j, k;
728char linestr[80];
729int c;
730731
printcommit(fp, ci);
732733
if (!ci->deltas)
734return;
735736
if (ci->filecount > 1000 ||
737ci->ndeltas > 1000 ||
738ci->addcount > 100000 ||
739ci->delcount > 100000) {
740fputs("Diff is too large, output suppressed.\n", fp);
741return;
742}
743744
/* diff stat */
745fputs("<b>Diffstat:</b>\n<table>", fp);
746for (i = 0; i < ci->ndeltas; i++) {
747delta = git_patch_get_delta(ci->deltas[i]->patch);
748749
switch (delta->status) {
750case GIT_DELTA_ADDED: c = 'A'; break;
751case GIT_DELTA_COPIED: c = 'C'; break;
752case GIT_DELTA_DELETED: c = 'D'; break;
753case GIT_DELTA_MODIFIED: c = 'M'; break;
754case GIT_DELTA_RENAMED: c = 'R'; break;
755case GIT_DELTA_TYPECHANGE: c = 'T'; break;
756default: c = ' '; break;
757}
758if (c == ' ')
759fprintf(fp, "<tr><td>%c", c);
760else
761fprintf(fp, "<tr><td class=\"%c\">%c", c, c);
762763
fprintf(fp, "</td><td><a href=\"#h%zu\">", i);
764xmlencode(fp, delta->old_file.path, strlen(delta->old_file.path));
765if (strcmp(delta->old_file.path, delta->new_file.path)) {
766fputs(" -> ", fp);
767xmlencode(fp, delta->new_file.path, strlen(delta->new_file.path));
768}
769770
add = ci->deltas[i]->addcount;
771del = ci->deltas[i]->delcount;
772changed = add + del;
773total = sizeof(linestr) - 2;
774if (changed > total) {
775if (add)
776add = ((float)total / changed * add) + 1;
777if (del)
778del = ((float)total / changed * del) + 1;
779}
780memset(&linestr, '+', add);
781memset(&linestr[add], '-', del);
782783
fprintf(fp, "</a></td><td> | </td><td class=\"num\">%zu</td><td><span class=\"i\">",
784ci->deltas[i]->addcount + ci->deltas[i]->delcount);
785fwrite(&linestr, 1, add, fp);
786fputs("</span><span class=\"d\">", fp);
787fwrite(&linestr[add], 1, del, fp);
788fputs("</span></td></tr>\n", fp);
789}
790fprintf(fp, "</table></pre><pre>%zu file%s changed, %zu insertion%s(+), %zu deletion%s(-)\n",
791ci->filecount, ci->filecount == 1 ? "" : "s",
792ci->addcount, ci->addcount == 1 ? "" : "s",
793ci->delcount, ci->delcount == 1 ? "" : "s");
794795
fputs("<hr/>", fp);
796797
for (i = 0; i < ci->ndeltas; i++) {
798patch = ci->deltas[i]->patch;
799delta = git_patch_get_delta(patch);
800fprintf(fp, "<b>diff --git a/<a id=\"h%zu\" href=\"%sfile/", i, relpath);
801percentencode(fp, delta->old_file.path, strlen(delta->old_file.path));
802fputs(".html\">", fp);
803xmlencode(fp, delta->old_file.path, strlen(delta->old_file.path));
804fprintf(fp, "</a> b/<a href=\"%sfile/", relpath);
805percentencode(fp, delta->new_file.path, strlen(delta->new_file.path));
806fprintf(fp, ".html\">");
807xmlencode(fp, delta->new_file.path, strlen(delta->new_file.path));
808fprintf(fp, "</a></b>\n");
809810
/* check binary data */
811if (delta->flags & GIT_DIFF_FLAG_BINARY) {
812fputs("Binary files differ.\n", fp);
813continue;
814}
815816
nhunks = git_patch_num_hunks(patch);
817for (j = 0; j < nhunks; j++) {
818if (git_patch_get_hunk(&hunk, &nhunklines, patch, j))
819break;
820821
fprintf(fp, "<a href=\"#h%zu-%zu\" id=\"h%zu-%zu\" class=\"h\">", i, j, i, j);
822xmlencode(fp, hunk->header, hunk->header_len);
823fputs("</a>", fp);
824825
for (k = 0; ; k++) {
826if (git_patch_get_line_in_hunk(&line, patch, j, k))
827break;
828if (line->old_lineno == -1)
829fprintf(fp, "<a href=\"#h%zu-%zu-%zu\" id=\"h%zu-%zu-%zu\" class=\"i\">+",
830i, j, k, i, j, k);
831else if (line->new_lineno == -1)
832fprintf(fp, "<a href=\"#h%zu-%zu-%zu\" id=\"h%zu-%zu-%zu\" class=\"d\">-",
833i, j, k, i, j, k);
834else
835putc(' ', fp);
836xmlencodeline(fp, line->content, line->content_len);
837putc('\n', fp);
838if (line->old_lineno == -1 || line->new_lineno == -1)
839fputs("</a>", fp);
840}
841}
842}
843}
844845
void
846writelogline(FILE *fp, struct commitinfo *ci)
847{
848fputs("<tr><td>", fp);
849if (ci->author)
850printtimeshort(fp, &(ci->author->when));
851fputs("</td><td>", fp);
852if (ci->summary) {
853fprintf(fp, "<a href=\"%scommit/%s.html\">", relpath, ci->oid);
854xmlencode(fp, ci->summary, strlen(ci->summary));
855fputs("</a>", fp);
856}
857fputs("</td><td>", fp);
858if (ci->author)
859xmlencode(fp, ci->author->name, strlen(ci->author->name));
860fputs("</td><td class=\"num\" align=\"right\">", fp);
861fprintf(fp, "%zu", ci->filecount);
862fputs("</td><td class=\"num\" align=\"right\">", fp);
863fprintf(fp, "+%zu", ci->addcount);
864fputs("</td><td class=\"num\" align=\"right\">", fp);
865fprintf(fp, "-%zu", ci->delcount);
866fputs("</td></tr>\n", fp);
867}
868869
int
870writelog(FILE *fp, const git_oid *oid)
871{
872struct commitinfo *ci;
873git_revwalk *w = NULL;
874git_oid id;
875char path[PATH_MAX], oidstr[GIT_OID_HEXSZ + 1];
876FILE *fpfile;
877size_t remcommits = 0;
878int r;
879880
git_revwalk_new(&w, repo);
881git_revwalk_push(w, oid);
882883
while (!git_revwalk_next(&id, w)) {
884relpath = "";
885886
if (cachefile && !memcmp(&id, &lastoid, sizeof(id)))
887break;
888889
git_oid_tostr(oidstr, sizeof(oidstr), &id);
890r = snprintf(path, sizeof(path), "commit/%s.html", oidstr);
891if (r < 0 || (size_t)r >= sizeof(path))
892errx(1, "path truncated: 'commit/%s.html'", oidstr);
893r = access(path, F_OK);
894895
/* optimization: if there are no log lines to write and
896the commit file already exists: skip the diffstat */
897if (!nlogcommits) {
898remcommits++;
899if (!r)
900continue;
901}
902903
if (!(ci = commitinfo_getbyoid(&id)))
904break;
905/* diffstat: for stagit HTML required for the log.html line */
906if (commitinfo_getstats(ci) == -1)
907goto err;
908909
if (nlogcommits != 0) {
910writelogline(fp, ci);
911if (nlogcommits > 0)
912nlogcommits--;
913}
914915
if (cachefile)
916writelogline(wcachefp, ci);
917918
/* check if file exists if so skip it */
919if (r) {
920relpath = "../";
921fpfile = efopen(path, "w");
922writeheader(fpfile, ci->summary);
923fputs("<pre>", fpfile);
924printshowfile(fpfile, ci);
925fputs("</pre>\n", fpfile);
926writefooter(fpfile);
927checkfileerror(fpfile, path, 'w');
928fclose(fpfile);
929}
930err:
931commitinfo_free(ci);
932}
933git_revwalk_free(w);
934935
if (nlogcommits == 0 && remcommits != 0) {
936fprintf(fp, "<tr><td></td><td colspan=\"5\">"
937"%zu more commits remaining, fetch the repository"
938"</td></tr>\n", remcommits);
939}
940941
relpath = "";
942943
return 0;
944}
945946
void
947printcommitatom(FILE *fp, struct commitinfo *ci, const char *tag)
948{
949fputs("<entry>\n", fp);
950951
fprintf(fp, "<id>%s</id>\n", ci->oid);
952if (ci->author) {
953fputs("<published>", fp);
954printtimez(fp, &(ci->author->when));
955fputs("</published>\n", fp);
956}
957if (ci->committer) {
958fputs("<updated>", fp);
959printtimez(fp, &(ci->committer->when));
960fputs("</updated>\n", fp);
961}
962if (ci->summary) {
963fputs("<title>", fp);
964if (tag && tag[0]) {
965fputs("[", fp);
966xmlencode(fp, tag, strlen(tag));
967fputs("] ", fp);
968}
969xmlencode(fp, ci->summary, strlen(ci->summary));
970fputs("</title>\n", fp);
971}
972fprintf(fp, "<link rel=\"alternate\" type=\"text/html\" href=\"%scommit/%s.html\" />\n",
973baseurl, ci->oid);
974975
if (ci->author) {
976fputs("<author>\n<name>", fp);
977xmlencode(fp, ci->author->name, strlen(ci->author->name));
978fputs("</name>\n<email>", fp);
979xmlencode(fp, ci->author->email, strlen(ci->author->email));
980fputs("</email>\n</author>\n", fp);
981}
982983
fputs("<content>", fp);
984fprintf(fp, "commit %s\n", ci->oid);
985if (ci->parentoid[0])
986fprintf(fp, "parent %s\n", ci->parentoid);
987if (ci->author) {
988fputs("Author: ", fp);
989xmlencode(fp, ci->author->name, strlen(ci->author->name));
990fputs(" <", fp);
991xmlencode(fp, ci->author->email, strlen(ci->author->email));
992fputs(">\nDate: ", fp);
993printtime(fp, &(ci->author->when));
994putc('\n', fp);
995}
996if (ci->msg) {
997putc('\n', fp);
998xmlencode(fp, ci->msg, strlen(ci->msg));
999}
1000fputs("\n</content>\n</entry>\n", fp);
1001}
10021003
int
1004writeatom(FILE *fp, int all)
1005{
1006struct referenceinfo *ris = NULL;
1007size_t refcount = 0;
1008struct commitinfo *ci;
1009git_revwalk *w = NULL;
1010git_oid id;
1011size_t i, m = 100; /* last 'm' commits */
10121013
fputs("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
1014"<feed xmlns=\"http://www.w3.org/2005/Atom\">\n<title>", fp);
1015xmlencode(fp, strippedname, strlen(strippedname));
1016fputs(", branch HEAD</title>\n<subtitle>", fp);
1017xmlencode(fp, description, strlen(description));
1018fputs("</subtitle>\n", fp);
10191020
/* all commits or only tags? */
1021if (all) {
1022git_revwalk_new(&w, repo);
1023git_revwalk_push_head(w);
1024for (i = 0; i < m && !git_revwalk_next(&id, w); i++) {
1025if (!(ci = commitinfo_getbyoid(&id)))
1026break;
1027printcommitatom(fp, ci, "");
1028commitinfo_free(ci);
1029}
1030git_revwalk_free(w);
1031} else if (getrefs(&ris, &refcount) != -1) {
1032/* references: tags */
1033for (i = 0; i < refcount; i++) {
1034if (git_reference_is_tag(ris[i].ref))
1035printcommitatom(fp, ris[i].ci,
1036git_reference_shorthand(ris[i].ref));
10371038
commitinfo_free(ris[i].ci);
1039git_reference_free(ris[i].ref);
1040}
1041free(ris);
1042}
10431044
fputs("</feed>\n", fp);
10451046
return 0;
1047}
10481049
size_t
1050writeblob(git_object *obj, const char *fpath, const char *filename, size_t filesize)
1051{
1052char tmp[PATH_MAX] = "", *d;
1053const char *p;
1054size_t lc = 0;
1055FILE *fp;
10561057
if (strlcpy(tmp, fpath, sizeof(tmp)) >= sizeof(tmp))
1058errx(1, "path truncated: '%s'", fpath);
1059if (!(d = dirname(tmp)))
1060err(1, "dirname");
1061if (mkdirp(d))
1062return -1;
10631064
for (p = fpath, tmp[0] = '\0'; *p; p++) {
1065if (*p == '/' && strlcat(tmp, "../", sizeof(tmp)) >= sizeof(tmp))
1066errx(1, "path truncated: '../%s'", tmp);
1067}
1068relpath = tmp;
10691070
fp = efopen(fpath, "w");
1071writeheader(fp, filename);
1072fputs("<p> ", fp);
1073xmlencode(fp, filename, strlen(filename));
1074fprintf(fp, " (%zuB)", filesize);
1075fputs("</p><hr/>", fp);
10761077
if (git_blob_is_binary((git_blob *)obj))
1078fputs("<p>Binary file.</p>\n", fp);
1079else
1080// passing parameter to be used by the code
1081lc = writeblobhtml(fp, (git_blob *)obj, filename);
10821083
writefooter(fp);
1084checkfileerror(fp, fpath, 'w');
1085fclose(fp);
10861087
relpath = "";
10881089
return lc;
1090}
10911092
const char *
1093filemode(git_filemode_t m)
1094{
1095static char mode[11];
10961097
memset(mode, '-', sizeof(mode) - 1);
1098mode[10] = '\0';
10991100
if (S_ISREG(m))
1101mode[0] = '-';
1102else if (S_ISBLK(m))
1103mode[0] = 'b';
1104else if (S_ISCHR(m))
1105mode[0] = 'c';
1106else if (S_ISDIR(m))
1107mode[0] = 'd';
1108else if (S_ISFIFO(m))
1109mode[0] = 'p';
1110else if (S_ISLNK(m))
1111mode[0] = 'l';
1112else if (S_ISSOCK(m))
1113mode[0] = 's';
1114else
1115mode[0] = '?';
11161117
if (m & S_IRUSR) mode[1] = 'r';
1118if (m & S_IWUSR) mode[2] = 'w';
1119if (m & S_IXUSR) mode[3] = 'x';
1120if (m & S_IRGRP) mode[4] = 'r';
1121if (m & S_IWGRP) mode[5] = 'w';
1122if (m & S_IXGRP) mode[6] = 'x';
1123if (m & S_IROTH) mode[7] = 'r';
1124if (m & S_IWOTH) mode[8] = 'w';
1125if (m & S_IXOTH) mode[9] = 'x';
11261127
if (m & S_ISUID) mode[3] = (mode[3] == 'x') ? 's' : 'S';
1128if (m & S_ISGID) mode[6] = (mode[6] == 'x') ? 's' : 'S';
1129if (m & S_ISVTX) mode[9] = (mode[9] == 'x') ? 't' : 'T';
11301131
return mode;
1132}
11331134
int
1135writefilestree(FILE *fp, git_tree *tree, const char *path)
1136{
1137const git_tree_entry *entry = NULL;
1138git_object *obj = NULL;
1139const char *entryname;
1140char filepath[PATH_MAX], entrypath[PATH_MAX], oid[8];
1141size_t count, i, lc, filesize;
1142int r, ret;
11431144
count = git_tree_entrycount(tree);
1145for (i = 0; i < count; i++) {
1146if (!(entry = git_tree_entry_byindex(tree, i)) ||
1147!(entryname = git_tree_entry_name(entry)))
1148return -1;
1149joinpath(entrypath, sizeof(entrypath), path, entryname);
11501151
r = snprintf(filepath, sizeof(filepath), "file/%s.html",
1152entrypath);
1153if (r < 0 || (size_t)r >= sizeof(filepath))
1154errx(1, "path truncated: 'file/%s.html'", entrypath);
11551156
if (!git_tree_entry_to_object(&obj, repo, entry)) {
1157switch (git_object_type(obj)) {
1158case GIT_OBJ_BLOB:
1159break;
1160case GIT_OBJ_TREE:
1161/* NOTE: recurses */
1162ret = writefilestree(fp, (git_tree *)obj,
1163entrypath);
1164git_object_free(obj);
1165if (ret)
1166return ret;
1167continue;
1168default:
1169git_object_free(obj);
1170continue;
1171}
11721173
filesize = git_blob_rawsize((git_blob *)obj);
1174lc = writeblob(obj, filepath, entryname, filesize);
11751176
fputs("<tr><td>", fp);
1177fputs(filemode(git_tree_entry_filemode(entry)), fp);
1178fprintf(fp, "</td><td><a href=\"%s", relpath);
1179percentencode(fp, filepath, strlen(filepath));
1180fputs("\">", fp);
1181xmlencode(fp, entrypath, strlen(entrypath));
1182fputs("</a></td><td class=\"num\" align=\"right\">", fp);
1183if (lc > 0)
1184fprintf(fp, "%zuL", lc);
1185else
1186fprintf(fp, "%zuB", filesize);
1187fputs("</td></tr>\n", fp);
1188git_object_free(obj);
1189} else if (git_tree_entry_type(entry) == GIT_OBJ_COMMIT) {
1190/* commit object in tree is a submodule */
1191fprintf(fp, "<tr><td>m---------</td><td><a href=\"%sfile/.gitmodules.html\">",
1192relpath);
1193xmlencode(fp, entrypath, strlen(entrypath));
1194fputs("</a> @ ", fp);
1195git_oid_tostr(oid, sizeof(oid), git_tree_entry_id(entry));
1196xmlencode(fp, oid, strlen(oid));
1197fputs("</td><td class=\"num\" align=\"right\"></td></tr>\n", fp);
1198}
1199}
12001201
return 0;
1202}
12031204
int
1205writefiles(FILE *fp, const git_oid *id)
1206{
1207git_tree *tree = NULL;
1208git_commit *commit = NULL;
1209int ret = -1;
12101211
fputs("<table id=\"files\"><thead>\n<tr>"
1212"<td><b>Mode</b></td><td><b>Name</b></td>"
1213"<td class=\"num\" align=\"right\"><b>Size</b></td>"
1214"</tr>\n</thead><tbody>\n", fp);
12151216
if (!git_commit_lookup(&commit, repo, id) &&
1217!git_commit_tree(&tree, commit))
1218ret = writefilestree(fp, tree, "");
12191220
fputs("</tbody></table>", fp);
12211222
git_commit_free(commit);
1223git_tree_free(tree);
12241225
return ret;
1226}
12271228
int
1229writerefs(FILE *fp)
1230{
1231struct referenceinfo *ris = NULL;
1232struct commitinfo *ci;
1233size_t count, i, j, refcount;
1234const char *titles[] = { "Branches", "Tags" };
1235const char *ids[] = { "branches", "tags" };
1236const char *s;
12371238
if (getrefs(&ris, &refcount) == -1)
1239return -1;
12401241
for (i = 0, j = 0, count = 0; i < refcount; i++) {
1242if (j == 0 && git_reference_is_tag(ris[i].ref)) {
1243if (count)
1244fputs("</tbody></table><br/>\n", fp);
1245count = 0;
1246j = 1;
1247}
12481249
/* print header if it has an entry (first). */
1250if (++count == 1) {
1251fprintf(fp, "<h2>%s</h2><table id=\"%s\">"
1252"<thead>\n<tr><td><b>Name</b></td>"
1253"<td><b>Last commit date</b></td>"
1254"<td><b>Author</b></td>\n</tr>\n"
1255"</thead><tbody>\n",
1256titles[j], ids[j]);
1257}
12581259
ci = ris[i].ci;
1260s = git_reference_shorthand(ris[i].ref);
12611262
fputs("<tr><td>", fp);
1263xmlencode(fp, s, strlen(s));
1264fputs("</td><td>", fp);
1265if (ci->author)
1266printtimeshort(fp, &(ci->author->when));
1267fputs("</td><td>", fp);
1268if (ci->author)
1269xmlencode(fp, ci->author->name, strlen(ci->author->name));
1270fputs("</td></tr>\n", fp);
1271}
1272/* table footer */
1273if (count)
1274fputs("</tbody></table><br/>\n", fp);
12751276
for (i = 0; i < refcount; i++) {
1277commitinfo_free(ris[i].ci);
1278git_reference_free(ris[i].ref);
1279}
1280free(ris);
12811282
return 0;
1283}
12841285
void
1286usage(char *argv0)
1287{
1288fprintf(stderr, "usage: %s [-c cachefile | -l commits] "
1289"[-u baseurl] repodir\n", argv0);
1290exit(1);
1291}
12921293
int
1294main(int argc, char *argv[])
1295{
1296git_object *obj = NULL;
1297const git_oid *head = NULL;
1298mode_t mask;
1299FILE *fp, *fpread;
1300char path[PATH_MAX], repodirabs[PATH_MAX + 1], *p;
1301char tmppath[64] = "cache.XXXXXXXXXXXX", buf[BUFSIZ];
1302size_t n;
1303int i, fd;
13041305
for (i = 1; i < argc; i++) {
1306if (argv[i][0] != '-') {
1307if (repodir)
1308usage(argv[0]);
1309repodir = argv[i];
1310} else if (argv[i][1] == 'c') {
1311if (nlogcommits > 0 || i + 1 >= argc)
1312usage(argv[0]);
1313cachefile = argv[++i];
1314} else if (argv[i][1] == 'l') {
1315if (cachefile || i + 1 >= argc)
1316usage(argv[0]);
1317errno = 0;
1318nlogcommits = strtoll(argv[++i], &p, 10);
1319if (argv[i][0] == '\0' || *p != '\0' ||
1320nlogcommits <= 0 || errno)
1321usage(argv[0]);
1322} else if (argv[i][1] == 'u') {
1323if (i + 1 >= argc)
1324usage(argv[0]);
1325baseurl = argv[++i];
1326}
1327}
1328if (!repodir)
1329usage(argv[0]);
13301331
if (!realpath(repodir, repodirabs))
1332err(1, "realpath");
13331334
/* do not search outside the git repository:
1335GIT_CONFIG_LEVEL_APP is the highest level currently */
1336git_libgit2_init();
1337for (i = 1; i <= GIT_CONFIG_LEVEL_APP; i++)
1338git_libgit2_opts(GIT_OPT_SET_SEARCH_PATH, i, "");
1339/* do not require the git repository to be owned by the current user */
1340git_libgit2_opts(GIT_OPT_SET_OWNER_VALIDATION, 0);
13411342
#ifdef __OpenBSD__
1343if (unveil(repodir, "r") == -1)
1344err(1, "unveil: %s", repodir);
1345if (unveil(".", "rwc") == -1)
1346err(1, "unveil: .");
1347if (cachefile && unveil(cachefile, "rwc") == -1)
1348err(1, "unveil: %s", cachefile);
13491350
if (cachefile) {
1351if (pledge("stdio rpath wpath cpath fattr", NULL) == -1)
1352err(1, "pledge");
1353} else {
1354if (pledge("stdio rpath wpath cpath", NULL) == -1)
1355err(1, "pledge");
1356}
1357#endif
13581359
if (git_repository_open_ext(&repo, repodir,
1360GIT_REPOSITORY_OPEN_NO_SEARCH, NULL) < 0) {
1361fprintf(stderr, "%s: cannot open repository\n", argv[0]);
1362return 1;
1363}
13641365
/* find HEAD */
1366if (!git_revparse_single(&obj, repo, "HEAD"))
1367head = git_object_id(obj);
1368git_object_free(obj);
13691370
/* use directory name as name */
1371if ((name = strrchr(repodirabs, '/')))
1372name++;
1373else
1374name = "";
13751376
/* strip .git suffix */
1377if (!(strippedname = strdup(name)))
1378err(1, "strdup");
1379if ((p = strrchr(strippedname, '.')))
1380if (!strcmp(p, ".git"))
1381*p = '\0';
13821383
/* read description or .git/description */
1384joinpath(path, sizeof(path), repodir, "description");
1385if (!(fpread = fopen(path, "r"))) {
1386joinpath(path, sizeof(path), repodir, ".git/description");
1387fpread = fopen(path, "r");
1388}
1389if (fpread) {
1390if (!fgets(description, sizeof(description), fpread))
1391description[0] = '\0';
1392checkfileerror(fpread, path, 'r');
1393fclose(fpread);
1394}
13951396
/* read url or .git/url */
1397joinpath(path, sizeof(path), repodir, "url");
1398if (!(fpread = fopen(path, "r"))) {
1399joinpath(path, sizeof(path), repodir, ".git/url");
1400fpread = fopen(path, "r");
1401}
1402if (fpread) {
1403if (!fgets(cloneurl, sizeof(cloneurl), fpread))
1404cloneurl[0] = '\0';
1405checkfileerror(fpread, path, 'r');
1406fclose(fpread);
1407cloneurl[strcspn(cloneurl, "\n")] = '\0';
1408}
14091410
/* check LICENSE */
1411for (i = 0; i < LEN(licensefiles) && !license; i++) {
1412if (!git_revparse_single(&obj, repo, licensefiles[i]) &&
1413git_object_type(obj) == GIT_OBJ_BLOB)
1414license = licensefiles[i] + strlen("HEAD:");
1415git_object_free(obj);
1416}
14171418
/* check README */
1419for (i = 0; i < LEN(readmefiles) && !readme; i++) {
1420if (!git_revparse_single(&obj, repo, readmefiles[i]) &&
1421git_object_type(obj) == GIT_OBJ_BLOB)
1422readme = readmefiles[i] + strlen("HEAD:");
1423git_object_free(obj);
1424}
14251426
if (!git_revparse_single(&obj, repo, "HEAD:.gitmodules") &&
1427git_object_type(obj) == GIT_OBJ_BLOB)
1428submodules = ".gitmodules";
1429git_object_free(obj);
14301431
/* log for HEAD */
1432fp = efopen("log.html", "w");
1433relpath = "";
1434mkdir("commit", S_IRWXU | S_IRWXG | S_IRWXO);
1435writeheader(fp, "Log");
1436fputs("<table id=\"log\"><thead>\n<tr><td><b>Date</b></td>"
1437"<td><b>Commit message</b></td>"
1438"<td><b>Author</b></td><td class=\"num\" align=\"right\"><b>Files</b></td>"
1439"<td class=\"num\" align=\"right\"><b>+</b></td>"
1440"<td class=\"num\" align=\"right\"><b>-</b></td></tr>\n</thead><tbody>\n", fp);
14411442
if (cachefile && head) {
1443/* read from cache file (does not need to exist) */
1444if ((rcachefp = fopen(cachefile, "r"))) {
1445if (!fgets(lastoidstr, sizeof(lastoidstr), rcachefp))
1446errx(1, "%s: no object id", cachefile);
1447if (git_oid_fromstr(&lastoid, lastoidstr))
1448errx(1, "%s: invalid object id", cachefile);
1449}
14501451
/* write log to (temporary) cache */
1452if ((fd = mkstemp(tmppath)) == -1)
1453err(1, "mkstemp");
1454if (!(wcachefp = fdopen(fd, "w")))
1455err(1, "fdopen: '%s'", tmppath);
1456/* write last commit id (HEAD) */
1457git_oid_tostr(buf, sizeof(buf), head);
1458fprintf(wcachefp, "%s\n", buf);
14591460
writelog(fp, head);
14611462
if (rcachefp) {
1463/* append previous log to log.html and the new cache */
1464while (!feof(rcachefp)) {
1465n = fread(buf, 1, sizeof(buf), rcachefp);
1466if (ferror(rcachefp))
1467break;
1468if (fwrite(buf, 1, n, fp) != n ||
1469fwrite(buf, 1, n, wcachefp) != n)
1470break;
1471}
1472checkfileerror(rcachefp, cachefile, 'r');
1473fclose(rcachefp);
1474}
1475checkfileerror(wcachefp, tmppath, 'w');
1476fclose(wcachefp);
1477} else {
1478if (head)
1479writelog(fp, head);
1480}
14811482
fputs("</tbody></table>", fp);
1483writefooter(fp);
1484checkfileerror(fp, "log.html", 'w');
1485fclose(fp);
14861487
/* files for HEAD */
1488fp = efopen("files.html", "w");
1489writeheader(fp, "Files");
1490if (head)
1491writefiles(fp, head);
1492writefooter(fp);
1493checkfileerror(fp, "files.html", 'w');
1494fclose(fp);
14951496
/* summary page with branches and tags */
1497fp = efopen("refs.html", "w");
1498writeheader(fp, "Refs");
1499writerefs(fp);
1500writefooter(fp);
1501checkfileerror(fp, "refs.html", 'w');
1502fclose(fp);
15031504
/* Atom feed */
1505fp = efopen("atom.xml", "w");
1506writeatom(fp, 1);
1507checkfileerror(fp, "atom.xml", 'w');
1508fclose(fp);
15091510
/* Atom feed for tags / releases */
1511fp = efopen("tags.xml", "w");
1512writeatom(fp, 0);
1513checkfileerror(fp, "tags.xml", 'w');
1514fclose(fp);
15151516
/* rename new cache file on success */
1517if (cachefile && head) {
1518if (rename(tmppath, cachefile))
1519err(1, "rename: '%s' to '%s'", tmppath, cachefile);
1520umask((mask = umask(0)));
1521if (chmod(cachefile,
1522(S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH) & ~mask))
1523err(1, "chmod: '%s'", cachefile);
1524}
15251526
/* cleanup */
1527git_repository_free(repo);
1528git_libgit2_shutdown();
15291530
return 0;
1531}