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