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