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