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", fp); 544 fputs("<div id=\"webpage-header-link\">\n", fp); 545 fputs("<a href=\"https://kloeckner.com.ar/\">https://kloeckner.com.ar\n", fp); 546 fputs("</a></div>\n<body>\n", fp); 547 fputs("<table id=\"repo-header-table\"><tr><td id=\"repo-logo\">", fp); 548 fprintf(fp, "<a href=\"https://git.kloeckner.com.ar\"><img src=\"%slogo.png\" alt=\"\" width=\"64\" height=\"64\" /></a>", 549 relpath, relpath); 550 fputs("</td><td id=\"repo-header\"><h1 id=\"repo-name\"><a href=\"/\">repos/</a>", fp); 551 xmlencode(fp, strippedname, strlen(strippedname)); 552 fputs("</h1><span id=\"repo-desc\">", fp); 553 xmlencode(fp, description, strlen(description)); 554 fputs("</span>", fp); 555 556 fputs("</td></tr></table>", fp); 557 558 fputs("<div id=\"repo-top-buttons\">\n", fp); 559 // fprintf(fp, "<a <a href=\"https://git.kloeckner.com.ar\">Index</a> ", relpath); 560 fprintf(fp, "<a href=\"%slog.html\">Commits</a> ", relpath); 561 fprintf(fp, "<a href=\"%sfiles.html\">Files</a> ", relpath); 562 fprintf(fp, "<a href=\"%srefs.html\">Refs</a>", relpath); 563 if (submodules) 564 fprintf(fp, " <a href=\"%sfile/%s.html\">Submodules</a>", 565 relpath, submodules); 566 if (readme) 567 fprintf(fp, " <a href=\"%sreadme.html\">README</a>", relpath); 568 if (license) 569 fprintf(fp, " <a href=\"%sfile/%s.html\">LICENSE</a>", 570 relpath, license); 571 572 if (cloneurl[0]) { 573 fputs("<tr class=\"url\"><td></td><td>git clone <a href=\"", fp); 574 xmlencode(fp, cloneurl, strlen(cloneurl)); /* not percent-encoded */ 575 fputs("\">", fp); 576 xmlencode(fp, cloneurl, strlen(cloneurl)); 577 fputs("</a></td></tr>", fp); 578 } 579 580 /* fputs("</div>\n<hr/>\n<div id=\"content\">\n", fp); */ 581 fputs("</div>\n", fp); 582 } 583 584 void 585 writefooter(FILE *fp) 586 { 587 /* fputs("</div>\n</div>\n</body>\n</html>\n", fp); */ 588 fputs("</div>\n</div>\n</body>\n", fp); 589 fputs("<footer>Generated with <a href=\"https://git.kloeckner.com.ar/stagit/\">Stagit</a></footer>\n", fp); 590 fputs("</html>\n", fp); 591 } 592 593 size_t 594 writeblobhtml(FILE *fp, const git_blob *blob) 595 { 596 size_t n = 0, i, len, prev; 597 const char *nfmt = "<a href=\"#l%zu\" class=\"line\" id=\"l%zu\">%4zu</a> "; 598 const char *s = git_blob_rawcontent(blob); 599 600 len = git_blob_rawsize(blob); 601 fputs("<pre id=\"blob\">\n", fp); 602 603 if (len > 0) { 604 for (i = 0, prev = 0; i < len; i++) { 605 if (s[i] != '\n') 606 continue; 607 n++; 608 fprintf(fp, nfmt, n, n, n); 609 xmlencodeline(fp, &s[prev], i - prev + 1); 610 putc('\n', fp); 611 prev = i + 1; 612 } 613 /* trailing data */ 614 if ((len - prev) > 0) { 615 n++; 616 fprintf(fp, nfmt, n, n, n); 617 xmlencodeline(fp, &s[prev], len - prev); 618 } 619 } 620 621 fputs("</pre>\n", fp); 622 623 return n; 624 } 625 626 void 627 printcommit(FILE *fp, struct commitinfo *ci) 628 { 629 fprintf(fp, "<b>commit</b> <a href=\"%scommit/%s.html\">%s</a>\n", 630 relpath, ci->oid, ci->oid); 631 632 if (ci->parentoid[0]) 633 fprintf(fp, "<b>parent</b> <a href=\"%scommit/%s.html\">%s</a>\n", 634 relpath, ci->parentoid, ci->parentoid); 635 636 if (ci->author) { 637 fputs("<b>Author:</b> ", fp); 638 xmlencode(fp, ci->author->name, strlen(ci->author->name)); 639 fputs(" <<a href=\"mailto:", fp); 640 xmlencode(fp, ci->author->email, strlen(ci->author->email)); /* not percent-encoded */ 641 fputs("\">", fp); 642 xmlencode(fp, ci->author->email, strlen(ci->author->email)); 643 fputs("</a>>\n<b>Date:</b> ", fp); 644 printtime(fp, &(ci->author->when)); 645 putc('\n', fp); 646 } 647 if (ci->msg) { 648 putc('\n', fp); 649 xmlencode(fp, ci->msg, strlen(ci->msg)); 650 putc('\n', fp); 651 } 652 } 653 654 void 655 printshowfile(FILE *fp, struct commitinfo *ci) 656 { 657 const git_diff_delta *delta; 658 const git_diff_hunk *hunk; 659 const git_diff_line *line; 660 git_patch *patch; 661 size_t nhunks, nhunklines, changed, add, del, total, i, j, k; 662 char linestr[80]; 663 int c; 664 665 printcommit(fp, ci); 666 667 if (!ci->deltas) 668 return; 669 670 if (ci->filecount > 1000 || 671 ci->ndeltas > 1000 || 672 ci->addcount > 100000 || 673 ci->delcount > 100000) { 674 fputs("Diff is too large, output suppressed.\n", fp); 675 return; 676 } 677 678 /* diff stat */ 679 fputs("<b>Diffstat:</b>\n<table>", fp); 680 for (i = 0; i < ci->ndeltas; i++) { 681 delta = git_patch_get_delta(ci->deltas[i]->patch); 682 683 switch (delta->status) { 684 case GIT_DELTA_ADDED: c = 'A'; break; 685 case GIT_DELTA_COPIED: c = 'C'; break; 686 case GIT_DELTA_DELETED: c = 'D'; break; 687 case GIT_DELTA_MODIFIED: c = 'M'; break; 688 case GIT_DELTA_RENAMED: c = 'R'; break; 689 case GIT_DELTA_TYPECHANGE: c = 'T'; break; 690 default: c = ' '; break; 691 } 692 if (c == ' ') 693 fprintf(fp, "<tr><td>%c", c); 694 else 695 fprintf(fp, "<tr><td class=\"%c\">%c", c, c); 696 697 fprintf(fp, "</td><td><a href=\"#h%zu\">", i); 698 xmlencode(fp, delta->old_file.path, strlen(delta->old_file.path)); 699 if (strcmp(delta->old_file.path, delta->new_file.path)) { 700 fputs(" -> ", fp); 701 xmlencode(fp, delta->new_file.path, strlen(delta->new_file.path)); 702 } 703 704 add = ci->deltas[i]->addcount; 705 del = ci->deltas[i]->delcount; 706 changed = add + del; 707 total = sizeof(linestr) - 2; 708 if (changed > total) { 709 if (add) 710 add = ((float)total / changed * add) + 1; 711 if (del) 712 del = ((float)total / changed * del) + 1; 713 } 714 memset(&linestr, '+', add); 715 memset(&linestr[add], '-', del); 716 717 fprintf(fp, "</a></td><td> | </td><td class=\"num\">%zu</td><td><span class=\"i\">", 718 ci->deltas[i]->addcount + ci->deltas[i]->delcount); 719 fwrite(&linestr, 1, add, fp); 720 fputs("</span><span class=\"d\">", fp); 721 fwrite(&linestr[add], 1, del, fp); 722 fputs("</span></td></tr>\n", fp); 723 } 724 fprintf(fp, "</table></pre><pre id=\"commit-diff\">%zu file%s changed, %zu insertion%s(+), %zu deletion%s(-)\n", 725 ci->filecount, ci->filecount == 1 ? "" : "s", 726 ci->addcount, ci->addcount == 1 ? "" : "s", 727 ci->delcount, ci->delcount == 1 ? "" : "s"); 728 729 for (i = 0; i < ci->ndeltas; i++) { 730 patch = ci->deltas[i]->patch; 731 delta = git_patch_get_delta(patch); 732 fprintf(fp, "<b>diff --git a/<a id=\"h%zu\" href=\"%sfile/", i, relpath); 733 percentencode(fp, delta->old_file.path, strlen(delta->old_file.path)); 734 fputs(".html\">", fp); 735 xmlencode(fp, delta->old_file.path, strlen(delta->old_file.path)); 736 fprintf(fp, "</a> b/<a href=\"%sfile/", relpath); 737 percentencode(fp, delta->new_file.path, strlen(delta->new_file.path)); 738 fprintf(fp, ".html\">"); 739 xmlencode(fp, delta->new_file.path, strlen(delta->new_file.path)); 740 fprintf(fp, "</a></b>\n"); 741 742 /* check binary data */ 743 if (delta->flags & GIT_DIFF_FLAG_BINARY) { 744 fputs("Binary files differ.\n", fp); 745 continue; 746 } 747 748 nhunks = git_patch_num_hunks(patch); 749 for (j = 0; j < nhunks; j++) { 750 if (git_patch_get_hunk(&hunk, &nhunklines, patch, j)) 751 break; 752 753 fprintf(fp, "<a href=\"#h%zu-%zu\" id=\"h%zu-%zu\" class=\"h\">", i, j, i, j); 754 xmlencode(fp, hunk->header, hunk->header_len); 755 fputs("</a>", fp); 756 757 for (k = 0; ; k++) { 758 if (git_patch_get_line_in_hunk(&line, patch, j, k)) 759 break; 760 if (line->old_lineno == -1) 761 fprintf(fp, "<a href=\"#h%zu-%zu-%zu\" id=\"h%zu-%zu-%zu\" class=\"i\">+", 762 i, j, k, i, j, k); 763 else if (line->new_lineno == -1) 764 fprintf(fp, "<a href=\"#h%zu-%zu-%zu\" id=\"h%zu-%zu-%zu\" class=\"d\">-", 765 i, j, k, i, j, k); 766 else 767 putc(' ', fp); 768 xmlencodeline(fp, line->content, line->content_len); 769 putc('\n', fp); 770 if (line->old_lineno == -1 || line->new_lineno == -1) 771 fputs("</a>", fp); 772 } 773 } 774 } 775 } 776 777 void 778 writelogline(FILE *fp, struct commitinfo *ci) 779 { 780 /* make entire table row clickable */ 781 fprintf(fp, "<tr id=\"entry\" onclick=\"window.location.href=\'commit/%s.html'\">", 782 ci->oid); 783 784 fputs("<td id=\"log-date\">", fp); 785 fprintf(fp, "<a href=\"%scommit/%s.html\">", relpath, ci->oid); 786 if (ci->author) 787 printtimeshort(fp, &(ci->author->when)); 788 fputs("</a></td>",fp); 789 790 fputs("<td id=\"log-summary\">", fp); 791 if (ci->summary) { 792 fprintf(fp, "<a href=\"%scommit/%s.html\">", relpath, ci->oid); 793 xmlencode(fp, ci->summary, strlen(ci->summary)); 794 fputs("</a>",fp); 795 } 796 fputs("</td>", fp); 797 798 fputs("<td id=\"log-author\">", fp); 799 if (ci->author) { 800 fprintf(fp, "<a href=\"%scommit/%s.html\">", relpath, ci->oid); 801 xmlencode(fp, ci->author->name, strlen(ci->author->name)); 802 fputs("</a>",fp); 803 } 804 fputs("</td>", fp); 805 806 fputs("<td id=\"log-files\" class=\"num\" align=\"right\">", fp); 807 fprintf(fp, "<a href=\"%scommit/%s.html\">", relpath, ci->oid); 808 fprintf(fp, "%zu", ci->filecount); 809 fputs("</a></td>",fp); 810 811 fputs("<td id=\"log-files\" class=\"num\" align=\"right\">", fp); 812 fprintf(fp, "<a href=\"%scommit/%s.html\">", relpath, ci->oid); 813 fprintf(fp, "+%zu", ci->addcount); 814 fputs("</a></td>",fp); 815 816 fputs("<td id=\"log-files\" class=\"num\" align=\"right\">", fp); 817 fprintf(fp, "<a href=\"%scommit/%s.html\">", relpath, ci->oid); 818 fprintf(fp, "-%zu", ci->delcount); 819 fputs("</a></td></tr>\n", fp); 820 } 821 822 int 823 writelog(FILE *fp, const git_oid *oid) 824 { 825 struct commitinfo *ci; 826 git_revwalk *w = NULL; 827 git_oid id; 828 char path[PATH_MAX], oidstr[GIT_OID_HEXSZ + 1]; 829 FILE *fpfile; 830 size_t remcommits = 0; 831 int r; 832 833 git_revwalk_new(&w, repo); 834 git_revwalk_push(w, oid); 835 836 while (!git_revwalk_next(&id, w)) { 837 relpath = ""; 838 839 if (cachefile && !memcmp(&id, &lastoid, sizeof(id))) 840 break; 841 842 git_oid_tostr(oidstr, sizeof(oidstr), &id); 843 r = snprintf(path, sizeof(path), "commit/%s.html", oidstr); 844 if (r < 0 || (size_t)r >= sizeof(path)) 845 errx(1, "path truncated: 'commit/%s.html'", oidstr); 846 r = access(path, F_OK); 847 848 /* optimization: if there are no log lines to write and 849 the commit file already exists: skip the diffstat */ 850 if (!nlogcommits) { 851 remcommits++; 852 if (!r) 853 continue; 854 } 855 856 if (!(ci = commitinfo_getbyoid(&id))) 857 break; 858 /* diffstat: for stagit HTML required for the log.html line */ 859 if (commitinfo_getstats(ci) == -1) 860 goto err; 861 862 if (nlogcommits != 0) { 863 writelogline(fp, ci); 864 if (nlogcommits > 0) 865 nlogcommits--; 866 } 867 868 if (cachefile) 869 writelogline(wcachefp, ci); 870 871 /* check if file exists if so skip it */ 872 if (r) { 873 relpath = "../"; 874 fpfile = efopen(path, "w"); 875 writeheader(fpfile, ci->summary); 876 fputs("<div id=\"content\">\n", fpfile); 877 fputs("<pre id=\"commit-summary\">", fpfile); 878 printshowfile(fpfile, ci); 879 printf("%s\n", ci->oid); 880 fputs("</pre>\n", fpfile); 881 writefooter(fpfile); 882 checkfileerror(fpfile, path, 'w'); 883 fclose(fpfile); 884 } 885 err: 886 commitinfo_free(ci); 887 } 888 git_revwalk_free(w); 889 890 if (nlogcommits == 0 && remcommits != 0) { 891 fprintf(fp, "<tr><td></td><td colspan=\"5\">" 892 "%zu more commits remaining, fetch the repository" 893 "</td></tr>\n", remcommits); 894 } 895 896 relpath = ""; 897 898 return 0; 899 } 900 901 void 902 printcommitatom(FILE *fp, struct commitinfo *ci, const char *tag) 903 { 904 fputs("<entry>\n", fp); 905 906 fprintf(fp, "<id>%s</id>\n", ci->oid); 907 if (ci->author) { 908 fputs("<published>", fp); 909 printtimez(fp, &(ci->author->when)); 910 fputs("</published>\n", fp); 911 } 912 if (ci->committer) { 913 fputs("<updated>", fp); 914 printtimez(fp, &(ci->committer->when)); 915 fputs("</updated>\n", fp); 916 } 917 if (ci->summary) { 918 fputs("<title type=\"text\">", fp); 919 if (tag && tag[0]) { 920 fputs("[", fp); 921 xmlencode(fp, tag, strlen(tag)); 922 fputs("] ", fp); 923 } 924 xmlencode(fp, ci->summary, strlen(ci->summary)); 925 fputs("</title>\n", fp); 926 } 927 fprintf(fp, "<link rel=\"alternate\" type=\"text/html\" href=\"%scommit/%s.html\" />\n", 928 baseurl, ci->oid); 929 930 if (ci->author) { 931 fputs("<author>\n<name>", fp); 932 xmlencode(fp, ci->author->name, strlen(ci->author->name)); 933 fputs("</name>\n<email>", fp); 934 xmlencode(fp, ci->author->email, strlen(ci->author->email)); 935 fputs("</email>\n</author>\n", fp); 936 } 937 938 fputs("<content type=\"text\">", fp); 939 fprintf(fp, "commit %s\n", ci->oid); 940 if (ci->parentoid[0]) 941 fprintf(fp, "parent %s\n", ci->parentoid); 942 if (ci->author) { 943 fputs("Author: ", fp); 944 xmlencode(fp, ci->author->name, strlen(ci->author->name)); 945 fputs(" <", fp); 946 xmlencode(fp, ci->author->email, strlen(ci->author->email)); 947 fputs(">\nDate: ", fp); 948 printtime(fp, &(ci->author->when)); 949 putc('\n', fp); 950 } 951 if (ci->msg) { 952 putc('\n', fp); 953 xmlencode(fp, ci->msg, strlen(ci->msg)); 954 } 955 fputs("\n</content>\n</entry>\n", fp); 956 } 957 958 int 959 writeatom(FILE *fp, int all) 960 { 961 struct referenceinfo *ris = NULL; 962 size_t refcount = 0; 963 struct commitinfo *ci; 964 git_revwalk *w = NULL; 965 git_oid id; 966 size_t i, m = 100; /* last 'm' commits */ 967 968 fputs("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" 969 "<feed xmlns=\"http://www.w3.org/2005/Atom\">\n<title>", fp); 970 xmlencode(fp, strippedname, strlen(strippedname)); 971 fputs(", branch HEAD</title>\n<subtitle>", fp); 972 xmlencode(fp, description, strlen(description)); 973 fputs("</subtitle>\n", fp); 974 975 /* all commits or only tags? */ 976 if (all) { 977 git_revwalk_new(&w, repo); 978 git_revwalk_push_head(w); 979 for (i = 0; i < m && !git_revwalk_next(&id, w); i++) { 980 if (!(ci = commitinfo_getbyoid(&id))) 981 break; 982 printcommitatom(fp, ci, ""); 983 commitinfo_free(ci); 984 } 985 git_revwalk_free(w); 986 } else if (getrefs(&ris, &refcount) != -1) { 987 /* references: tags */ 988 for (i = 0; i < refcount; i++) { 989 if (git_reference_is_tag(ris[i].ref)) 990 printcommitatom(fp, ris[i].ci, 991 git_reference_shorthand(ris[i].ref)); 992 993 commitinfo_free(ris[i].ci); 994 git_reference_free(ris[i].ref); 995 } 996 free(ris); 997 } 998 999 fputs("</feed>\n", fp); 1000 1001 return 0; 1002 } 1003 1004 void 1005 writeblobraw(const git_blob *blob, const char *fpath, const char *filename, git_off_t filesize) 1006 { 1007 char tmp[PATH_MAX] = ""; 1008 const char *p; 1009 size_t lc = 0; 1010 FILE *fp; 1011 1012 mkdirfile(fpath); 1013 1014 if (strlcpy(tmp, fpath, sizeof(tmp)) >= sizeof(tmp)) 1015 errx(1, "path truncated: '%s'", fpath); 1016 1017 for (p = fpath, tmp[0] = '\0'; *p; p++) { 1018 if (*p == '/' && strlcat(tmp, "../", sizeof(tmp)) >= sizeof(tmp)) 1019 errx(1, "path truncated: '../%s'", tmp); 1020 } 1021 1022 fp = efopen(fpath, "w"); 1023 fwrite(git_blob_rawcontent(blob), (size_t)git_blob_rawsize(blob), 1, fp); 1024 fclose(fp); 1025 } 1026 1027 void 1028 process_output_md(const char* text, unsigned int size, void* fp) 1029 { 1030 fprintf((FILE *)fp, "%.*s", size, text); 1031 } 1032 1033 int 1034 ismarkdownfile(const char *name) 1035 { 1036 const char *exts[] = { ".md", ".markdown" }; 1037 const char *dot; 1038 size_t i, len; 1039 1040 if (!name) 1041 return 0; 1042 dot = strrchr(name, '.'); 1043 if (!dot) 1044 return 0; 1045 for (i = 0; i < LEN(exts); i++) { 1046 len = strlen(exts[i]); 1047 if (!strncasecmp(dot, exts[i], len)) 1048 return 1; 1049 } 1050 return 0; 1051 } 1052 1053 void 1054 writeblobmd(FILE *fp, const git_blob *blob, const char *rawprefix) 1055 { 1056 const char *s = git_blob_rawcontent(blob); 1057 git_off_t len = git_blob_rawsize(blob); 1058 1059 fputs("<div id=\"md-content\">", fp); 1060 if (md_html(s, len, process_output_md, fp, 1061 MD_FLAG_TABLES | 1062 MD_FLAG_TASKLISTS | 1063 MD_FLAG_PERMISSIVEEMAILAUTOLINKS | 1064 MD_FLAG_PERMISSIVEURLAUTOLINKS, 0)) 1065 err(1, "error parsing markdown"); 1066 fputs("</div>\n", fp); 1067 fputs("<script>" 1068 "document.querySelectorAll('#md-content img').forEach(img => {" 1069 "const src = img.getAttribute('src');" 1070 "if (src && !src.startsWith('/') && !src.startsWith('http'))" 1071 "img.src = '", fp); 1072 fputs(rawprefix, fp); 1073 fputs("raw/' + src;" 1074 "});" 1075 "</script>\n", fp); 1076 } 1077 1078 size_t 1079 writeblob(git_object *obj, const char *fpath, const char *rpath, const char *filename, const char *path, size_t filesize) 1080 { 1081 char tmp[PATH_MAX] = "", *file_parent; 1082 const char *p, *oldrelpath; 1083 int lc = 0; 1084 FILE *fp; 1085 1086 mkdirfile(fpath); 1087 1088 if (strlcpy(tmp, fpath, sizeof(tmp)) >= sizeof(tmp)) 1089 errx(1, "path truncated: '%s'", fpath); 1090 1091 file_parent = strrchr(tmp, '/'); 1092 for (p = fpath, tmp[0] = '\0'; *p; p++) { 1093 if (*p == '/' && strlcat(tmp, "../", sizeof(tmp)) >= sizeof(tmp)) 1094 errx(1, "path truncated: '../%s'", tmp); 1095 } 1096 1097 oldrelpath = relpath; 1098 relpath = tmp; 1099 1100 if (file_parent == NULL) 1101 file_parent = "files"; 1102 else { 1103 *file_parent = '\0'; 1104 file_parent = strrchr(tmp, '/'); 1105 if (file_parent == NULL) 1106 file_parent = tmp; 1107 else 1108 ++file_parent; 1109 } 1110 1111 fp = efopen(fpath, "w"); 1112 writeheader(fp, filename); 1113 /* fputs("<hr>\n", fp); */ 1114 fputs("<div id=\"content\">\n", fp); 1115 fputs("<script>" 1116 "function toggleLineNumbers() {" 1117 " var lines = document.querySelectorAll('.line');" 1118 " var lineCheckbox = document.getElementById('line-checkbox');" 1119 1120 " lines.forEach(function(element) {" 1121 " if (lineCheckbox.checked) {" 1122 " element.style.display = 'inline';" 1123 " } else {" 1124 " element.style.display = 'none';" 1125 " }" 1126 " });" 1127 " localStorage.setItem('line-checkbox', lineCheckbox.checked);" 1128 "}" 1129 "function setCheckboxState() {" 1130 " var lineCheckbox = document.getElementById('line-checkbox');" 1131 " var savedState = localStorage.getItem('line-checkbox');" 1132 " if (savedState !== null) {" 1133 " lineCheckbox.checked = savedState === 'true';" 1134 " toggleLineNumbers();" 1135 " }" 1136 "}" 1137 "document.addEventListener('DOMContentLoaded', setCheckboxState);" 1138 "</script>", fp); 1139 1140 fputs("<div id=\"open-file-header\"><div id=\"open-file-name\"><a id=\"file-parent-path\" href=\"", fp); 1141 // xmlencode(fp, relpath, strlen(relpath)); 1142 // printf("relpath is '%s'\n", relpath); 1143 fputs(relpath, fp); 1144 fputs("file/", fp); 1145 xmlencode(fp, path, strlen(path)); 1146 fputs(".html\">", fp); 1147 xmlencode(fp, path, strlen(path)); 1148 fprintf(fp, "</a>%s", strlen(path) == 0 ? "" : "/"); 1149 xmlencode(fp, filename, strlen(filename)); 1150 fprintf(fp, " (%zuB)</div>\n\n", filesize); 1151 1152 fputs("<div id=\"line-checkbox-div\"><input type=\"checkbox\"" 1153 "id=\"line-checkbox\" onchange=\"toggleLineNumbers()\" checked>", fp); 1154 fputs("<label for=\"line-checkbox\">Line numbers</label></div>", fp); 1155 1156 if (ismarkdownfile(filename)) { 1157 fputs("<div id=\"md-checkbox-div\"><input type=\"checkbox\"" 1158 "id=\"md-checkbox\" onchange=\"toggleMdView()\" checked>", fp); 1159 fputs("<label for=\"md-checkbox\">Render</label></div>", fp); 1160 } 1161 1162 // fputs("<p id=\"openfile-name\"> ", fp); 1163 // xmlencode(fp, filename, strlen(filename)); 1164 // fprintf(fp, " (%zuB)", filesize); 1165 1166 fprintf(fp, "<div id=\"file-raw\"><a href=\"%s%s\">raw</a></div></div>", relpath, rpath); 1167 /* fputs("<hr>\n", fp); */ 1168 1169 if (git_blob_is_binary((git_blob *)obj)) 1170 fputs("<p id=\"binary-file\">Binary file.</p>\n", fp); 1171 else if (ismarkdownfile(filename)) { 1172 fputs("<div id=\"md-view\">", fp); 1173 writeblobmd(fp, (git_blob *)obj, relpath); 1174 fputs("</div>\n", fp); 1175 fputs("<div id=\"source-view\">", fp); 1176 lc = writeblobhtml(fp, (git_blob *)obj); 1177 fputs("</div>\n", fp); 1178 fputs("<script>" 1179 "function toggleMdView() {" 1180 " var mdCheckbox = document.getElementById('md-checkbox');" 1181 " var mdView = document.getElementById('md-view');" 1182 " var sourceView = document.getElementById('source-view');" 1183 " var lineCheckboxDiv = document.getElementById('line-checkbox-div');" 1184 " if (mdView && sourceView) {" 1185 " mdView.style.display = mdCheckbox.checked ? 'block' : 'none';" 1186 " sourceView.style.display = mdCheckbox.checked ? 'none' : 'block';" 1187 " }" 1188 " if (lineCheckboxDiv)" 1189 " lineCheckboxDiv.style.display = mdCheckbox.checked ? 'none' : 'block';" 1190 " localStorage.setItem('md-checkbox', mdCheckbox.checked);" 1191 "}" 1192 "document.addEventListener('DOMContentLoaded', function() {" 1193 " var mdCheckbox = document.getElementById('md-checkbox');" 1194 " var savedState = localStorage.getItem('md-checkbox');" 1195 " if (savedState !== null) {" 1196 " mdCheckbox.checked = savedState === 'true';" 1197 " }" 1198 " toggleMdView();" 1199 "});" 1200 "</script>", fp); 1201 } else 1202 lc = writeblobhtml(fp, (git_blob *)obj); 1203 1204 writefooter(fp); 1205 checkfileerror(fp, fpath, 'w'); 1206 fclose(fp); 1207 1208 relpath = oldrelpath; 1209 1210 return lc; 1211 } 1212 1213 const char * 1214 filemode(git_filemode_t m) 1215 { 1216 static char mode[11]; 1217 1218 memset(mode, '-', sizeof(mode) - 1); 1219 mode[10] = '\0'; 1220 1221 if (S_ISREG(m)) 1222 mode[0] = '-'; 1223 else if (S_ISBLK(m)) 1224 mode[0] = 'b'; 1225 else if (S_ISCHR(m)) 1226 mode[0] = 'c'; 1227 else if (S_ISDIR(m)) 1228 mode[0] = 'd'; 1229 else if (S_ISFIFO(m)) 1230 mode[0] = 'p'; 1231 else if (S_ISLNK(m)) 1232 mode[0] = 'l'; 1233 else if (S_ISSOCK(m)) 1234 mode[0] = 's'; 1235 else 1236 mode[0] = '?'; 1237 1238 if (m & S_IRUSR) mode[1] = 'r'; 1239 if (m & S_IWUSR) mode[2] = 'w'; 1240 if (m & S_IXUSR) mode[3] = 'x'; 1241 if (m & S_IRGRP) mode[4] = 'r'; 1242 if (m & S_IWGRP) mode[5] = 'w'; 1243 if (m & S_IXGRP) mode[6] = 'x'; 1244 if (m & S_IROTH) mode[7] = 'r'; 1245 if (m & S_IWOTH) mode[8] = 'w'; 1246 if (m & S_IXOTH) mode[9] = 'x'; 1247 1248 if (m & S_ISUID) mode[3] = (mode[3] == 'x') ? 's' : 'S'; 1249 if (m & S_ISGID) mode[6] = (mode[6] == 'x') ? 's' : 'S'; 1250 if (m & S_ISVTX) mode[9] = (mode[9] == 'x') ? 't' : 'T'; 1251 1252 return mode; 1253 } 1254 1255 int 1256 writefilestree(FILE *fp, git_tree *tree, const char *path) 1257 { 1258 const git_tree_entry *entry = NULL; 1259 git_object *obj = NULL; 1260 FILE *fp_subtree; 1261 const char *entryname, *oldrelpath; 1262 char filepath[PATH_MAX], rawpath[PATH_MAX], entrypath[PATH_MAX], tmp[PATH_MAX], tmp2[PATH_MAX], oid[8]; 1263 char* parent; 1264 size_t count, i, lc, filesize; 1265 int r, rf, ret, is_obj_tree; 1266 1267 if (strlen(path) > 0) { 1268 fputs("<h2 id=\"dir-title\">Directory: ", fp); 1269 xmlencode(fp, path, strlen(path)); 1270 fputs("</h2>\n\n", fp); 1271 1272 fputs("<table id=\"dir-files\"><thead id=\"legends\">\n<tr>" 1273 "<td id=\"file-mode\"><b>Mode</b></td><td><b>Name</b></td>" 1274 "<td id=\"file-size\" class=\"num\" align=\"right\"><b>Size</b></td>" 1275 "</tr>\n</thead><tbody>\n", fp); 1276 } else { 1277 fputs("<table id=\"files\"><thead id=\"legends\">\n<tr>" 1278 "<td id=\"file-mode\"><b>Mode</b></td><td><b>Name</b></td>" 1279 "<td id=\"file-size\" class=\"num\" align=\"right\"><b>Size</b></td>" 1280 "</tr>\n</thead><tbody>\n", fp); 1281 } 1282 1283 if (strlen(path) > 0) { 1284 if (strlcpy(tmp, path, sizeof(tmp)) >= sizeof(tmp)) 1285 errx(1, "path truncated: '%s'", path); 1286 parent = strrchr(tmp, '/'); 1287 if (parent == NULL) 1288 parent = "files"; 1289 else { 1290 *parent = '\0'; 1291 parent = strrchr(tmp, '/'); 1292 if (parent == NULL) 1293 parent = tmp; 1294 else 1295 ++parent; 1296 } 1297 1298 fprintf(fp, "<tr id=\"entry\" onclick=\"window.location.href=\'../"); 1299 percentencode(fp, parent, strlen(parent)); 1300 fputs(".html\'\">", fp); 1301 1302 fputs("<td id=\"file-mode\"><a href=\"../", fp); 1303 xmlencode(fp, parent, strlen(parent)); 1304 fputs(".html\">d---------</a></td>", fp); 1305 1306 fputs("<td id=\"dir-name\"><a href=\"../", fp); 1307 xmlencode(fp, parent, strlen(parent)); 1308 fputs(".html\">..</a></td>", fp); 1309 1310 fputs("<td id=\"dir-size\"><a href=\"../", fp); 1311 xmlencode(fp, parent, strlen(parent)); 1312 fputs(".html\">.</a></td></tr>\n", fp); 1313 } 1314 1315 count = git_tree_entrycount(tree); 1316 1317 /* print directories first if any */ 1318 for (i = 0; i < count; i++) { 1319 if (!(entry = git_tree_entry_byindex(tree, i)) || 1320 !(entryname = git_tree_entry_name(entry))) 1321 return -1; 1322 1323 joinpath(entrypath, sizeof(entrypath), path, entryname); 1324 1325 r = snprintf(filepath, sizeof(filepath), "file/%s.html", 1326 entrypath); 1327 if (r < 0 || (size_t)r >= sizeof(filepath)) 1328 errx(1, "path truncated: 'file/%s.html'", entrypath); 1329 rf = snprintf(rawpath, sizeof(rawpath), "raw/%s", 1330 entrypath); 1331 if (rf < 0 || (size_t)rf >= sizeof(rawpath)) 1332 errx(1, "path truncated: 'raw/%s'", entrypath); 1333 1334 if (!git_tree_entry_to_object(&obj, repo, entry)) { 1335 if (git_object_type(obj) == GIT_OBJ_TREE) { 1336 mkdirfile(filepath); 1337 1338 if (strlcpy(tmp, relpath, sizeof(tmp)) >= sizeof(tmp)) 1339 errx(1, "path truncated: '%s'", relpath); 1340 if (strlcat(tmp, "../", sizeof(tmp)) >= sizeof(tmp)) 1341 errx(1, "path truncated: '../%s'", tmp); 1342 1343 oldrelpath = relpath; 1344 relpath = tmp; 1345 fp_subtree = efopen(filepath, "w"); 1346 strlcpy(tmp2, "Files - ", sizeof(tmp2)); 1347 if (strlcat(tmp2, entrypath, sizeof(tmp2)) >= sizeof(tmp2)) 1348 errx(1, "path truncated: '%s'", tmp2); 1349 writeheader(fp_subtree, tmp2); 1350 /* fputs("<hr/>\n", fp_subtree); */ 1351 fputs("<div id=\"content\">\n", fp_subtree); 1352 1353 /* NOTE: recurses */ 1354 ret = writefilestree(fp_subtree, (git_tree *)obj, 1355 entrypath); 1356 writefooter(fp_subtree); 1357 relpath = oldrelpath; 1358 lc = 0; 1359 is_obj_tree = 1; 1360 if (ret) 1361 return ret; 1362 1363 /* make entire table row clickable */ 1364 fprintf(fp, "<tr id=\"entry\" onclick=\"window.location.href=\'%s", 1365 relpath); 1366 1367 percentencode(fp, filepath, strlen(filepath)); 1368 fputs("\'\"><td id=\"file-mode\">", fp); 1369 1370 fprintf(fp, "<a href=\"%s", relpath); 1371 percentencode(fp, filepath, strlen(filepath)); 1372 fputs("\">",fp); 1373 fputs(filemode(git_tree_entry_filemode(entry)), fp); 1374 fputs("</a></td>", fp); 1375 1376 if (git_object_type(obj) == GIT_OBJ_TREE) 1377 fprintf(fp, "<td id=\"dir-name\"><a href=\"%s", relpath); 1378 else 1379 fprintf(fp, "<td id=\"file-name\"><a href=\"%s", relpath); 1380 1381 percentencode(fp, filepath, strlen(filepath)); 1382 fputs("\">", fp); 1383 xmlencode(fp, entryname, strlen(entryname)); 1384 fputs("</a></td>", fp); 1385 1386 if (lc > 0) { 1387 fputs("<td id=\"file-size\" class=\"num\">", fp); 1388 fprintf(fp, "<a href=\"%s", relpath); 1389 percentencode(fp, filepath, strlen(filepath)); 1390 fputs("\">", fp); 1391 fprintf(fp, "%zuL", lc); 1392 } 1393 else if (!is_obj_tree) { 1394 fputs("<td id=\"file-size\" class=\"num\">", fp); 1395 fprintf(fp, "<a href=\"%s", relpath); 1396 percentencode(fp, filepath, strlen(filepath)); 1397 fputs("\">", fp); 1398 fprintf(fp, "%zuB", filesize); 1399 } 1400 else if (is_obj_tree) { 1401 fputs("<td id=\"dir-size\">", fp); 1402 fprintf(fp, "<a href=\"%s", relpath); 1403 percentencode(fp, filepath, strlen(filepath)); 1404 fputs("\">.", fp); 1405 } 1406 1407 fputs("</a></td></tr>\n", fp); 1408 git_object_free(obj); 1409 } 1410 } 1411 } 1412 1413 /* print all files skipping directories */ 1414 for (i = 0; i < count; i++) { 1415 if (!(entry = git_tree_entry_byindex(tree, i)) || 1416 !(entryname = git_tree_entry_name(entry))) 1417 return -1; 1418 joinpath(entrypath, sizeof(entrypath), path, entryname); 1419 1420 r = snprintf(filepath, sizeof(filepath), "file/%s.html", 1421 entrypath); 1422 if (r < 0 || (size_t)r >= sizeof(filepath)) 1423 errx(1, "path truncated: 'file/%s.html'", entrypath); 1424 rf = snprintf(rawpath, sizeof(rawpath), "raw/%s", 1425 entrypath); 1426 if (rf < 0 || (size_t)rf >= sizeof(rawpath)) 1427 errx(1, "path truncated: 'raw/%s'", entrypath); 1428 1429 if (!git_tree_entry_to_object(&obj, repo, entry)) { 1430 switch (git_object_type(obj)) { 1431 case GIT_OBJ_BLOB: 1432 is_obj_tree = 0; 1433 filesize = git_blob_rawsize((git_blob *)obj); 1434 lc = writeblob(obj, filepath, rawpath, entryname, path, filesize); 1435 writeblobraw((git_blob *)obj, rawpath, entryname, filesize); 1436 break; 1437 case GIT_OBJ_TREE: 1438 continue; 1439 default: 1440 git_object_free(obj); 1441 continue; 1442 } 1443 1444 /* make entire table row clickable */ 1445 fprintf(fp, "<tr id=\"entry\" onclick=\"window.location.href=\'%s", 1446 relpath); 1447 percentencode(fp, filepath, strlen(filepath)); 1448 fputs("\'\"><td id=\"file-mode\">", fp); 1449 1450 fprintf(fp, "<a href=\"%s", relpath); 1451 percentencode(fp, filepath, strlen(filepath)); 1452 fputs("\">",fp); 1453 fputs(filemode(git_tree_entry_filemode(entry)), fp); 1454 fputs("</a></td>", fp); 1455 1456 if (git_object_type(obj) == GIT_OBJ_TREE) 1457 fprintf(fp, "<td id=\"dir-name\"><a href=\"%s", relpath); 1458 else 1459 fprintf(fp, "<td id=\"file-name\"><a href=\"%s", relpath); 1460 1461 percentencode(fp, filepath, strlen(filepath)); 1462 fputs("\">", fp); 1463 1464 xmlencode(fp, entryname, strlen(entryname)); 1465 1466 fputs("</a></td>", fp); 1467 1468 if (lc > 0) { 1469 fputs("<td id=\"file-size\" class=\"num\">", fp); 1470 fprintf(fp, "<a href=\"%s", relpath); 1471 percentencode(fp, filepath, strlen(filepath)); 1472 fputs("\">", fp); 1473 fprintf(fp, "%zuL", lc); 1474 } 1475 else if (!is_obj_tree) { 1476 fputs("<td id=\"file-size\" class=\"num\">", fp); 1477 fprintf(fp, "<a href=\"%s", relpath); 1478 percentencode(fp, filepath, strlen(filepath)); 1479 fputs("\">", fp); 1480 fprintf(fp, "%zuB", filesize); 1481 } 1482 else if (is_obj_tree) { 1483 fputs("<td id=\"dir-size\">", fp); 1484 fprintf(fp, "<a href=\"%s", relpath); 1485 percentencode(fp, filepath, strlen(filepath)); 1486 fputs("\">.", fp); 1487 } 1488 1489 fputs("</a></td></tr>\n", fp); 1490 git_object_free(obj); 1491 1492 } else if (git_tree_entry_type(entry) == GIT_OBJ_COMMIT) { 1493 /* commit object in tree is a submodule */ 1494 fputs("<tr><td id=\"file-mode\">m---------</td>", fp); 1495 fprintf(fp, "<td><a href=\"%sfile/.gitmodules.html\">", relpath); 1496 xmlencode(fp, entrypath, strlen(entrypath)); 1497 fputs("</a> @ ", fp); 1498 git_oid_tostr(oid, sizeof(oid), git_tree_entry_id(entry)); 1499 xmlencode(fp, oid, strlen(oid)); 1500 fputs("</td><td class=\"num\" align=\"right\"></td></tr>\n", fp); 1501 } 1502 } 1503 1504 fputs("</tbody></table>", fp); 1505 return 0; 1506 } 1507 1508 int 1509 writefiles(FILE *fp, const git_oid *id) 1510 { 1511 git_tree *tree = NULL; 1512 git_commit *commit = NULL; 1513 int ret = -1; 1514 1515 if (!git_commit_lookup(&commit, repo, id) && 1516 !git_commit_tree(&tree, commit)) 1517 ret = writefilestree(fp, tree, ""); 1518 1519 git_commit_free(commit); 1520 git_tree_free(tree); 1521 1522 return ret; 1523 } 1524 1525 int 1526 writerefs(FILE *fp) 1527 { 1528 struct referenceinfo *ris = NULL; 1529 struct commitinfo *ci; 1530 size_t count, i, j, refcount; 1531 const char *titles[] = { "Branches", "Tags" }; 1532 const char *ids[] = { "branches", "tags" }; 1533 const char *s; 1534 1535 if (getrefs(&ris, &refcount) == -1) 1536 return -1; 1537 1538 for (i = 0, j = 0, count = 0; i < refcount; i++) { 1539 if (j == 0 && git_reference_is_tag(ris[i].ref)) { 1540 if (count) 1541 fputs("</tbody></table><br/>\n", fp); 1542 count = 0; 1543 j = 1; 1544 } 1545 1546 /* print header if it has an entry (first). */ 1547 if (++count == 1) { 1548 fprintf(fp, "<h2>%s</h2><table id=\"%s\">" 1549 "<thead id=\"legends\">\n<tr><td><b>Name</b></td>" 1550 "<td><b>Last commit date</b></td>" 1551 "<td><b>Author</b></td>\n</tr>\n" 1552 "</thead><tbody>\n", 1553 titles[j], ids[j]); 1554 } 1555 1556 ci = ris[i].ci; 1557 s = git_reference_shorthand(ris[i].ref); 1558 1559 fputs("<tr><td>", fp); 1560 xmlencode(fp, s, strlen(s)); 1561 fputs("</td><td>", fp); 1562 if (ci->author) 1563 printtimeshort(fp, &(ci->author->when)); 1564 fputs("</td><td>", fp); 1565 if (ci->author) 1566 xmlencode(fp, ci->author->name, strlen(ci->author->name)); 1567 fputs("</td></tr>\n", fp); 1568 } 1569 /* table footer */ 1570 if (count) 1571 fputs("</tbody></table><br/>\n", fp); 1572 1573 for (i = 0; i < refcount; i++) { 1574 commitinfo_free(ris[i].ci); 1575 git_reference_free(ris[i].ref); 1576 } 1577 free(ris); 1578 1579 return 0; 1580 } 1581 1582 void 1583 usage(char *argv0) 1584 { 1585 fprintf(stderr, "usage: %s [-c cachefile | -l commits] " 1586 "[-u baseurl] repodir\n", argv0); 1587 exit(1); 1588 } 1589 1590 int 1591 main(int argc, char *argv[]) 1592 { 1593 git_object *obj = NULL; 1594 const git_oid *head = NULL; 1595 mode_t mask; 1596 FILE *fp, *fpread; 1597 char path[PATH_MAX], repodirabs[PATH_MAX + 1], *p; 1598 char tmppath[64] = "cache.XXXXXXXXXXXX", buf[BUFSIZ]; 1599 size_t n; 1600 int i, fd, r; 1601 1602 for (i = 1; i < argc; i++) { 1603 if (argv[i][0] != '-') { 1604 if (repodir) 1605 usage(argv[0]); 1606 repodir = argv[i]; 1607 } else if (argv[i][1] == 'c') { 1608 if (nlogcommits > 0 || i + 1 >= argc) 1609 usage(argv[0]); 1610 cachefile = argv[++i]; 1611 } else if (argv[i][1] == 'l') { 1612 if (cachefile || i + 1 >= argc) 1613 usage(argv[0]); 1614 errno = 0; 1615 nlogcommits = strtoll(argv[++i], &p, 10); 1616 if (argv[i][0] == '\0' || *p != '\0' || 1617 nlogcommits <= 0 || errno) 1618 usage(argv[0]); 1619 } else if (argv[i][1] == 'u') { 1620 if (i + 1 >= argc) 1621 usage(argv[0]); 1622 baseurl = argv[++i]; 1623 } 1624 } 1625 if (!repodir) 1626 usage(argv[0]); 1627 1628 if (!realpath(repodir, repodirabs)) 1629 err(1, "realpath"); 1630 1631 /* do not search outside the git repository: 1632 GIT_CONFIG_LEVEL_APP is the highest level currently */ 1633 git_libgit2_init(); 1634 for (i = 1; i <= GIT_CONFIG_LEVEL_APP; i++) 1635 git_libgit2_opts(GIT_OPT_SET_SEARCH_PATH, i, ""); 1636 /* do not require the git repository to be owned by the current user */ 1637 git_libgit2_opts(GIT_OPT_SET_OWNER_VALIDATION, 0); 1638 1639 #ifdef __OpenBSD__ 1640 if (unveil(repodir, "r") == -1) 1641 err(1, "unveil: %s", repodir); 1642 if (unveil(".", "rwc") == -1) 1643 err(1, "unveil: ."); 1644 if (cachefile && unveil(cachefile, "rwc") == -1) 1645 err(1, "unveil: %s", cachefile); 1646 1647 if (cachefile) { 1648 if (pledge("stdio rpath wpath cpath fattr", NULL) == -1) 1649 err(1, "pledge"); 1650 } else { 1651 if (pledge("stdio rpath wpath cpath", NULL) == -1) 1652 err(1, "pledge"); 1653 } 1654 #endif 1655 1656 if (git_repository_open_ext(&repo, repodir, 1657 GIT_REPOSITORY_OPEN_NO_SEARCH, NULL) < 0) { 1658 fprintf(stderr, "%s: cannot open repository\n", argv[0]); 1659 return 1; 1660 } 1661 1662 /* find HEAD */ 1663 if (!git_revparse_single(&obj, repo, "HEAD")) 1664 head = git_object_id(obj); 1665 git_object_free(obj); 1666 1667 /* use directory name as name */ 1668 if ((name = strrchr(repodirabs, '/'))) 1669 name++; 1670 else 1671 name = ""; 1672 1673 /* strip .git suffix */ 1674 if (!(strippedname = strdup(name))) 1675 err(1, "strdup"); 1676 if ((p = strrchr(strippedname, '.'))) 1677 if (!strcmp(p, ".git")) 1678 *p = '\0'; 1679 1680 printf("%s\n", strippedname); 1681 1682 /* read description or .git/description */ 1683 joinpath(path, sizeof(path), repodir, "description"); 1684 if (!(fpread = fopen(path, "r"))) { 1685 joinpath(path, sizeof(path), repodir, ".git/description"); 1686 fpread = fopen(path, "r"); 1687 } 1688 if (fpread) { 1689 if (!fgets(description, sizeof(description), fpread)) 1690 description[0] = '\0'; 1691 checkfileerror(fpread, path, 'r'); 1692 fclose(fpread); 1693 } 1694 1695 /* read url or .git/url */ 1696 joinpath(path, sizeof(path), repodir, "url"); 1697 if (!(fpread = fopen(path, "r"))) { 1698 joinpath(path, sizeof(path), repodir, ".git/url"); 1699 fpread = fopen(path, "r"); 1700 } 1701 if (fpread) { 1702 if (!fgets(cloneurl, sizeof(cloneurl), fpread)) 1703 cloneurl[0] = '\0'; 1704 checkfileerror(fpread, path, 'r'); 1705 fclose(fpread); 1706 cloneurl[strcspn(cloneurl, "\n")] = '\0'; 1707 } 1708 1709 /* check LICENSE */ 1710 for (i = 0; i < LEN(licensefiles) && !license; i++) { 1711 if (!git_revparse_single(&obj, repo, licensefiles[i]) && 1712 git_object_type(obj) == GIT_OBJ_BLOB) 1713 license = licensefiles[i] + strlen("HEAD:"); 1714 git_object_free(obj); 1715 } 1716 1717 /* check README */ 1718 for (i = 0; i < LEN(readmefiles) && !readme; i++) { 1719 if (!git_revparse_single(&obj, repo, readmefiles[i]) && 1720 git_object_type(obj) == GIT_OBJ_BLOB) 1721 readme = readmefiles[i] + strlen("HEAD:"); 1722 r = i; 1723 git_object_free(obj); 1724 } 1725 1726 if (!git_revparse_single(&obj, repo, "HEAD:.gitmodules") && 1727 git_object_type(obj) == GIT_OBJ_BLOB) 1728 submodules = ".gitmodules"; 1729 git_object_free(obj); 1730 1731 /* about page */ 1732 if (readme) { 1733 fp = efopen("readme.html", "w"); 1734 writeheader(fp, "README"); 1735 1736 git_revparse_single(&obj, repo, readmefiles[r]); 1737 const char *s = git_blob_rawcontent((git_blob *)obj); 1738 if (r == 1) { 1739 writeblobmd(fp, (git_blob *)obj, ""); 1740 } else { 1741 fputs("<pre id=\"readme\">", fp); 1742 xmlencode(fp, s, strlen(s)); 1743 fputs("</pre>\n", fp); 1744 } 1745 git_object_free(obj); 1746 writefooter(fp); 1747 fclose(fp); 1748 } 1749 1750 /* log for HEAD */ 1751 fp = efopen("log.html", "w"); 1752 relpath = ""; 1753 mkdir("commit", S_IRWXU | S_IRWXG | S_IRWXO); 1754 writeheader(fp, "Commits"); 1755 fputs("<div id=\"content\">\n", fp); 1756 fputs("<table id=\"log\"><thead id=\"legends\">\n<tr>" 1757 "<td id=\"log-date\"><b>Date</b></td>" 1758 "<td id=\"log-summary\"><b>Commit message</b></td>" 1759 "<td id=\"log-author\"><b>Author</b></td>" 1760 "<td id=\"log-files\" lass=\"num\" align=\"right\"><b>Files</b></td>" 1761 "<td id=\"log-files\" lass=\"num\" align=\"right\"><b>+</b></td>" 1762 "<td id=\"log-files\" lass=\"num\" align=\"right\"><b>-</b></td></tr>\n</thead><tbody>\n", fp); 1763 1764 if (cachefile && head) { 1765 /* read from cache file (does not need to exist) */ 1766 if ((rcachefp = fopen(cachefile, "r"))) { 1767 if (!fgets(lastoidstr, sizeof(lastoidstr), rcachefp)) 1768 errx(1, "%s: no object id", cachefile); 1769 if (git_oid_fromstr(&lastoid, lastoidstr)) 1770 errx(1, "%s: invalid object id", cachefile); 1771 } 1772 1773 /* write log to (temporary) cache */ 1774 if ((fd = mkstemp(tmppath)) == -1) 1775 err(1, "mkstemp"); 1776 if (!(wcachefp = fdopen(fd, "w"))) 1777 err(1, "fdopen: '%s'", tmppath); 1778 /* write last commit id (HEAD) */ 1779 git_oid_tostr(buf, sizeof(buf), head); 1780 fprintf(wcachefp, "%s\n", buf); 1781 1782 writelog(fp, head); 1783 1784 if (rcachefp) { 1785 /* append previous log to log.html and the new cache */ 1786 while (!feof(rcachefp)) { 1787 n = fread(buf, 1, sizeof(buf), rcachefp); 1788 if (ferror(rcachefp)) 1789 break; 1790 if (fwrite(buf, 1, n, fp) != n || 1791 fwrite(buf, 1, n, wcachefp) != n) 1792 break; 1793 } 1794 checkfileerror(rcachefp, cachefile, 'r'); 1795 fclose(rcachefp); 1796 } 1797 checkfileerror(wcachefp, tmppath, 'w'); 1798 fclose(wcachefp); 1799 } else { 1800 if (head) 1801 writelog(fp, head); 1802 } 1803 1804 fputs("</tbody></table>", fp); 1805 writefooter(fp); 1806 checkfileerror(fp, "log.html", 'w'); 1807 fclose(fp); 1808 1809 /* files for HEAD */ 1810 fp = efopen("files.html", "w"); 1811 writeheader(fp, "Files"); 1812 if (head) 1813 writefiles(fp, head); 1814 if (readme) { 1815 git_revparse_single(&obj, repo, readmefiles[r]); 1816 const char *s = git_blob_rawcontent((git_blob *)obj); 1817 if (r == 1) { 1818 writeblobmd(fp, (git_blob *)obj, ""); 1819 } else { 1820 fputs("<pre id=\"readme\">", fp); 1821 xmlencode(fp, s, strlen(s)); 1822 fputs("</pre>\n", fp); 1823 } 1824 git_object_free(obj); 1825 } 1826 writefooter(fp); 1827 checkfileerror(fp, "files.html", 'w'); 1828 fclose(fp); 1829 1830 /* summary page with branches and tags */ 1831 fp = efopen("refs.html", "w"); 1832 writeheader(fp, "Refs"); 1833 fputs("<div id=\"content\">\n<div id=\"refs\">\n", fp); 1834 writerefs(fp); 1835 writefooter(fp); 1836 checkfileerror(fp, "refs.html", 'w'); 1837 fclose(fp); 1838 1839 /* Atom feed */ 1840 fp = efopen("atom.xml", "w"); 1841 writeatom(fp, 1); 1842 checkfileerror(fp, "atom.xml", 'w'); 1843 fclose(fp); 1844 1845 /* Atom feed for tags / releases */ 1846 fp = efopen("tags.xml", "w"); 1847 writeatom(fp, 0); 1848 checkfileerror(fp, "tags.xml", 'w'); 1849 fclose(fp); 1850 1851 /* rename new cache file on success */ 1852 if (cachefile && head) { 1853 if (rename(tmppath, cachefile)) 1854 err(1, "rename: '%s' to '%s'", tmppath, cachefile); 1855 umask((mask = umask(0))); 1856 if (chmod(cachefile, 1857 (S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH) & ~mask)) 1858 err(1, "chmod: '%s'", cachefile); 1859 } 1860 1861 /* cleanup */ 1862 git_repository_free(repo); 1863 git_libgit2_shutdown(); 1864 1865 return 0; 1866 }
