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