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