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