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