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