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