stagit-gopher

[fork] gopher git frontend
Log | Files | Refs | README | LICENSE

commit 8968f8cf7ac514463c703e863e953881094ea941
parent b7568c323867513fa17a181ef8b077a8d0bc3057
Author: hhvn <dev@hhvn.uk>
Date:   Sat, 22 Jan 2022 11:50:10 +0000

Merge changes from upstream

Diffstat:
MLICENSE | 2+-
Mstagit-gopher-index.1 | 7++++++-
Mstagit-gopher-index.c | 67+++++++++++++++++++++++++++++++++++++++++++++++++------------------
Mstagit-gopher.1 | 22++++++++++++++++++++--
Mstagit-gopher.c | 184++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
5 files changed, 188 insertions(+), 94 deletions(-)

diff --git a/LICENSE b/LICENSE @@ -1,6 +1,6 @@ MIT/X Consortium License -(c) 2015-2019 Hiltjo Posthuma <hiltjo@codemadness.org> +(c) 2015-2022 Hiltjo Posthuma <hiltjo@codemadness.org> (c) 2015-2016 Dimitris Papastamos <sin@2f30.org> (c) 2020-2021 Hayden Hamilton <dev@hhvn.uk> diff --git a/stagit-gopher-index.1 b/stagit-gopher-index.1 @@ -1,4 +1,4 @@ -.Dd Jun 23, 2017 +.Dd August 2, 2021 .Dt STAGIT-GOPHER-INDEX 1 .Os .Sh NAME @@ -32,6 +32,11 @@ The content of the follow files specifies the meta data for each repository: .It .git/description or description (bare repos). description .El +.Sh EXAMPLES +.Bd -literal +cd gphroot +stagit-gopher-index path/to/gitrepo1 path/to/gitrepo2 > index.gph +.Ed .Sh SEE ALSO .Xr stagit-gopher 1 .Sh AUTHORS diff --git a/stagit-gopher-index.c b/stagit-gopher-index.c @@ -10,6 +10,9 @@ #include <git2.h> +#define PAD_TRUNCATE_SYMBOL "\xe2\x80\xa6" /* symbol: "ellipsis" */ +#define UTF_INVALID_SYMBOL "\xef\xbf\xbd" /* symbol: "replacement" */ + static git_repository *repo; static const char *relpath = ""; @@ -17,36 +20,61 @@ static const char *relpath = ""; static char description[255] = "Repositories"; static char *name = ""; -/* format `len' columns of characters. If string is shorter pad the rest +/* Format `len' columns of characters. If string is shorter pad the rest * with characters `pad`. */ int utf8pad(char *buf, size_t bufsiz, const char *s, size_t len, int pad) { - wchar_t w; + wchar_t wc; size_t col = 0, i, slen, siz = 0; - int rl, wc; + int inc, rl, w; - if (!len) + if (!bufsiz) return -1; + if (!len) { + buf[0] = '\0'; + return 0; + } slen = strlen(s); - for (i = 0; i < slen && col < len + 1; i += rl) { - if ((rl = mbtowc(&w, &s[i], slen - i < 4 ? slen - i : 4)) <= 0) + for (i = 0; i < slen; i += inc) { + inc = 1; /* next byte */ + if ((unsigned char)s[i] < 32) + continue; + + rl = mbtowc(&wc, &s[i], slen - i < 4 ? slen - i : 4); + inc = rl; + if (rl < 0) { + mbtowc(NULL, NULL, 0); /* reset state */ + inc = 1; /* invalid, seek next byte */ + w = 1; /* replacement char is one width */ + } else if ((w = wcwidth(wc)) == -1) { + continue; + } + + if (col + w > len || (col + w == len && s[i + inc])) { + if (siz + 4 >= bufsiz) + return -1; + memcpy(&buf[siz], PAD_TRUNCATE_SYMBOL, sizeof(PAD_TRUNCATE_SYMBOL) - 1); + siz += sizeof(PAD_TRUNCATE_SYMBOL) - 1; + buf[siz] = '\0'; + col++; break; - if ((wc = wcwidth(w)) == -1) - wc = 1; - col += wc; - if (col >= len && s[i + rl]) { + } else if (rl < 0) { if (siz + 4 >= bufsiz) return -1; - memcpy(&buf[siz], "\xe2\x80\xa6", 4); - return 0; + memcpy(&buf[siz], UTF_INVALID_SYMBOL, sizeof(UTF_INVALID_SYMBOL) - 1); + siz += sizeof(UTF_INVALID_SYMBOL) - 1; + buf[siz] = '\0'; + col++; + continue; } - if (siz + rl + 1 >= bufsiz) + if (siz + inc + 1 >= bufsiz) return -1; - memcpy(&buf[siz], &s[i], rl); - siz += rl; + memcpy(&buf[siz], &s[i], inc); + siz += inc; buf[siz] = '\0'; + col += w; } len -= col; @@ -75,7 +103,7 @@ gphtext(FILE *fp, const char *s, size_t len) fputs(" ", fp); break; default: - fputc(*s, fp); + putc(*s, fp); break; } } @@ -99,7 +127,7 @@ gphlink(FILE *fp, const char *s, size_t len) fputs("\\|", fp); break; default: - fputc(*s, fp); + putc(*s, fp); break; } } @@ -158,7 +186,6 @@ writelog(FILE *fp) git_revwalk_new(&w, repo); git_revwalk_push_head(w); - git_revwalk_simplify_first_parent(w); if (git_revwalk_next(&id, w) || git_commit_lookup(&commit, repo, &id)) { @@ -211,7 +238,11 @@ main(int argc, char *argv[]) setlocale(LC_CTYPE, ""); + /* do not search outside the git repository: + GIT_CONFIG_LEVEL_APP is the highest level currently */ git_libgit2_init(); + for (i = 1; i <= GIT_CONFIG_LEVEL_APP; i++) + git_libgit2_opts(GIT_OPT_SET_SEARCH_PATH, i, ""); #ifdef __OpenBSD__ if (pledge("stdio rpath", NULL) == -1) diff --git a/stagit-gopher.1 b/stagit-gopher.1 @@ -1,4 +1,4 @@ -.Dd July 19, 2020 +.Dd August 2, 2021 .Dt STAGIT-GOPHER 1 .Os .Sh NAME @@ -9,6 +9,7 @@ .Op Fl b Ar baseprefix .Op Fl c Ar cachefile .Op Fl l Ar commits +.Op Fl u Ar baseurl .Ar repodir .Sh DESCRIPTION .Nm @@ -35,6 +36,11 @@ Write a maximum number of .Ar commits to the log.gph file only. However the commit files are written as usual. +.It Fl u Ar baseurl +Base URL to make links in the Atom feeds absolute. +Does not use the prefix from the -b option. +It should include the gopher type. +For example: "gopher://codemadness.org/1/git/stagit-gopher/". .El .Pp The options @@ -90,11 +96,23 @@ The content of the follow files specifies the metadata for each repository: .It .git/description or description (bare repo). description .It .git/url or url (bare repo). -primary clone url of the repository, for example: git://git.2f30.org/stagit +primary clone URL of the repository, for example: +git://git.codemadness.org/stagit .El .Pp When a README or LICENSE file exists in HEAD or a .gitmodules submodules file exists in HEAD a direct link in the index is made. +.Sh EXIT STATUS +.Ex -std +.Sh EXAMPLES +.Bd -literal +mkdir -p gphroot/gphrepo1 && cd gphroot/gphrepo1 +stagit-gopher path/to/gitrepo1 +# repeat for other repositories. +.Ed +.Pp +To update the gph files when the repository is changed a git post-receive hook +can be used, see the file example_post-receive.sh for an example. .Sh SEE ALSO .Xr stagit-gopher-index 1 .Sh AUTHORS diff --git a/stagit-gopher.c b/stagit-gopher.c @@ -18,6 +18,10 @@ #include "compat.h" +#define LEN(s) (sizeof(s)/sizeof(*s)) +#define PAD_TRUNCATE_SYMBOL "\xe2\x80\xa6" /* symbol: "ellipsis" */ +#define UTF_INVALID_SYMBOL "\xef\xbf\xbd" /* symbol: "replacement" */ + struct deltainfo { git_patch *patch; @@ -58,6 +62,7 @@ struct referenceinfo { static git_repository *repo; +static const char *baseurl = ""; /* base URL to make absolute RSS/Atom URI */ static const char *relpath = ""; static const char *repodir; @@ -70,7 +75,7 @@ static char *licensefiles[] = { "HEAD:LICENSE", "HEAD:LICENSE.md", "HEAD:COPYING static char *license; static char *readmefiles[] = { "HEAD:README", "HEAD:README.md" }; static char *readme; -static long long nlogcommits = -1; /* < 0 indicates not used */ +static long long nlogcommits = -1; /* -1 indicates not used */ /* cache */ static git_oid lastoid; @@ -78,40 +83,61 @@ static char lastoidstr[GIT_OID_HEXSZ + 2]; /* id + newline + NUL byte */ static FILE *rcachefp, *wcachefp; static const char *cachefile; -/* format `len' columns of characters. If string is shorter pad the rest +/* Format `len' columns of characters. If string is shorter pad the rest * with characters `pad`. */ int utf8pad(char *buf, size_t bufsiz, const char *s, size_t len, int pad) { wchar_t wc; size_t col = 0, i, slen, siz = 0; - int rl, w; + int inc, rl, w; - if (!len) + if (!bufsiz) return -1; + if (!len) { + buf[0] = '\0'; + return 0; + } slen = strlen(s); - for (i = 0; i < slen; i += rl) { - if ((rl = mbtowc(&wc, &s[i], slen - i < 4 ? slen - i : 4)) <= 0) - break; - if ((w = wcwidth(wc)) == -1) + for (i = 0; i < slen; i += inc) { + inc = 1; /* next byte */ + if ((unsigned char)s[i] < 32) continue; - if (col + w > len || (col + w == len && s[i + rl])) { + + rl = mbtowc(&wc, &s[i], slen - i < 4 ? slen - i : 4); + inc = rl; + if (rl < 0) { + mbtowc(NULL, NULL, 0); /* reset state */ + inc = 1; /* invalid, seek next byte */ + w = 1; /* replacement char is one width */ + } else if ((w = wcwidth(wc)) == -1) { + continue; + } + + if (col + w > len || (col + w == len && s[i + inc])) { if (siz + 4 >= bufsiz) return -1; - memcpy(&buf[siz], "\xe2\x80\xa6", 3); - siz += 3; - if (col + w == len && w > 1) - buf[siz++] = pad; + memcpy(&buf[siz], PAD_TRUNCATE_SYMBOL, sizeof(PAD_TRUNCATE_SYMBOL) - 1); + siz += sizeof(PAD_TRUNCATE_SYMBOL) - 1; buf[siz] = '\0'; - return 0; + col++; + break; + } else if (rl < 0) { + if (siz + 4 >= bufsiz) + return -1; + memcpy(&buf[siz], UTF_INVALID_SYMBOL, sizeof(UTF_INVALID_SYMBOL) - 1); + siz += sizeof(UTF_INVALID_SYMBOL) - 1; + buf[siz] = '\0'; + col++; + continue; } - if (siz + rl + 1 >= bufsiz) + if (siz + inc + 1 >= bufsiz) return -1; - memcpy(&buf[siz], &s[i], rl); - col += w; - siz += rl; + memcpy(&buf[siz], &s[i], inc); + siz += inc; buf[siz] = '\0'; + col += w; } len -= col; @@ -297,8 +323,7 @@ err: int refs_cmp(const void *v1, const void *v2) { - struct referenceinfo *r1 = (struct referenceinfo *)v1; - struct referenceinfo *r2 = (struct referenceinfo *)v2; + const struct referenceinfo *r1 = v1, *r2 = v2; time_t t1, t2; int r; @@ -393,12 +418,12 @@ err: } FILE * -efopen(const char *name, const char *flags) +efopen(const char *filename, const char *flags) { FILE *fp; - if (!(fp = fopen(name, flags))) - err(1, "fopen: '%s'", name); + if (!(fp = fopen(filename, flags))) + err(1, "fopen: '%s'", filename); return fp; } @@ -416,7 +441,7 @@ xmlencode(FILE *fp, const char *s, size_t len) case '\'': fputs("&#39;", fp); break; case '&': fputs("&amp;", fp); break; case '"': fputs("&quot;", fp); break; - default: fputc(*s, fp); + default: putc(*s, fp); } } } @@ -430,12 +455,12 @@ gphtextnl(FILE *fp, const char *s, size_t len) for (i = 0; s[i] && i < len; i++) { /* escape with 't' at the start of a line */ if (!n && (s[i] == 't' || s[i] == '[')) - fputc('t', fp); + putc('t', fp); switch (s[i]) { case '\t': fputs(" ", fp); case '\r': break; - default: fputc(s[i], fp); + default: putc(s[i], fp); } n = (s[i] != '\n'); } @@ -457,7 +482,7 @@ gphtext(FILE *fp, const char *s, size_t len) fputs(" ", fp); break; default: - fputc(*s, fp); + putc(*s, fp); break; } } @@ -481,7 +506,7 @@ gphlink(FILE *fp, const char *s, size_t len) fputs("\\|", fp); break; default: - fputc(*s, fp); + putc(*s, fp); break; } } @@ -557,7 +582,7 @@ printtimeshort(FILE *fp, const git_time *intime) void writeheader(FILE *fp, const char *title) { - fputc('t', fp); + putc('t', fp); gphtext(fp, title, strlen(title)); if (title[0] && strippedname[0]) fputs(" - ", fp); @@ -593,16 +618,16 @@ writefooter(FILE *fp) { } -int +size_t writeblobgph(FILE *fp, const git_blob *blob) { - size_t n = 0, i, j, prev; - const char *nfmt = "%6d "; + size_t n = 0, i, j, len, prev; + const char *nfmt = "%6zu "; const char *s = git_blob_rawcontent(blob); - git_off_t len = git_blob_rawsize(blob); + len = git_blob_rawsize(blob); if (len > 0) { - for (i = 0, prev = 0; i < (size_t)len; i++) { + for (i = 0, prev = 0; i < len; i++) { if (s[i] != '\n') continue; n++; @@ -611,7 +636,7 @@ writeblobgph(FILE *fp, const git_blob *blob) switch (s[j]) { case '\r': break; case '\t': fputs(" ", fp); break; - default: fputc(s[j], fp); + default: putc(s[j], fp); } } prev = i + 1; @@ -624,7 +649,7 @@ writeblobgph(FILE *fp, const git_blob *blob) switch (s[j]) { case '\r': break; case '\t': fputs(" ", fp); break; - default: fputc(s[j], fp); + default: putc(s[j], fp); } } } @@ -653,12 +678,12 @@ printcommit(FILE *fp, struct commitinfo *ci) fputs("|server|port]\n", fp); fputs("Date: ", fp); printtime(fp, &(ci->author->when)); - fputc('\n', fp); + putc('\n', fp); } if (ci->msg) { - fputc('\n', fp); + putc('\n', fp); gphtextnl(fp, ci->msg, strlen(ci->msg)); - fputc('\n', fp); + putc('\n', fp); } } @@ -760,9 +785,9 @@ printshowfile(FILE *fp, struct commitinfo *ci) if (git_patch_get_hunk(&hunk, &nhunklines, patch, j)) break; - fputc('t', fp); + putc('t', fp); gphtext(fp, hunk->header, hunk->header_len); - fputc('\n', fp); + putc('\n', fp); for (k = 0; ; k++) { if (git_patch_get_line_in_hunk(&line, patch, j, k)) @@ -774,7 +799,7 @@ printshowfile(FILE *fp, struct commitinfo *ci) else fputs(" ", fp); gphtext(fp, line->content, line->content_len); - fputc('\n', fp); + putc('\n', fp); } } } @@ -807,11 +832,11 @@ writelog(FILE *fp, const git_oid *oid) git_oid id; char path[PATH_MAX], oidstr[GIT_OID_HEXSZ + 1]; FILE *fpfile; + size_t remcommits = 0; int r; git_revwalk_new(&w, repo); git_revwalk_push(w, oid); - git_revwalk_simplify_first_parent(w); while (!git_revwalk_next(&id, w)) { if (cachefile && !memcmp(&id, &lastoid, sizeof(id))) @@ -825,19 +850,19 @@ writelog(FILE *fp, const git_oid *oid) /* optimization: if there are no log lines to write and the commit file already exists: skip the diffstat */ - if (!nlogcommits && !r) - continue; + if (!nlogcommits) { + remcommits++; + if (!r) + continue; + } if (!(ci = commitinfo_getbyoid(&id))) break; - if (nlogcommits < 0) { - writelogline(fp, ci); - } else if (nlogcommits > 0) { + if (nlogcommits != 0) { writelogline(fp, ci); - nlogcommits--; - if (!nlogcommits && ci->parentoid[0]) - fprintf(fp, "%16.16s More commits remaining [...]\n", ""); + if (nlogcommits > 0) + nlogcommits--; } if (cachefile) @@ -860,6 +885,12 @@ err: } git_revwalk_free(w); + if (nlogcommits == 0 && remcommits != 0) { + fprintf(fp, "%16.16s " + "%zu more commits remaining, fetch the repository\n", + "", remcommits); + } + return 0; } @@ -889,8 +920,8 @@ printcommitatom(FILE *fp, struct commitinfo *ci, const char *tag) xmlencode(fp, ci->summary, strlen(ci->summary)); fputs("</title>\n", fp); } - fprintf(fp, "<link rel=\"alternate\" type=\"text/html\" href=\"commit/%s.gph\" />\n", - ci->oid); + fprintf(fp, "<link rel=\"alternate\" href=\"%scommit/%s.gph\" />\n", + baseurl, ci->oid); if (ci->author) { fputs("<author>\n<name>", fp); @@ -911,10 +942,10 @@ printcommitatom(FILE *fp, struct commitinfo *ci, const char *tag) xmlencode(fp, ci->author->email, strlen(ci->author->email)); fputs("&gt;\nDate: ", fp); printtime(fp, &(ci->author->when)); - fputc('\n', fp); + putc('\n', fp); } if (ci->msg) { - fputc('\n', fp); + putc('\n', fp); xmlencode(fp, ci->msg, strlen(ci->msg)); } fputs("\n</content>\n</entry>\n", fp); @@ -941,7 +972,6 @@ writeatom(FILE *fp, int all) if (all) { git_revwalk_new(&w, repo); git_revwalk_push_head(w); - git_revwalk_simplify_first_parent(w); for (i = 0; i < m && !git_revwalk_next(&id, w); i++) { if (!(ci = commitinfo_getbyoid(&id))) break; @@ -967,11 +997,11 @@ writeatom(FILE *fp, int all) return 0; } -int -writeblob(git_object *obj, const char *fpath, const char *filename, git_off_t filesize) +size_t +writeblob(git_object *obj, const char *fpath, const char *filename, size_t filesize) { char tmp[PATH_MAX] = "", *d; - int lc = 0; + size_t lc = 0; FILE *fp; if (strlcpy(tmp, fpath, sizeof(tmp)) >= sizeof(tmp)) @@ -983,9 +1013,9 @@ writeblob(git_object *obj, const char *fpath, const char *filename, git_off_t fi fp = efopen(fpath, "w"); writeheader(fp, filename); - fputc('t', fp); + putc('t', fp); gphtext(fp, filename, strlen(filename)); - fprintf(fp, " (%juB)\n", (uintmax_t)filesize); + fprintf(fp, " (%zuB)\n", filesize); fputs("---\n", fp); if (git_blob_is_binary((git_blob *)obj)) { @@ -1048,11 +1078,10 @@ writefilestree(FILE *fp, git_tree *tree, const char *path) { const git_tree_entry *entry = NULL; git_object *obj = NULL; - git_off_t filesize; const char *entryname; - char buf[256], filepath[PATH_MAX], entrypath[PATH_MAX]; - size_t count, i; - int lc, r, ret; + char buf[256], filepath[PATH_MAX], entrypath[PATH_MAX], oid[8]; + size_t count, i, lc, filesize; + int r, ret; count = git_tree_entrycount(tree); for (i = 0; i < count; i++) { @@ -1093,9 +1122,9 @@ writefilestree(FILE *fp, git_tree *tree, const char *path) gphlink(fp, buf, strlen(buf)); fputs(" ", fp); if (lc > 0) - fprintf(fp, "%7dL", lc); + fprintf(fp, "%7zuL", lc); else - fprintf(fp, "%7juB", (uintmax_t)filesize); + fprintf(fp, "%7zuB", filesize); fprintf(fp, "|%s/", relpath); gphlink(fp, filepath, strlen(filepath)); fputs("|server|port]\n", fp); @@ -1103,8 +1132,10 @@ writefilestree(FILE *fp, git_tree *tree, const char *path) } else if (git_tree_entry_type(entry) == GIT_OBJ_COMMIT) { /* commit object in tree is a submodule */ fputs("[1|m--------- ", fp); - utf8pad(buf, sizeof(buf), entrypath, 50, ' '); - gphlink(fp, buf, strlen(buf)); + gphlink(fp, entrypath, strlen(entrypath)); + fputs(" @ ", fp); + git_oid_tostr(oid, sizeof(oid), git_tree_entry_id(entry)); + gphlink(fp, oid, strlen(oid)); fprintf(fp, "|%s/file/.gitmodules.gph|server|port]\n", relpath); /* NOTE: linecount omitted */ } @@ -1198,7 +1229,8 @@ writerefs(FILE *fp) void usage(char *argv0) { - fprintf(stderr, "%s [-b baseprefix] [-c cachefile | -l commits] repodir\n", argv0); + fprintf(stderr, "%s [-b baseprefix] [-c cachefile | -l commits] " + "[-u baseurl] repodir\n", argv0); exit(1); } @@ -1237,6 +1269,10 @@ main(int argc, char *argv[]) if (argv[i][0] == '\0' || *p != '\0' || nlogcommits <= 0 || errno) usage(argv[0]); + } else if (argv[i][1] == 'u') { + if (i + 1 >= argc) + usage(argv[0]); + baseurl = argv[++i]; } } if (!repodir) @@ -1245,7 +1281,11 @@ main(int argc, char *argv[]) if (!realpath(repodir, repodirabs)) err(1, "realpath"); + /* do not search outside the git repository: + GIT_CONFIG_LEVEL_APP is the highest level currently */ git_libgit2_init(); + for (i = 1; i <= GIT_CONFIG_LEVEL_APP; i++) + git_libgit2_opts(GIT_OPT_SET_SEARCH_PATH, i, ""); #ifdef __OpenBSD__ if (unveil(repodir, "r") == -1) @@ -1314,7 +1354,7 @@ main(int argc, char *argv[]) } /* check LICENSE */ - for (i = 0; i < sizeof(licensefiles) / sizeof(*licensefiles) && !license; i++) { + for (i = 0; i < LEN(licensefiles) && !license; i++) { if (!git_revparse_single(&obj, repo, licensefiles[i]) && git_object_type(obj) == GIT_OBJ_BLOB) license = licensefiles[i] + strlen("HEAD:"); @@ -1322,7 +1362,7 @@ main(int argc, char *argv[]) } /* check README */ - for (i = 0; i < sizeof(readmefiles) / sizeof(*readmefiles) && !readme; i++) { + for (i = 0; i < LEN(readmefiles) && !readme; i++) { if (!git_revparse_single(&obj, repo, readmefiles[i]) && git_object_type(obj) == GIT_OBJ_BLOB) readme = readmefiles[i] + strlen("HEAD:");