logo

stagit

STAtic GIT web view generator (in C) git clone https://hacktivis.me/git/stagit.git

stagit.c (36849B)


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