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