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