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