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