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