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