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