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