stagit-gopher

static git page generator for gopher
git clone git://git.2f30.org/stagit-gopher.git
Log | Files | Refs | README | LICENSE

commit f2e11d669c0f5b2f75980d62d129728b44bba7b0
parent b6154c444d011845fbe68f6ed2cde1b29880d54d
Author: Hiltjo Posthuma <hiltjo@codemadness.org>
Date:   Thu Jun 15 19:39:29 +0200

rename this version stagit -> stagit-gopher

to avoid confusion with the original HTML version (stagit)

Diffstat:
Makefile | 27++++++++++++---------------
README | 20++++++++++----------
stagit-gopher-index.1 | 42++++++++++++++++++++++++++++++++++++++++++
stagit-gopher-index.c | 250+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
stagit-gopher.1 | 95+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
stagit-gopher.c | 1243+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
stagit-index.1 | 42------------------------------------------
stagit-index.c | 250-------------------------------------------------------------------------------
stagit.1 | 95-------------------------------------------------------------------------------
stagit.c | 1243-------------------------------------------------------------------------------
10 files changed, 1652 insertions(+), 1655 deletions(-)
diff --git a/Makefile b/Makefile @@ -1,19 +1,19 @@ include config.mk -NAME = stagit +NAME = stagit-gopher VERSION = 0.5 SRC = \ - stagit.c\ - stagit-index.c + stagit-gopher.c\ + stagit-gopher-index.c COMPATSRC = \ reallocarray.c\ strlcpy.c BIN = \ - stagit\ - stagit-index + stagit-gopher\ + stagit-gopher-index MAN1 = \ - stagit.1\ - stagit-index.1 + stagit-gopher.1\ + stagit-gopher-index.1 DOC = \ LICENSE\ README\ @@ -38,7 +38,7 @@ dist: rm -rf ${NAME}-${VERSION} mkdir -p ${NAME}-${VERSION} cp -f ${MAN1} ${HDR} ${SCRIPTS} ${SRC} ${COMPATSRC} ${DOC} \ - Makefile config.mk favicon.png logo.png style.css \ + Makefile config.mk \ example.sh \ ${NAME}-${VERSION} # make tarball @@ -48,11 +48,11 @@ dist: ${OBJ}: config.mk ${HDR} -stagit: stagit.o ${COMPATOBJ} - ${CC} -o $@ stagit.o ${COMPATOBJ} ${LDFLAGS} +stagit-gopher: stagit-gopher.o ${COMPATOBJ} + ${CC} -o $@ stagit-gopher.o ${COMPATOBJ} ${LDFLAGS} -stagit-index: stagit-index.o ${COMPATOBJ} - ${CC} -o $@ stagit-index.o ${COMPATOBJ} ${LDFLAGS} +stagit-gopher-index: stagit-gopher-index.o ${COMPATOBJ} + ${CC} -o $@ stagit-gopher-index.o ${COMPATOBJ} ${LDFLAGS} clean: rm -f ${BIN} ${OBJ} ${NAME}-${VERSION}.tar.gz @@ -77,9 +77,6 @@ uninstall: for f in ${BIN} ${SCRIPTS}; do rm -f ${DESTDIR}${PREFIX}/bin/$$f; done # removing example files. rm -f \ - ${DESTDIR}${PREFIX}/share/${NAME}/style.css\ - ${DESTDIR}${PREFIX}/share/${NAME}/favicon.png\ - ${DESTDIR}${PREFIX}/share/${NAME}/logo.png\ ${DESTDIR}${PREFIX}/share/${NAME}/example.sh\ ${DESTDIR}${PREFIX}/share/${NAME}/README -rmdir ${DESTDIR}${PREFIX}/share/${NAME} diff --git a/README b/README @@ -1,7 +1,7 @@ -stagit -====== +stagit-gopher +============= -static git page generator +static git page generator for gopher Usage @@ -9,12 +9,12 @@ Usage Make files per repository: - $ mkdir -p htmldir && cd htmldir - $ stagit path-to-repo + $ mkdir -p gphdir && cd gphdir + $ stagit-gopher path-to-repo Make index file for repositories: - $ stagit-index repodir1 repodir2 repodir3 > index.html + $ stagit-gopher-index repodir1 repodir2 repodir3 > index.gph Install @@ -36,7 +36,7 @@ Dependencies Documentation ------------- -See man pages: stagit(1) and stagit-index(1). +See man pages: stagit-gopher(1) and stagit-gopher-index(1). Building a static binary @@ -85,10 +85,10 @@ Features - Log and diffstat per commit. - Show file tree with linkable line numbers. - Show references: local branches and tags. -- Detect README and LICENSE file from HEAD and link it as a webpage. -- Detect submodules (.gitmodules file) from HEAD and link it as a webpage. +- Detect README and LICENSE file from HEAD and link it as a page. +- Detect submodules (.gitmodules file) from HEAD and link it as a page. - Atom feed log (atom.xml). -- Make index page for multiple repositories with stagit-index. +- Make index page for multiple repositories with stagit-gopher-index. - After generating the pages (relatively slow) serving the files is very fast, simple and requires little resources (because the content is static), only a HTTP file server is required. diff --git a/stagit-gopher-index.1 b/stagit-gopher-index.1 @@ -0,0 +1,42 @@ +.Dd December 26, 2015 +.Dt STAGIT-INDEX 1 +.Os +.Sh NAME +.Nm stagit-index +.Nd static git index page generator +.Sh SYNOPSIS +.Nm +.Op Ar repodir... +.Sh DESCRIPTION +.Nm +will create an index HTML page for the repositories specified and writes +the HTML data to stdout. +The repos in the index are in the same order as the arguments +.Ar repodir +specified. +.Pp +The basename of the directory is used as the repository name. +The suffix ".git" is removed from the basename, this suffix is commonly used +for "bare" repos. +.Pp +The content of the follow files specifies the meta data for each repository: +.Bl -tag -width Ds +.It .git/description or description (bare repos). +description +.It .git/owner or owner (bare repo). +owner of repository +.El +.Pp +For changing the style of the page you can use the following files: +.Bl -tag -width Ds +.It favicon.png +favicon image. +.It logo.png +32x32 logo. +.It style.css +CSS stylesheet. +.El +.Sh SEE ALSO +.Xr stagit 1 +.Sh AUTHORS +.An Hiltjo Posthuma Aq Mt hiltjo@codemadness.org diff --git a/stagit-gopher-index.c b/stagit-gopher-index.c @@ -0,0 +1,250 @@ +#include <sys/stat.h> + +#include <err.h> +#include <errno.h> +#include <inttypes.h> +#include <limits.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <wchar.h> + +#include <git2.h> + +#include "compat.h" + +static git_repository *repo; + +static const char *relpath = "/"; + +static char description[255] = "Repositories"; +static char *name = ""; + +#ifndef USE_PLEDGE +#define pledge(p1,p2) 0 +#endif + +#define ISUTF8(c) (((c) & 0xc0) != 0x80) + +/* print `len' columns of characters. If string is shorter pad the rest + * with characters `pad`. */ +void +printutf8pad(FILE *fp, const char *s, size_t len, int pad) +{ + wchar_t w; + size_t n = 0, i; + int r; + + for (i = 0; *s && n < len; i++, s++) { + if (ISUTF8(*s)) { + if ((r = mbtowc(&w, s, 4)) == -1) + break; + if ((r = wcwidth(w)) == -1) + r = 1; + n += (size_t)r; + } + putc(*s, fp); + } + for (; n < len; n++) + putc(pad, fp); +} + +void +trim(char *buf, size_t bufsiz, const char *src) +{ + size_t d = 0, i, len, s; + + len = strlen(src); + for (s = 0; s < len && d < bufsiz - 1; s++) { + switch (src[s]) { + case '\t': + if (d + 8 >= bufsiz - 1) + goto end; + for (i = 0; i < 8; i++) + buf[d++] = ' '; + break; + case '|': + case '\n': + case '\r': + buf[d++] = ' '; + break; + default: + buf[d++] = src[s]; + break; + } + } +end: + buf[d] = '\0'; +} + +void +joinpath(char *buf, size_t bufsiz, const char *path, const char *path2) +{ + int r; + + r = snprintf(buf, bufsiz, "%s%s%s", + path, path[0] && path[strlen(path) - 1] != '/' ? "/" : "", path2); + if (r == -1 || (size_t)r >= bufsiz) + errx(1, "path truncated: '%s%s%s'", + path, path[0] && path[strlen(path) - 1] != '/' ? "/" : "", path2); +} + +void +printtimeshort(FILE *fp, const git_time *intime) +{ + struct tm *intm; + time_t t; + char out[32]; + + t = (time_t)intime->time; + if (!(intm = gmtime(&t))) + return; + strftime(out, sizeof(out), "%Y-%m-%d %H:%M", intm); + fputs(out, fp); +} + +void +writeheader(FILE *fp) +{ + char buf[1024]; + + trim(buf, sizeof(buf), description); + if (buf[0] == 't') + fputc('t', fp); + fprintf(fp, "%s\n\n", buf); + + fprintf(fp, "%-20.20s ", "Name"); + fprintf(fp, "%-50.50s ", "Description"); + fprintf(fp, "%-16.16s\n", "Last commit"); +} + +void +writefooter(FILE *fp) +{ +} + +int +writelog(FILE *fp) +{ + git_commit *commit = NULL; + const git_signature *author; + git_revwalk *w = NULL; + git_oid id; + char *stripped_name = NULL, *p; + char buf[1024]; + int ret = 0; + + git_revwalk_new(&w, repo); + git_revwalk_push_head(w); + git_revwalk_sorting(w, GIT_SORT_TIME); + git_revwalk_simplify_first_parent(w); + + if (git_revwalk_next(&id, w) || + git_commit_lookup(&commit, repo, &id)) { + ret = -1; + goto err; + } + + author = git_commit_author(commit); + + /* strip .git suffix */ + if (!(stripped_name = strdup(name))) + err(1, "strdup"); + if ((p = strrchr(stripped_name, '.'))) + if (!strcmp(p, ".git")) + *p = '\0'; + + fputs("[1|", fp); + trim(buf, sizeof(buf), stripped_name); + printutf8pad(fp, buf, 20, ' '); + fputs(" ", fp); + trim(buf, sizeof(buf), description); + printutf8pad(fp, buf, 50, ' '); + fputs(" ", fp); + if (author) + printtimeshort(fp, &(author->when)); + trim(buf, sizeof(buf), stripped_name); + fprintf(fp, "|%s%s/log.gph|server|port]\n", relpath, buf); + + git_commit_free(commit); +err: + git_revwalk_free(w); + free(stripped_name); + + return ret; +} + +void +usage(const char *argv0) +{ + fprintf(stderr, "%s [repodir...]\n", argv0); + exit(1); +} + + +int +main(int argc, char *argv[]) +{ + const git_error *e = NULL; + FILE *fp; + char path[PATH_MAX], repodirabs[PATH_MAX + 1]; + const char *repodir = NULL; + int i, ret = 0; + + if (pledge("stdio rpath", NULL) == -1) + err(1, "pledge"); + + git_libgit2_init(); + + writeheader(stdout); + + for (i = 1; i < argc; i++) { + if (argv[i][0] == '-') { + if (argv[i][1] != 'b' || i + 1 >= argc) + usage(argv[0]); + relpath = argv[++i]; + continue; + } + + repodir = argv[i]; + if (!realpath(repodir, repodirabs)) + err(1, "realpath"); + + if (git_repository_open_ext(&repo, repodir, + GIT_REPOSITORY_OPEN_NO_SEARCH, NULL)) { + e = giterr_last(); + fprintf(stderr, "%s: %s\n", argv[0], e->message); + ret = 1; + continue; + } + + /* use directory name as name */ + if ((name = strrchr(repodirabs, '/'))) + name++; + else + name = ""; + + /* read description or .git/description */ + joinpath(path, sizeof(path), repodir, "description"); + if (!(fp = fopen(path, "r"))) { + joinpath(path, sizeof(path), repodir, ".git/description"); + fp = fopen(path, "r"); + } + description[0] = '\0'; + if (fp) { + if (!fgets(description, sizeof(description), fp)) + description[0] = '\0'; + fclose(fp); + } + + writelog(stdout); + } + writefooter(stdout); + + /* cleanup */ + git_repository_free(repo); + git_libgit2_shutdown(); + + return ret; +} diff --git a/stagit-gopher.1 b/stagit-gopher.1 @@ -0,0 +1,95 @@ +.Dd May 1, 2016 +.Dt STAGIT 1 +.Os +.Sh NAME +.Nm stagit +.Nd static git page generator +.Sh SYNOPSIS +.Nm +.Op Fl c Ar cachefile +.Ar repodir +.Sh DESCRIPTION +.Nm +writes HTML pages for the repository +.Ar repodir +to the current directory. +.Pp +The options are as follows: +.Bl -tag -width Ds +.It Fl c Ar cachefile +Cache the entries of the log page up to the point of +the last commit. +The +.Ar cachefile +will store the last commit id and the entries in the HTML table. +It is up to the user to make sure the state of the +.Ar cachefile +is in sync with the history of the repository. +.El +.Pp +The following files will be written: +.Bl -tag -width Ds +.It atom.xml +Atom XML feed +.It files.html +List of files in the latest tree, linking to the file. +.It log.html +List of commits in order of most recent to old of the commits (top to bottom), +each commit links to a page with a diffstat and diff of the commit. +.It refs.html +Lists references of the repository such as branches and tags. +.El +.Pp +For each entry in HEAD a file will be written in the format: +file/filepath.html. +This file will contain the textual data of the file prefixed by line numbers. +The file will have the string "Binary file" if the data is considered to be +non-textual. +.Pp +For each commit a file will be written in the format: +commit/commitid.html. +This file will contain the diffstat and diff of the commit. +It will write the string "Binary files differ" if the data is considered to +be non-textual. +Too large diffs will be suppressed and a string +"Diff is too large, output suppressed" will be written. +.Pp +When a commit HTML file exists it won't be overwritten again, note that if +you've changed +.Nm +or changed one of the metadata files of the repository it is recommended to +recreate all the output files because it will contain old data. +To do this remove the output directory and +.Ar cachefile , +then recreate the files. +.Pp +The basename of the directory is used as the repository name. +The suffix ".git" is removed from the basename, this suffix is commonly used +for "bare" repos. +.Pp +The content of the follow files specifies the metadata for each repository: +.Bl -tag -width Ds +.It .git/description or description (bare repo). +description +.It .git/owner or owner (bare repo). +owner of repository +.It .git/url or url (bare repo). +primary clone url of the repository, for example: git://git.2f30.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 menu is made. +.Pp +For changing the style of the page you can use the following files: +.Bl -tag -width Ds +.It favicon.png +favicon image. +.It logo.png +32x32 logo. +.It style.css +CSS stylesheet. +.El +.Sh SEE ALSO +.Xr stagit-index 1 +.Sh AUTHORS +.An Hiltjo Posthuma Aq Mt hiltjo@codemadness.org diff --git a/stagit-gopher.c b/stagit-gopher.c @@ -0,0 +1,1243 @@ +#include <sys/stat.h> + +#include <err.h> +#include <errno.h> +#include <inttypes.h> +#include <libgen.h> +#include <limits.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <wchar.h> + +#include <git2.h> + +#include "compat.h" + +struct deltainfo { + git_patch *patch; + + size_t addcount; + size_t delcount; +}; + +struct commitinfo { + const git_oid *id; + + char oid[GIT_OID_HEXSZ + 1]; + char parentoid[GIT_OID_HEXSZ + 1]; + + const git_signature *author; + const git_signature *committer; + const char *summary; + const char *msg; + + git_diff *diff; + git_commit *commit; + git_commit *parent; + git_tree *commit_tree; + git_tree *parent_tree; + + size_t addcount; + size_t delcount; + size_t filecount; + + struct deltainfo **deltas; + size_t ndeltas; +}; + +static git_repository *repo; + +static const char *relpath = ""; +static const char *repodir; + +static char *name = ""; +static char *strippedname = ""; +static char description[255]; +static char cloneurl[1024]; +static int haslicense, hasreadme, hassubmodules; + +/* cache */ +static git_oid lastoid; +static char lastoidstr[GIT_OID_HEXSZ + 2]; /* id + newline + nul byte */ +static FILE *rcachefp, *wcachefp; +static const char *cachefile; + +#ifndef USE_PLEDGE +#define pledge(p1,p2) 0 +#endif + +#define ISUTF8(c) (((c) & 0xc0) != 0x80) + +/* print `len' columns of characters. If string is shorter pad the rest + * with characters `pad`. */ +void +printutf8pad(FILE *fp, const char *s, size_t len, int pad) +{ + wchar_t w; + size_t n = 0, i; + int r; + + for (i = 0; *s && n < len; i++, s++) { + if (ISUTF8(*s)) { + if ((r = mbtowc(&w, s, 4)) == -1) + break; + if ((r = wcwidth(w)) == -1) + r = 1; + n += (size_t)r; + } + putc(*s, fp); + } + for (; n < len; n++) + putc(pad, fp); +} + +void +joinpath(char *buf, size_t bufsiz, const char *path, const char *path2) +{ + int r; + + r = snprintf(buf, bufsiz, "%s%s%s", + path, path[0] && path[strlen(path) - 1] != '/' ? "/" : "", path2); + if (r == -1 || (size_t)r >= bufsiz) + errx(1, "path truncated: '%s%s%s'", + path, path[0] && path[strlen(path) - 1] != '/' ? "/" : "", path2); +} + +void +deltainfo_free(struct deltainfo *di) +{ + if (!di) + return; + git_patch_free(di->patch); + di->patch = NULL; + free(di); +} + +int +commitinfo_getstats(struct commitinfo *ci) +{ + struct deltainfo *di; + const git_diff_delta *delta; + const git_diff_hunk *hunk; + const git_diff_line *line; + git_patch *patch = NULL; + size_t ndeltas, nhunks, nhunklines; + size_t i, j, k; + + ndeltas = git_diff_num_deltas(ci->diff); + if (ndeltas && !(ci->deltas = calloc(ndeltas, sizeof(struct deltainfo *)))) + err(1, "calloc"); + + for (i = 0; i < ndeltas; i++) { + if (git_patch_from_diff(&patch, ci->diff, i)) + goto err; + if (!(di = calloc(1, sizeof(struct deltainfo)))) + err(1, "calloc"); + di->patch = patch; + ci->deltas[i] = di; + + delta = git_patch_get_delta(patch); + + /* skip stats for binary data */ + if (delta->flags & GIT_DIFF_FLAG_BINARY) + continue; + + nhunks = git_patch_num_hunks(patch); + for (j = 0; j < nhunks; j++) { + if (git_patch_get_hunk(&hunk, &nhunklines, patch, j)) + break; + for (k = 0; ; k++) { + if (git_patch_get_line_in_hunk(&line, patch, j, k)) + break; + if (line->old_lineno == -1) { + di->addcount++; + ci->addcount++; + } else if (line->new_lineno == -1) { + di->delcount++; + ci->delcount++; + } + } + } + } + ci->ndeltas = i; + ci->filecount = i; + + return 0; + +err: + if (ci->deltas) + for (i = 0; i < ci->ndeltas; i++) + deltainfo_free(ci->deltas[i]); + free(ci->deltas); + ci->deltas = NULL; + ci->ndeltas = 0; + ci->addcount = 0; + ci->delcount = 0; + ci->filecount = 0; + + return -1; +} + +void +commitinfo_free(struct commitinfo *ci) +{ + size_t i; + + if (!ci) + return; + if (ci->deltas) + for (i = 0; i < ci->ndeltas; i++) + deltainfo_free(ci->deltas[i]); + free(ci->deltas); + ci->deltas = NULL; + git_diff_free(ci->diff); + git_tree_free(ci->commit_tree); + git_tree_free(ci->parent_tree); + git_commit_free(ci->commit); + git_commit_free(ci->parent); + free(ci); +} + +struct commitinfo * +commitinfo_getbyoid(const git_oid *id) +{ + struct commitinfo *ci; + git_diff_options opts; + + if (!(ci = calloc(1, sizeof(struct commitinfo)))) + err(1, "calloc"); + + if (git_commit_lookup(&(ci->commit), repo, id)) + goto err; + ci->id = id; + + git_oid_tostr(ci->oid, sizeof(ci->oid), git_commit_id(ci->commit)); + git_oid_tostr(ci->parentoid, sizeof(ci->parentoid), git_commit_parent_id(ci->commit, 0)); + + ci->author = git_commit_author(ci->commit); + ci->committer = git_commit_committer(ci->commit); + ci->summary = git_commit_summary(ci->commit); + ci->msg = git_commit_message(ci->commit); + + if (git_tree_lookup(&(ci->commit_tree), repo, git_commit_tree_id(ci->commit))) + goto err; + if (!git_commit_parent(&(ci->parent), ci->commit, 0)) { + if (git_tree_lookup(&(ci->parent_tree), repo, git_commit_tree_id(ci->parent))) { + ci->parent = NULL; + ci->parent_tree = NULL; + } + } + + git_diff_init_options(&opts, GIT_DIFF_OPTIONS_VERSION); + opts.flags |= GIT_DIFF_DISABLE_PATHSPEC_MATCH; + if (git_diff_tree_to_tree(&(ci->diff), repo, ci->parent_tree, ci->commit_tree, &opts)) + goto err; + if (commitinfo_getstats(ci) == -1) + goto err; + + return ci; + +err: + commitinfo_free(ci); + + return NULL; +} + +FILE * +efopen(const char *name, const char *flags) +{ + FILE *fp; + + if (!(fp = fopen(name, flags))) + err(1, "fopen"); + + return fp; +} + +/* Escape characters below as HTML 2.0 / XML 1.0. */ +void +xmlencode(FILE *fp, const char *s, size_t len) +{ + size_t i; + + for (i = 0; *s && i < len; s++, i++) { + switch(*s) { + case '<': fputs("&lt;", fp); break; + case '>': fputs("&gt;", fp); break; + case '\'': fputs("&#39;", fp); break; + case '&': fputs("&amp;", fp); break; + case '"': fputs("&quot;", fp); break; + default: fputc(*s, fp); + } + } +} + +void +trim(char *buf, size_t bufsiz, const char *src) +{ + size_t d = 0, i, len, s; + + len = strlen(src); + for (s = 0; s < len && d < bufsiz - 1; s++) { + switch (src[s]) { + case '\t': + if (d + 8 >= bufsiz - 1) + goto end; + for (i = 0; i < 8; i++) + buf[d++] = ' '; + break; + case '|': + case '\n': + case '\r': + buf[d++] = ' '; + break; + default: + buf[d++] = src[s]; + break; + } + } +end: + buf[d] = '\0'; +} + +/* Escape characters in text in geomyidae .gph format */ +void +gphtext(FILE *fp, const char *s, size_t len) +{ + size_t i, n = 0; + + for (i = 0; *s && i < len; i++) { + if (s[i] == '\n') + n = 0; + + /* escape 't' at the start of a line */ + if (!n && s[i] == 't') { + fputc('t', fp); + n = 1; + } + + switch (s[i]) { + case '\r': break; + case '\t': fputs(" ", fp); break; + default: fputc(s[i], fp); + } + n++; + } +} + +/* Escape characters in links in geomyidae .gph format */ +void +gphlink(FILE *fp, const char *s, size_t len) +{ + size_t i; + + for (i = 0; *s && i < len; i++) { + switch (s[i]) { + case '\n': + /* in this context replace newline with space */ + fputc(' ', fp); + break; + case '\r': /* ignore CR */ + case '|': /* ignore separators for now */ + break; + case '\t': + fputs(" ", fp); + break; + default: + fputc(s[i], fp); + break; + } + } +} + +int +mkdirp(const char *path) +{ + char tmp[PATH_MAX], *p; + + if (strlcpy(tmp, path, sizeof(tmp)) >= sizeof(tmp)) + errx(1, "path truncated: '%s'", path); + for (p = tmp + (tmp[0] == '/'); *p; p++) { + if (*p != '/') + continue; + *p = '\0'; + if (mkdir(tmp, S_IRWXU | S_IRWXG | S_IRWXO) < 0 && errno != EEXIST) + return -1; + *p = '/'; + } + if (mkdir(tmp, S_IRWXU | S_IRWXG | S_IRWXO) < 0 && errno != EEXIST) + return -1; + return 0; +} + +void +printtimez(FILE *fp, const git_time *intime) +{ + struct tm *intm; + time_t t; + char out[32]; + + t = (time_t)intime->time; + if (!(intm = gmtime(&t))) + return; + strftime(out, sizeof(out), "%Y-%m-%dT%H:%M:%SZ", intm); + fputs(out, fp); +} + +void +printtime(FILE *fp, const git_time *intime) +{ + struct tm *intm; + time_t t; + char out[32]; + + t = (time_t)intime->time + (intime->offset * 60); + if (!(intm = gmtime(&t))) + return; + strftime(out, sizeof(out), "%a, %e %b %Y %H:%M:%S", intm); + if (intime->offset < 0) + fprintf(fp, "%s -%02d%02d", out, + -(intime->offset) / 60, -(intime->offset) % 60); + else + fprintf(fp, "%s +%02d%02d", out, + intime->offset / 60, intime->offset % 60); +} + +void +printtimeshort(FILE *fp, const git_time *intime) +{ + struct tm *intm; + time_t t; + char out[32]; + + t = (time_t)intime->time; + if (!(intm = gmtime(&t))) + return; + strftime(out, sizeof(out), "%Y-%m-%d %H:%M", intm); + fputs(out, fp); +} + +void +writeheader(FILE *fp, const char *title) +{ + gphtext(fp, title, strlen(title)); + if (title[0] && strippedname[0]) + fputs(" - ", fp); + gphtext(fp, strippedname, strlen(strippedname)); + if (description[0]) + fputs(" - ", fp); + gphtext(fp, description, strlen(description)); + fputs("\n", fp); + if (cloneurl[0]) { + fputs("[h|git clone ", fp); + gphlink(fp, cloneurl, strlen(cloneurl)); + fputs("|URL:", fp); + gphlink(fp, cloneurl, strlen(cloneurl)); + fputs("|server|port]\n", fp); + } + fprintf(fp, "[1|Log|%slog.gph|server|port]\n", relpath); + fprintf(fp, "[1|Files|%sfiles.gph|server|port]\n", relpath); + fprintf(fp, "[1|Refs|%srefs.gph|server|port]\n", relpath); + if (hassubmodules) + fprintf(fp, "[1|Submodules|%sfile/.gitmodules.gph|server|port]\n", relpath); + if (hasreadme) + fprintf(fp, "[1|README|%sfile/README.gph|server|port]\n", relpath); + if (haslicense) + fprintf(fp, "[1|LICENSE|%sfile/LICENSE.gph|server|port]\n", relpath); + fputs("---\n", fp); +} + +void +writefooter(FILE *fp) +{ +} + +int +writeblobgph(FILE *fp, const git_blob *blob) +{ + size_t n = 0, i, j, prev; + const char *nfmt = "%6d "; + const char *s = git_blob_rawcontent(blob); + git_off_t len = git_blob_rawsize(blob); + + if (len > 0) { + for (i = 0, prev = 0; i < (size_t)len; i++) { + if (s[i] != '\n') + continue; + n++; + fprintf(fp, nfmt, n, n, n); + for (j = prev; s[j] && j <= i; j++) { + switch (s[j]) { + case '\r': break; + case '\t': fputs(" ", fp); break; + default: fputc(s[j], fp); + } + } + prev = i + 1; + } + /* trailing data */ + if ((len - prev) > 0) { + n++; + fprintf(fp, nfmt, n, n, n); + for (j = prev; s[j] && j < len - prev; j++) { + switch (s[j]) { + case '\r': break; + case '\t': fputs(" ", fp); break; + default: fputc(s[j], fp); + } + } + } + } + + return n; +} + +void +printcommit(FILE *fp, struct commitinfo *ci) +{ + fprintf(fp, "[1|commit %s|%scommit/%s.gph|server|port]\n", + ci->oid, relpath, ci->oid); + + if (ci->parentoid[0]) + fprintf(fp, "[1|parent %s|%scommit/%s.gph|server|port]\n", + ci->parentoid, relpath, ci->parentoid); + + if (ci->author) { + fputs("[h|Author: ", fp); + gphlink(fp, ci->author->name, strlen(ci->author->name)); + fputs(" <", fp); + gphlink(fp, ci->author->email, strlen(ci->author->email)); + fputs(">|URL:mailto:", fp); + gphlink(fp, ci->author->email, strlen(ci->author->email)); + fputs("|server|port]\n", fp); + fputs("Date: ", fp); + printtime(fp, &(ci->author->when)); + fputc('\n', fp); + } + if (ci->msg) { + fputc('\n', fp); + gphtext(fp, ci->msg, strlen(ci->msg)); + fputc('\n', fp); + } +} + +void +printshowfile(FILE *fp, struct commitinfo *ci) +{ + const git_diff_delta *delta; + const git_diff_hunk *hunk; + const git_diff_line *line; + git_patch *patch; + size_t nhunks, nhunklines, changed, add, del, total, i, j, k; + char linestr[80]; + + printcommit(fp, ci); + + if (!ci->deltas) + return; + + if (ci->filecount > 1000 || + ci->ndeltas > 1000 || + ci->addcount > 100000 || + ci->delcount > 100000) { + fputs("\nDiff is too large, output suppressed.\n", fp); + return; + } + + /* diff stat */ + fputs("Diffstat:\n", fp); + for (i = 0; i < ci->ndeltas; i++) { + delta = git_patch_get_delta(ci->deltas[i]->patch); + /* TODO: make file linkable */ + gphtext(fp, delta->old_file.path, strlen(delta->old_file.path)); + if (strcmp(delta->old_file.path, delta->new_file.path)) { + fputs(" -> ", fp); + gphtext(fp, delta->new_file.path, strlen(delta->new_file.path)); + } + + add = ci->deltas[i]->addcount; + del = ci->deltas[i]->delcount; + changed = add + del; + total = sizeof(linestr) - 2; + if (changed > total) { + if (add) + add = ((float)total / changed * add) + 1; + if (del) + del = ((float)total / changed * del) + 1; + } + memset(&linestr, '+', add); + memset(&linestr[add], '-', del); + + fprintf(fp, " | %zu ", + ci->deltas[i]->addcount + ci->deltas[i]->delcount); + fwrite(&linestr, 1, add, fp); + fwrite(&linestr[add], 1, del, fp); + fputs("\n", fp); + } + fprintf(fp, "\n%zu file%s changed, %zu insertion%s(+), %zu deletion%s(-)\n", + ci->filecount, ci->filecount == 1 ? "" : "s", + ci->addcount, ci->addcount == 1 ? "" : "s", + ci->delcount, ci->delcount == 1 ? "" : "s"); + + fputs("---\n", fp); + + for (i = 0; i < ci->ndeltas; i++) { + patch = ci->deltas[i]->patch; + delta = git_patch_get_delta(patch); + /* NOTE: only links to new path */ + fprintf(fp, "[1|diff --git a/%s b/%s", + delta->old_file.path, delta->new_file.path); + fprintf(fp, "|%sfile/%s.gph|server|port]\n", relpath, delta->new_file.path); + + /* check binary data */ + if (delta->flags & GIT_DIFF_FLAG_BINARY) { + fputs("Binary files differ.\n", fp); + continue; + } + + nhunks = git_patch_num_hunks(patch); + for (j = 0; j < nhunks; j++) { + if (git_patch_get_hunk(&hunk, &nhunklines, patch, j)) + break; + + gphtext(fp, hunk->header, hunk->header_len); + + for (k = 0; ; k++) { + if (git_patch_get_line_in_hunk(&line, patch, j, k)) + break; + if (line->old_lineno == -1) + fputs("+", fp); + else if (line->new_lineno == -1) + fputs("-", fp); + else + fputs(" ", fp); + gphtext(fp, line->content, line->content_len); + } + } + } +} + +void +writelogline(FILE *fp, struct commitinfo *ci) +{ + char buf[1024]; + + fputs("[1|", fp); + if (ci->author) + printtimeshort(fp, &(ci->author->when)); + fputs(" ", fp); + if (ci->summary) { + trim(buf, sizeof(buf), ci->summary); + printutf8pad(fp, buf, 50, ' '); + } + fputs(" ", fp); + if (ci->author) { + trim(buf, sizeof(buf), ci->author->name); + printutf8pad(fp, buf, 25, ' '); + } + fprintf(fp, "|%scommit/%s.gph", relpath, ci->oid); + fputs("|server|port]\n", fp); +} + +int +writelog(FILE *fp, const git_oid *oid) +{ + struct commitinfo *ci; + git_revwalk *w = NULL; + git_oid id; + char path[PATH_MAX]; + FILE *fpfile; + int r; + + git_revwalk_new(&w, repo); + git_revwalk_push(w, oid); + git_revwalk_sorting(w, GIT_SORT_TIME); + git_revwalk_simplify_first_parent(w); + + while (!git_revwalk_next(&id, w)) { + if (cachefile && !memcmp(&id, &lastoid, sizeof(id))) + break; + if (!(ci = commitinfo_getbyoid(&id))) + break; + + writelogline(fp, ci); + if (cachefile) + writelogline(wcachefp, ci); + + r = snprintf(path, sizeof(path), "commit/%s.gph", ci->oid); + if (r == -1 || (size_t)r >= sizeof(path)) + errx(1, "path truncated: 'commit/%s.gph'", ci->oid); + + /* check if file exists if so skip it */ + if (access(path, F_OK)) { + fpfile = efopen(path, "w"); + writeheader(fpfile, ci->summary); + printshowfile(fpfile, ci); + writefooter(fpfile); + fclose(fpfile); + } + commitinfo_free(ci); + } + git_revwalk_free(w); + + return 0; +} + +void +printcommitatom(FILE *fp, struct commitinfo *ci) +{ + fputs("<entry>\n", fp); + + fprintf(fp, "<id>%s</id>\n", ci->oid); + if (ci->author) { + fputs("<published>", fp); + printtimez(fp, &(ci->author->when)); + fputs("</published>\n", fp); + } + if (ci->committer) { + fputs("<updated>", fp); + printtimez(fp, &(ci->committer->when)); + fputs("</updated>\n", fp); + } + if (ci->summary) { + fputs("<title type=\"text\">", fp); + xmlencode(fp, ci->summary, strlen(ci->summary)); + fputs("</title>\n", fp); + } + fprintf(fp, "<link rel=\"alternate\" type=\"text/html\" href=\"commit/%s.gph\" />", + ci->oid); + + if (ci->author) { + fputs("<author><name>", fp); + xmlencode(fp, ci->author->name, strlen(ci->author->name)); + fputs("</name>\n<email>", fp); + xmlencode(fp, ci->author->email, strlen(ci->author->email)); + fputs("</email>\n</author>\n", fp); + } + + fputs("<content type=\"text\">", fp); + fprintf(fp, "commit %s\n", ci->oid); + if (ci->parentoid[0]) + fprintf(fp, "parent %s\n", ci->parentoid); + if (ci->author) { + fputs("Author: ", fp); + xmlencode(fp, ci->author->name, strlen(ci->author->name)); + fputs(" &lt;", fp); + xmlencode(fp, ci->author->email, strlen(ci->author->email)); + fputs("&gt;\nDate: ", fp); + printtime(fp, &(ci->author->when)); + fputc('\n', fp); + } + if (ci->msg) { + fputc('\n', fp); + xmlencode(fp, ci->msg, strlen(ci->msg)); + } + fputs("\n</content>\n</entry>\n", fp); +} + +int +writeatom(FILE *fp) +{ + struct commitinfo *ci; + git_revwalk *w = NULL; + git_oid id; + size_t i, m = 100; /* last 'm' commits */ + + fputs("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + "<feed xmlns=\"http://www.w3.org/2005/Atom\">\n<title>", fp); + xmlencode(fp, strippedname, strlen(strippedname)); + fputs(", branch HEAD</title>\n<subtitle>", fp); + xmlencode(fp, description, strlen(description)); + fputs("</subtitle>\n", fp); + + git_revwalk_new(&w, repo); + git_revwalk_push_head(w); + git_revwalk_sorting(w, GIT_SORT_TIME); + git_revwalk_simplify_first_parent(w); + + for (i = 0; i < m && !git_revwalk_next(&id, w); i++) { + if (!(ci = commitinfo_getbyoid(&id))) + break; + printcommitatom(fp, ci); + commitinfo_free(ci); + } + git_revwalk_free(w); + + fputs("</feed>\n", fp); + + return 0; +} + +int +writeblob(git_object *obj, const char *fpath, const char *filename, git_off_t filesize) +{ + char tmp[PATH_MAX] = "", *d; + int lc = 0; + FILE *fp; + + if (strlcpy(tmp, fpath, sizeof(tmp)) >= sizeof(tmp)) + errx(1, "path truncated: '%s'", fpath); + if (!(d = dirname(tmp))) + err(1, "dirname"); + if (mkdirp(d)) + return -1; + + fp = efopen(fpath, "w"); + writeheader(fp, filename); + gphtext(fp, filename, strlen(filename)); + fprintf(fp, " (%juB)\n", (uintmax_t)filesize); + fputs("---\n", fp); + + if (git_blob_is_binary((git_blob *)obj)) { + fputs("Binary file.\n", fp); + } else { + lc = writeblobgph(fp, (git_blob *)obj); + if (ferror(fp)) + err(1, "fwrite"); + } + writefooter(fp); + fclose(fp); + + return lc; +} + +const char * +filemode(git_filemode_t m) +{ + static char mode[11]; + + memset(mode, '-', sizeof(mode) - 1); + mode[10] = '\0'; + + if (S_ISREG(m)) + mode[0] = '-'; + else if (S_ISBLK(m)) + mode[0] = 'b'; + else if (S_ISCHR(m)) + mode[0] = 'c'; + else if (S_ISDIR(m)) + mode[0] = 'd'; + else if (S_ISFIFO(m)) + mode[0] = 'p'; + else if (S_ISLNK(m)) + mode[0] = 'l'; + else if (S_ISSOCK(m)) + mode[0] = 's'; + else + mode[0] = '?'; + + if (m & S_IRUSR) mode[1] = 'r'; + if (m & S_IWUSR) mode[2] = 'w'; + if (m & S_IXUSR) mode[3] = 'x'; + if (m & S_IRGRP) mode[4] = 'r'; + if (m & S_IWGRP) mode[5] = 'w'; + if (m & S_IXGRP) mode[6] = 'x'; + if (m & S_IROTH) mode[7] = 'r'; + if (m & S_IWOTH) mode[8] = 'w'; + if (m & S_IXOTH) mode[9] = 'x'; + + if (m & S_ISUID) mode[3] = (mode[3] == 'x') ? 's' : 'S'; + if (m & S_ISGID) mode[6] = (mode[6] == 'x') ? 's' : 'S'; + if (m & S_ISVTX) mode[9] = (mode[9] == 'x') ? 't' : 'T'; + + return mode; +} + +int +writefilestree(FILE *fp, git_tree *tree, const char *path) +{ + const git_tree_entry *entry = NULL; + git_submodule *module = NULL; + git_object *obj = NULL; + git_off_t filesize; + const char *entryname; + char filepath[PATH_MAX], entrypath[PATH_MAX]; + char buf[1024]; + size_t count, i; + int lc, r, ret; + + count = git_tree_entrycount(tree); + for (i = 0; i < count; i++) { + if (!(entry = git_tree_entry_byindex(tree, i)) || + !(entryname = git_tree_entry_name(entry))) + return -1; + joinpath(entrypath, sizeof(entrypath), path, entryname); + + r = snprintf(filepath, sizeof(filepath), "file/%s.gph", + entrypath); + if (r == -1 || (size_t)r >= sizeof(filepath)) + errx(1, "path truncated: 'file/%s.gph'", entrypath); + + if (!git_tree_entry_to_object(&obj, repo, entry)) { + switch (git_object_type(obj)) { + case GIT_OBJ_BLOB: + break; + case GIT_OBJ_TREE: + /* NOTE: recurses */ + ret = writefilestree(fp, (git_tree *)obj, + entrypath); + git_object_free(obj); + if (ret) + return ret; + continue; + default: + git_object_free(obj); + continue; + } + + filesize = git_blob_rawsize((git_blob *)obj); + lc = writeblob(obj, filepath, entryname, filesize); + + fputs("[1|", fp); + fputs(filemode(git_tree_entry_filemode(entry)), fp); + fputs(" ", fp); + trim(buf, sizeof(buf), entrypath); + printutf8pad(fp, buf, 50, ' '); + fputs(" ", fp); + if (lc > 0) + fprintf(fp, "%7dL", lc); + else + fprintf(fp, "%7juB", (uintmax_t)filesize); + fprintf(fp, "|%s%s", relpath, filepath); + fputs("|server|port]\n", fp); + git_object_free(obj); + } else if (!git_submodule_lookup(&module, repo, entryname)) { + fputs("[1|m--------- ", fp); + trim(buf, sizeof(buf), entrypath); + printutf8pad(fp, buf, 50, ' '); + fprintf(fp, "|%sfile/.gitmodules.gph|server|port]\n", relpath); + /* NOTE: linecount omitted */ + git_submodule_free(module); + } + } + + return 0; +} + +int +writefiles(FILE *fp, const git_oid *id) +{ + git_tree *tree = NULL; + git_commit *commit = NULL; + int ret = -1; + + fprintf(fp, "%-10.10s ", "Mode"); + fprintf(fp, "%-50.50s ", "Name"); + fprintf(fp, "%8.8s\n", "Size"); + + if (!git_commit_lookup(&commit, repo, id) && + !git_commit_tree(&tree, commit)) + ret = writefilestree(fp, tree, ""); + + git_commit_free(commit); + git_tree_free(tree); + + return ret; +} + +int +refs_cmp(const void *v1, const void *v2) +{ + git_reference *r1 = (*(git_reference **)v1); + git_reference *r2 = (*(git_reference **)v2); + int r; + + if ((r = git_reference_is_branch(r1) - git_reference_is_branch(r2))) + return r; + + return strcmp(git_reference_shorthand(r1), + git_reference_shorthand(r2)); +} + +int +writerefs(FILE *fp) +{ + struct commitinfo *ci; + const git_oid *id = NULL; + git_object *obj = NULL; + git_reference *dref = NULL, *r, *ref = NULL; + git_reference_iterator *it = NULL; + git_reference **refs = NULL; + size_t count, i, j, refcount; + const char *titles[] = { "Branches", "Tags" }; + const char *name; + char buf[1024]; + + if (git_reference_iterator_new(&it, repo)) + return -1; + + for (refcount = 0; !git_reference_next(&ref, it); refcount++) { + if (!(refs = reallocarray(refs, refcount + 1, sizeof(git_reference *)))) + err(1, "realloc"); + refs[refcount] = ref; + } + git_reference_iterator_free(it); + + /* sort by type then shorthand name */ + qsort(refs, refcount, sizeof(git_reference *), refs_cmp); + + for (j = 0; j < 2; j++) { + for (i = 0, count = 0; i < refcount; i++) { + if (!(git_reference_is_branch(refs[i]) && j == 0) && + !(git_reference_is_tag(refs[i]) && j == 1)) + continue; + + switch (git_reference_type(refs[i])) { + case GIT_REF_SYMBOLIC: + if (git_reference_resolve(&dref, refs[i])) + goto err; + r = dref; + break; + case GIT_REF_OID: + r = refs[i]; + break; + default: + continue; + } + if (!git_reference_target(r) || + git_reference_peel(&obj, r, GIT_OBJ_ANY)) + goto err; + if (!(id = git_object_id(obj))) + goto err; + if (!(ci = commitinfo_getbyoid(id))) + break; + + /* print header if it has an entry (first). */ + if (++count == 1) { + fprintf(fp, "%s\n", titles[j]); + fprintf(fp, " %-20.20s", "Name"); + fprintf(fp, " %-16.16s", "Last commit date"); + fprintf(fp, " %-25.25s\n", "Author"); + } + + name = git_reference_shorthand(r); + + fputs(" ", fp); + trim(buf, sizeof(buf), name); + printutf8pad(fp, buf, 20, ' '); + fputs(" ", fp); + if (ci->author) + printtimeshort(fp, &(ci->author->when)); + fputs(" ", fp); + if (ci->author) { + trim(buf, sizeof(buf), ci->author->name); + printutf8pad(fp, buf, 25, ' '); + } + fputs("\n", fp); + + commitinfo_free(ci); + git_object_free(obj); + obj = NULL; + git_reference_free(dref); + dref = NULL; + } + /* table footer */ + if (count) + fputs("\n", fp); + } + +err: + git_object_free(obj); + git_reference_free(dref); + + for (i = 0; i < refcount; i++) + git_reference_free(refs[i]); + free(refs); + + return 0; +} + +void +usage(char *argv0) +{ + fprintf(stderr, "%s [-c cachefile] repodir\n", argv0); + exit(1); +} + +/* TODO: add base argument, gopher does not support relative urls, document it too */ +int +main(int argc, char *argv[]) +{ + git_object *obj = NULL; + const git_oid *head = NULL; + const git_error *e = NULL; + FILE *fp, *fpread; + char path[PATH_MAX], repodirabs[PATH_MAX + 1], *p; + char tmppath[64] = "cache.XXXXXXXXXXXX", buf[BUFSIZ]; + size_t n; + int i, fd; + + if (pledge("stdio rpath wpath cpath", NULL) == -1) + err(1, "pledge"); + + for (i = 1; i < argc; i++) { + if (argv[i][0] != '-') { + if (repodir) + usage(argv[0]); + repodir = argv[i]; + } else if (argv[i][1] == 'c') { + if (i + 1 >= argc) + usage(argv[0]); + cachefile = argv[++i]; + } else if (argv[i][1] == 'b') { + if (i + 1 >= argc) + usage(argv[0]); + relpath = argv[++i]; + } + } + if (!repodir) + usage(argv[0]); + + if (!realpath(repodir, repodirabs)) + err(1, "realpath"); + + git_libgit2_init(); + + if (git_repository_open_ext(&repo, repodir, + GIT_REPOSITORY_OPEN_NO_SEARCH, NULL) < 0) { + e = giterr_last(); + fprintf(stderr, "%s: %s\n", argv[0], e->message); + return 1; + } + + /* find HEAD */ + if (!git_revparse_single(&obj, repo, "HEAD")) + head = git_object_id(obj); + git_object_free(obj); + + /* don't cache if there is no HEAD */ + if (!head) + cachefile = NULL; + + /* use directory name as name */ + if ((name = strrchr(repodirabs, '/'))) + name++; + else + name = ""; + + /* strip .git suffix */ + if (!(strippedname = strdup(name))) + err(1, "strdup"); + if ((p = strrchr(strippedname, '.'))) + if (!strcmp(p, ".git")) + *p = '\0'; + + /* read description or .git/description */ + joinpath(path, sizeof(path), repodir, "description"); + if (!(fpread = fopen(path, "r"))) { + joinpath(path, sizeof(path), repodir, ".git/description"); + fpread = fopen(path, "r"); + } + if (fpread) { + if (!fgets(description, sizeof(description), fpread)) + description[0] = '\0'; + fclose(fpread); + } + + /* read url or .git/url */ + joinpath(path, sizeof(path), repodir, "url"); + if (!(fpread = fopen(path, "r"))) { + joinpath(path, sizeof(path), repodir, ".git/url"); + fpread = fopen(path, "r"); + } + if (fpread) { + if (!fgets(cloneurl, sizeof(cloneurl), fpread)) + cloneurl[0] = '\0'; + cloneurl[strcspn(cloneurl, "\n")] = '\0'; + fclose(fpread); + } + + /* check LICENSE */ + haslicense = (!git_revparse_single(&obj, repo, "HEAD:LICENSE") && + git_object_type(obj) == GIT_OBJ_BLOB); + git_object_free(obj); + + /* check README */ + hasreadme = (!git_revparse_single(&obj, repo, "HEAD:README") && + git_object_type(obj) == GIT_OBJ_BLOB); + git_object_free(obj); + + hassubmodules = (!git_revparse_single(&obj, repo, "HEAD:.gitmodules") && + git_object_type(obj) == GIT_OBJ_BLOB); + git_object_free(obj); + + /* log for HEAD */ + fp = efopen("log.gph", "w"); + mkdir("commit", 0755); + writeheader(fp, "Log"); + fprintf(fp, "%-16.16s ", "Date"); + fprintf(fp, "%-50.50s ", "Commit message"); + fprintf(fp, "%-25.25s\n", "Author"); + + if (cachefile) { + /* read from cache file (does not need to exist) */ + if ((rcachefp = fopen(cachefile, "r"))) { + if (!fgets(lastoidstr, sizeof(lastoidstr), rcachefp)) + errx(1, "%s: no object id", cachefile); + if (git_oid_fromstr(&lastoid, lastoidstr)) + errx(1, "%s: invalid object id", cachefile); + } + + /* write log to (temporary) cache */ + if ((fd = mkstemp(tmppath)) == -1) + err(1, "mkstemp"); + if (!(wcachefp = fdopen(fd, "w"))) + err(1, "fdopen"); + /* write last commit id (HEAD) */ + git_oid_tostr(buf, sizeof(buf), head); + fprintf(wcachefp, "%s\n", buf); + + writelog(fp, head); + + if (rcachefp) { + /* append previous log to log.gph and the new cache */ + while (!feof(rcachefp)) { + n = fread(buf, 1, sizeof(buf), rcachefp); + if (ferror(rcachefp)) + err(1, "fread"); + if (fwrite(buf, 1, n, fp) != n || + fwrite(buf, 1, n, wcachefp) != n) + err(1, "fwrite"); + } + fclose(rcachefp); + } + fclose(wcachefp); + } else { + if (head) + writelog(fp, head); + } + writefooter(fp); + fclose(fp); + + /* files for HEAD */ + fp = efopen("files.gph", "w"); + writeheader(fp, "Files"); + if (head) + writefiles(fp, head); + writefooter(fp); + fclose(fp); + + /* summary page with branches and tags */ + fp = efopen("refs.gph", "w"); + writeheader(fp, "Refs"); + writerefs(fp); + writefooter(fp); + fclose(fp); + + /* Atom feed */ + fp = efopen("atom.xml", "w"); + writeatom(fp); + fclose(fp); + + /* rename new cache file on success */ + if (cachefile && rename(tmppath, cachefile)) + err(1, "rename: '%s' to '%s'", tmppath, cachefile); + + /* cleanup */ + git_repository_free(repo); + git_libgit2_shutdown(); + + return 0; +} diff --git a/stagit-index.1 b/stagit-index.1 @@ -1,42 +0,0 @@ -.Dd December 26, 2015 -.Dt STAGIT-INDEX 1 -.Os -.Sh NAME -.Nm stagit-index -.Nd static git index page generator -.Sh SYNOPSIS -.Nm -.Op Ar repodir... -.Sh DESCRIPTION -.Nm -will create an index HTML page for the repositories specified and writes -the HTML data to stdout. -The repos in the index are in the same order as the arguments -.Ar repodir -specified. -.Pp -The basename of the directory is used as the repository name. -The suffix ".git" is removed from the basename, this suffix is commonly used -for "bare" repos. -.Pp -The content of the follow files specifies the meta data for each repository: -.Bl -tag -width Ds -.It .git/description or description (bare repos). -description -.It .git/owner or owner (bare repo). -owner of repository -.El -.Pp -For changing the style of the page you can use the following files: -.Bl -tag -width Ds -.It favicon.png -favicon image. -.It logo.png -32x32 logo. -.It style.css -CSS stylesheet. -.El -.Sh SEE ALSO -.Xr stagit 1 -.Sh AUTHORS -.An Hiltjo Posthuma Aq Mt hiltjo@codemadness.org diff --git a/stagit-index.c b/stagit-index.c @@ -1,250 +0,0 @@ -#include <sys/stat.h> - -#include <err.h> -#include <errno.h> -#include <inttypes.h> -#include <limits.h> -#include <stdio.h> -#include <stdlib.h> -#include <string.h> -#include <unistd.h> -#include <wchar.h> - -#include <git2.h> - -#include "compat.h" - -static git_repository *repo; - -static const char *relpath = "/"; - -static char description[255] = "Repositories"; -static char *name = ""; - -#ifndef USE_PLEDGE -#define pledge(p1,p2) 0 -#endif - -#define ISUTF8(c) (((c) & 0xc0) != 0x80) - -/* print `len' columns of characters. If string is shorter pad the rest - * with characters `pad`. */ -void -printutf8pad(FILE *fp, const char *s, size_t len, int pad) -{ - wchar_t w; - size_t n = 0, i; - int r; - - for (i = 0; *s && n < len; i++, s++) { - if (ISUTF8(*s)) { - if ((r = mbtowc(&w, s, 4)) == -1) - break; - if ((r = wcwidth(w)) == -1) - r = 1; - n += (size_t)r; - } - putc(*s, fp); - } - for (; n < len; n++) - putc(pad, fp); -} - -void -trim(char *buf, size_t bufsiz, const char *src) -{ - size_t d = 0, i, len, s; - - len = strlen(src); - for (s = 0; s < len && d < bufsiz - 1; s++) { - switch (src[s]) { - case '\t': - if (d + 8 >= bufsiz - 1) - goto end; - for (i = 0; i < 8; i++) - buf[d++] = ' '; - break; - case '|': - case '\n': - case '\r': - buf[d++] = ' '; - break; - default: - buf[d++] = src[s]; - break; - } - } -end: - buf[d] = '\0'; -} - -void -joinpath(char *buf, size_t bufsiz, const char *path, const char *path2) -{ - int r; - - r = snprintf(buf, bufsiz, "%s%s%s", - path, path[0] && path[strlen(path) - 1] != '/' ? "/" : "", path2); - if (r == -1 || (size_t)r >= bufsiz) - errx(1, "path truncated: '%s%s%s'", - path, path[0] && path[strlen(path) - 1] != '/' ? "/" : "", path2); -} - -void -printtimeshort(FILE *fp, const git_time *intime) -{ - struct tm *intm; - time_t t; - char out[32]; - - t = (time_t)intime->time; - if (!(intm = gmtime(&t))) - return; - strftime(out, sizeof(out), "%Y-%m-%d %H:%M", intm); - fputs(out, fp); -} - -void -writeheader(FILE *fp) -{ - char buf[1024]; - - trim(buf, sizeof(buf), description); - if (buf[0] == 't') - fputc('t', fp); - fprintf(fp, "%s\n\n", buf); - - fprintf(fp, "%-20.20s ", "Name"); - fprintf(fp, "%-50.50s ", "Description"); - fprintf(fp, "%-16.16s\n", "Last commit"); -} - -void -writefooter(FILE *fp) -{ -} - -int -writelog(FILE *fp) -{ - git_commit *commit = NULL; - const git_signature *author; - git_revwalk *w = NULL; - git_oid id; - char *stripped_name = NULL, *p; - char buf[1024]; - int ret = 0; - - git_revwalk_new(&w, repo); - git_revwalk_push_head(w); - git_revwalk_sorting(w, GIT_SORT_TIME); - git_revwalk_simplify_first_parent(w); - - if (git_revwalk_next(&id, w) || - git_commit_lookup(&commit, repo, &id)) { - ret = -1; - goto err; - } - - author = git_commit_author(commit); - - /* strip .git suffix */ - if (!(stripped_name = strdup(name))) - err(1, "strdup"); - if ((p = strrchr(stripped_name, '.'))) - if (!strcmp(p, ".git")) - *p = '\0'; - - fputs("[1|", fp); - trim(buf, sizeof(buf), stripped_name); - printutf8pad(fp, buf, 20, ' '); - fputs(" ", fp); - trim(buf, sizeof(buf), description); - printutf8pad(fp, buf, 50, ' '); - fputs(" ", fp); - if (author) - printtimeshort(fp, &(author->when)); - trim(buf, sizeof(buf), stripped_name); - fprintf(fp, "|%s%s/log.gph|server|port]\n", relpath, buf); - - git_commit_free(commit); -err: - git_revwalk_free(w); - free(stripped_name); - - return ret; -} - -void -usage(const char *argv0) -{ - fprintf(stderr, "%s [repodir...]\n", argv0); - exit(1); -} - - -int -main(int argc, char *argv[]) -{ - const git_error *e = NULL; - FILE *fp; - char path[PATH_MAX], repodirabs[PATH_MAX + 1]; - const char *repodir = NULL; - int i, ret = 0; - - if (pledge("stdio rpath", NULL) == -1) - err(1, "pledge"); - - git_libgit2_init(); - - writeheader(stdout); - - for (i = 1; i < argc; i++) { - if (argv[i][0] == '-') { - if (argv[i][1] != 'b' || i + 1 >= argc) - usage(argv[0]); - relpath = argv[++i]; - continue; - } - - repodir = argv[i]; - if (!realpath(repodir, repodirabs)) - err(1, "realpath"); - - if (git_repository_open_ext(&repo, repodir, - GIT_REPOSITORY_OPEN_NO_SEARCH, NULL)) { - e = giterr_last(); - fprintf(stderr, "%s: %s\n", argv[0], e->message); - ret = 1; - continue; - } - - /* use directory name as name */ - if ((name = strrchr(repodirabs, '/'))) - name++; - else - name = ""; - - /* read description or .git/description */ - joinpath(path, sizeof(path), repodir, "description"); - if (!(fp = fopen(path, "r"))) { - joinpath(path, sizeof(path), repodir, ".git/description"); - fp = fopen(path, "r"); - } - description[0] = '\0'; - if (fp) { - if (!fgets(description, sizeof(description), fp)) - description[0] = '\0'; - fclose(fp); - } - - writelog(stdout); - } - writefooter(stdout); - - /* cleanup */ - git_repository_free(repo); - git_libgit2_shutdown(); - - return ret; -} diff --git a/stagit.1 b/stagit.1 @@ -1,95 +0,0 @@ -.Dd May 1, 2016 -.Dt STAGIT 1 -.Os -.Sh NAME -.Nm stagit -.Nd static git page generator -.Sh SYNOPSIS -.Nm -.Op Fl c Ar cachefile -.Ar repodir -.Sh DESCRIPTION -.Nm -writes HTML pages for the repository -.Ar repodir -to the current directory. -.Pp -The options are as follows: -.Bl -tag -width Ds -.It Fl c Ar cachefile -Cache the entries of the log page up to the point of -the last commit. -The -.Ar cachefile -will store the last commit id and the entries in the HTML table. -It is up to the user to make sure the state of the -.Ar cachefile -is in sync with the history of the repository. -.El -.Pp -The following files will be written: -.Bl -tag -width Ds -.It atom.xml -Atom XML feed -.It files.html -List of files in the latest tree, linking to the file. -.It log.html -List of commits in order of most recent to old of the commits (top to bottom), -each commit links to a page with a diffstat and diff of the commit. -.It refs.html -Lists references of the repository such as branches and tags. -.El -.Pp -For each entry in HEAD a file will be written in the format: -file/filepath.html. -This file will contain the textual data of the file prefixed by line numbers. -The file will have the string "Binary file" if the data is considered to be -non-textual. -.Pp -For each commit a file will be written in the format: -commit/commitid.html. -This file will contain the diffstat and diff of the commit. -It will write the string "Binary files differ" if the data is considered to -be non-textual. -Too large diffs will be suppressed and a string -"Diff is too large, output suppressed" will be written. -.Pp -When a commit HTML file exists it won't be overwritten again, note that if -you've changed -.Nm -or changed one of the metadata files of the repository it is recommended to -recreate all the output files because it will contain old data. -To do this remove the output directory and -.Ar cachefile , -then recreate the files. -.Pp -The basename of the directory is used as the repository name. -The suffix ".git" is removed from the basename, this suffix is commonly used -for "bare" repos. -.Pp -The content of the follow files specifies the metadata for each repository: -.Bl -tag -width Ds -.It .git/description or description (bare repo). -description -.It .git/owner or owner (bare repo). -owner of repository -.It .git/url or url (bare repo). -primary clone url of the repository, for example: git://git.2f30.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 menu is made. -.Pp -For changing the style of the page you can use the following files: -.Bl -tag -width Ds -.It favicon.png -favicon image. -.It logo.png -32x32 logo. -.It style.css -CSS stylesheet. -.El -.Sh SEE ALSO -.Xr stagit-index 1 -.Sh AUTHORS -.An Hiltjo Posthuma Aq Mt hiltjo@codemadness.org diff --git a/stagit.c b/stagit.c @@ -1,1243 +0,0 @@ -#include <sys/stat.h> - -#include <err.h> -#include <errno.h> -#include <inttypes.h> -#include <libgen.h> -#include <limits.h> -#include <stdio.h> -#include <stdlib.h> -#include <string.h> -#include <unistd.h> -#include <wchar.h> - -#include <git2.h> - -#include "compat.h" - -struct deltainfo { - git_patch *patch; - - size_t addcount; - size_t delcount; -}; - -struct commitinfo { - const git_oid *id; - - char oid[GIT_OID_HEXSZ + 1]; - char parentoid[GIT_OID_HEXSZ + 1]; - - const git_signature *author; - const git_signature *committer; - const char *summary; - const char *msg; - - git_diff *diff; - git_commit *commit; - git_commit *parent; - git_tree *commit_tree; - git_tree *parent_tree; - - size_t addcount; - size_t delcount; - size_t filecount; - - struct deltainfo **deltas; - size_t ndeltas; -}; - -static git_repository *repo; - -static const char *relpath = ""; -static const char *repodir; - -static char *name = ""; -static char *strippedname = ""; -static char description[255]; -static char cloneurl[1024]; -static int haslicense, hasreadme, hassubmodules; - -/* cache */ -static git_oid lastoid; -static char lastoidstr[GIT_OID_HEXSZ + 2]; /* id + newline + nul byte */ -static FILE *rcachefp, *wcachefp; -static const char *cachefile; - -#ifndef USE_PLEDGE -#define pledge(p1,p2) 0 -#endif - -#define ISUTF8(c) (((c) & 0xc0) != 0x80) - -/* print `len' columns of characters. If string is shorter pad the rest - * with characters `pad`. */ -void -printutf8pad(FILE *fp, const char *s, size_t len, int pad) -{ - wchar_t w; - size_t n = 0, i; - int r; - - for (i = 0; *s && n < len; i++, s++) { - if (ISUTF8(*s)) { - if ((r = mbtowc(&w, s, 4)) == -1) - break; - if ((r = wcwidth(w)) == -1) - r = 1; - n += (size_t)r; - } - putc(*s, fp); - } - for (; n < len; n++) - putc(pad, fp); -} - -void -joinpath(char *buf, size_t bufsiz, const char *path, const char *path2) -{ - int r; - - r = snprintf(buf, bufsiz, "%s%s%s", - path, path[0] && path[strlen(path) - 1] != '/' ? "/" : "", path2); - if (r == -1 || (size_t)r >= bufsiz) - errx(1, "path truncated: '%s%s%s'", - path, path[0] && path[strlen(path) - 1] != '/' ? "/" : "", path2); -} - -void -deltainfo_free(struct deltainfo *di) -{ - if (!di) - return; - git_patch_free(di->patch); - di->patch = NULL; - free(di); -} - -int -commitinfo_getstats(struct commitinfo *ci) -{ - struct deltainfo *di; - const git_diff_delta *delta; - const git_diff_hunk *hunk; - const git_diff_line *line; - git_patch *patch = NULL; - size_t ndeltas, nhunks, nhunklines; - size_t i, j, k; - - ndeltas = git_diff_num_deltas(ci->diff); - if (ndeltas && !(ci->deltas = calloc(ndeltas, sizeof(struct deltainfo *)))) - err(1, "calloc"); - - for (i = 0; i < ndeltas; i++) { - if (git_patch_from_diff(&patch, ci->diff, i)) - goto err; - if (!(di = calloc(1, sizeof(struct deltainfo)))) - err(1, "calloc"); - di->patch = patch; - ci->deltas[i] = di; - - delta = git_patch_get_delta(patch); - - /* skip stats for binary data */ - if (delta->flags & GIT_DIFF_FLAG_BINARY) - continue; - - nhunks = git_patch_num_hunks(patch); - for (j = 0; j < nhunks; j++) { - if (git_patch_get_hunk(&hunk, &nhunklines, patch, j)) - break; - for (k = 0; ; k++) { - if (git_patch_get_line_in_hunk(&line, patch, j, k)) - break; - if (line->old_lineno == -1) { - di->addcount++; - ci->addcount++; - } else if (line->new_lineno == -1) { - di->delcount++; - ci->delcount++; - } - } - } - } - ci->ndeltas = i; - ci->filecount = i; - - return 0; - -err: - if (ci->deltas) - for (i = 0; i < ci->ndeltas; i++) - deltainfo_free(ci->deltas[i]); - free(ci->deltas); - ci->deltas = NULL; - ci->ndeltas = 0; - ci->addcount = 0; - ci->delcount = 0; - ci->filecount = 0; - - return -1; -} - -void -commitinfo_free(struct commitinfo *ci) -{ - size_t i; - - if (!ci) - return; - if (ci->deltas) - for (i = 0; i < ci->ndeltas; i++) - deltainfo_free(ci->deltas[i]); - free(ci->deltas); - ci->deltas = NULL; - git_diff_free(ci->diff); - git_tree_free(ci->commit_tree); - git_tree_free(ci->parent_tree); - git_commit_free(ci->commit); - git_commit_free(ci->parent); - free(ci); -} - -struct commitinfo * -commitinfo_getbyoid(const git_oid *id) -{ - struct commitinfo *ci; - git_diff_options opts; - - if (!(ci = calloc(1, sizeof(struct commitinfo)))) - err(1, "calloc"); - - if (git_commit_lookup(&(ci->commit), repo, id)) - goto err; - ci->id = id; - - git_oid_tostr(ci->oid, sizeof(ci->oid), git_commit_id(ci->commit)); - git_oid_tostr(ci->parentoid, sizeof(ci->parentoid), git_commit_parent_id(ci->commit, 0)); - - ci->author = git_commit_author(ci->commit); - ci->committer = git_commit_committer(ci->commit); - ci->summary = git_commit_summary(ci->commit); - ci->msg = git_commit_message(ci->commit); - - if (git_tree_lookup(&(ci->commit_tree), repo, git_commit_tree_id(ci->commit))) - goto err; - if (!git_commit_parent(&(ci->parent), ci->commit, 0)) { - if (git_tree_lookup(&(ci->parent_tree), repo, git_commit_tree_id(ci->parent))) { - ci->parent = NULL; - ci->parent_tree = NULL; - } - } - - git_diff_init_options(&opts, GIT_DIFF_OPTIONS_VERSION); - opts.flags |= GIT_DIFF_DISABLE_PATHSPEC_MATCH; - if (git_diff_tree_to_tree(&(ci->diff), repo, ci->parent_tree, ci->commit_tree, &opts)) - goto err; - if (commitinfo_getstats(ci) == -1) - goto err; - - return ci; - -err: - commitinfo_free(ci); - - return NULL; -} - -FILE * -efopen(const char *name, const char *flags) -{ - FILE *fp; - - if (!(fp = fopen(name, flags))) - err(1, "fopen"); - - return fp; -} - -/* Escape characters below as HTML 2.0 / XML 1.0. */ -void -xmlencode(FILE *fp, const char *s, size_t len) -{ - size_t i; - - for (i = 0; *s && i < len; s++, i++) { - switch(*s) { - case '<': fputs("&lt;", fp); break; - case '>': fputs("&gt;", fp); break; - case '\'': fputs("&#39;", fp); break; - case '&': fputs("&amp;", fp); break; - case '"': fputs("&quot;", fp); break; - default: fputc(*s, fp); - } - } -} - -void -trim(char *buf, size_t bufsiz, const char *src) -{ - size_t d = 0, i, len, s; - - len = strlen(src); - for (s = 0; s < len && d < bufsiz - 1; s++) { - switch (src[s]) { - case '\t': - if (d + 8 >= bufsiz - 1) - goto end; - for (i = 0; i < 8; i++) - buf[d++] = ' '; - break; - case '|': - case '\n': - case '\r': - buf[d++] = ' '; - break; - default: - buf[d++] = src[s]; - break; - } - } -end: - buf[d] = '\0'; -} - -/* Escape characters in text in geomyidae .gph format */ -void -gphtext(FILE *fp, const char *s, size_t len) -{ - size_t i, n = 0; - - for (i = 0; *s && i < len; i++) { - if (s[i] == '\n') - n = 0; - - /* escape 't' at the start of a line */ - if (!n && s[i] == 't') { - fputc('t', fp); - n = 1; - } - - switch (s[i]) { - case '\r': break; - case '\t': fputs(" ", fp); break; - default: fputc(s[i], fp); - } - n++; - } -} - -/* Escape characters in links in geomyidae .gph format */ -void -gphlink(FILE *fp, const char *s, size_t len) -{ - size_t i; - - for (i = 0; *s && i < len; i++) { - switch (s[i]) { - case '\n': - /* in this context replace newline with space */ - fputc(' ', fp); - break; - case '\r': /* ignore CR */ - case '|': /* ignore separators for now */ - break; - case '\t': - fputs(" ", fp); - break; - default: - fputc(s[i], fp); - break; - } - } -} - -int -mkdirp(const char *path) -{ - char tmp[PATH_MAX], *p; - - if (strlcpy(tmp, path, sizeof(tmp)) >= sizeof(tmp)) - errx(1, "path truncated: '%s'", path); - for (p = tmp + (tmp[0] == '/'); *p; p++) { - if (*p != '/') - continue; - *p = '\0'; - if (mkdir(tmp, S_IRWXU | S_IRWXG | S_IRWXO) < 0 && errno != EEXIST) - return -1; - *p = '/'; - } - if (mkdir(tmp, S_IRWXU | S_IRWXG | S_IRWXO) < 0 && errno != EEXIST) - return -1; - return 0; -} - -void -printtimez(FILE *fp, const git_time *intime) -{ - struct tm *intm; - time_t t; - char out[32]; - - t = (time_t)intime->time; - if (!(intm = gmtime(&t))) - return; - strftime(out, sizeof(out), "%Y-%m-%dT%H:%M:%SZ", intm); - fputs(out, fp); -} - -void -printtime(FILE *fp, const git_time *intime) -{ - struct tm *intm; - time_t t; - char out[32]; - - t = (time_t)intime->time + (intime->offset * 60); - if (!(intm = gmtime(&t))) - return; - strftime(out, sizeof(out), "%a, %e %b %Y %H:%M:%S", intm); - if (intime->offset < 0) - fprintf(fp, "%s -%02d%02d", out, - -(intime->offset) / 60, -(intime->offset) % 60); - else - fprintf(fp, "%s +%02d%02d", out, - intime->offset / 60, intime->offset % 60); -} - -void -printtimeshort(FILE *fp, const git_time *intime) -{ - struct tm *intm; - time_t t; - char out[32]; - - t = (time_t)intime->time; - if (!(intm = gmtime(&t))) - return; - strftime(out, sizeof(out), "%Y-%m-%d %H:%M", intm); - fputs(out, fp); -} - -void -writeheader(FILE *fp, const char *title) -{ - gphtext(fp, title, strlen(title)); - if (title[0] && strippedname[0]) - fputs(" - ", fp); - gphtext(fp, strippedname, strlen(strippedname)); - if (description[0]) - fputs(" - ", fp); - gphtext(fp, description, strlen(description)); - fputs("\n", fp); - if (cloneurl[0]) { - fputs("[h|git clone ", fp); - gphlink(fp, cloneurl, strlen(cloneurl)); - fputs("|URL:", fp); - gphlink(fp, cloneurl, strlen(cloneurl)); - fputs("|server|port]\n", fp); - } - fprintf(fp, "[1|Log|%slog.gph|server|port]\n", relpath); - fprintf(fp, "[1|Files|%sfiles.gph|server|port]\n", relpath); - fprintf(fp, "[1|Refs|%srefs.gph|server|port]\n", relpath); - if (hassubmodules) - fprintf(fp, "[1|Submodules|%sfile/.gitmodules.gph|server|port]\n", relpath); - if (hasreadme) - fprintf(fp, "[1|README|%sfile/README.gph|server|port]\n", relpath); - if (haslicense) - fprintf(fp, "[1|LICENSE|%sfile/LICENSE.gph|server|port]\n", relpath); - fputs("---\n", fp); -} - -void -writefooter(FILE *fp) -{ -} - -int -writeblobgph(FILE *fp, const git_blob *blob) -{ - size_t n = 0, i, j, prev; - const char *nfmt = "%6d "; - const char *s = git_blob_rawcontent(blob); - git_off_t len = git_blob_rawsize(blob); - - if (len > 0) { - for (i = 0, prev = 0; i < (size_t)len; i++) { - if (s[i] != '\n') - continue; - n++; - fprintf(fp, nfmt, n, n, n); - for (j = prev; s[j] && j <= i; j++) { - switch (s[j]) { - case '\r': break; - case '\t': fputs(" ", fp); break; - default: fputc(s[j], fp); - } - } - prev = i + 1; - } - /* trailing data */ - if ((len - prev) > 0) { - n++; - fprintf(fp, nfmt, n, n, n); - for (j = prev; s[j] && j < len - prev; j++) { - switch (s[j]) { - case '\r': break; - case '\t': fputs(" ", fp); break; - default: fputc(s[j], fp); - } - } - } - } - - return n; -} - -void -printcommit(FILE *fp, struct commitinfo *ci) -{ - fprintf(fp, "[1|commit %s|%scommit/%s.gph|server|port]\n", - ci->oid, relpath, ci->oid); - - if (ci->parentoid[0]) - fprintf(fp, "[1|parent %s|%scommit/%s.gph|server|port]\n", - ci->parentoid, relpath, ci->parentoid); - - if (ci->author) { - fputs("[h|Author: ", fp); - gphlink(fp, ci->author->name, strlen(ci->author->name)); - fputs(" <", fp); - gphlink(fp, ci->author->email, strlen(ci->author->email)); - fputs(">|URL:mailto:", fp); - gphlink(fp, ci->author->email, strlen(ci->author->email)); - fputs("|server|port]\n", fp); - fputs("Date: ", fp); - printtime(fp, &(ci->author->when)); - fputc('\n', fp); - } - if (ci->msg) { - fputc('\n', fp); - gphtext(fp, ci->msg, strlen(ci->msg)); - fputc('\n', fp); - } -} - -void -printshowfile(FILE *fp, struct commitinfo *ci) -{ - const git_diff_delta *delta; - const git_diff_hunk *hunk; - const git_diff_line *line; - git_patch *patch; - size_t nhunks, nhunklines, changed, add, del, total, i, j, k; - char linestr[80]; - - printcommit(fp, ci); - - if (!ci->deltas) - return; - - if (ci->filecount > 1000 || - ci->ndeltas > 1000 || - ci->addcount > 100000 || - ci->delcount > 100000) { - fputs("\nDiff is too large, output suppressed.\n", fp); - return; - } - - /* diff stat */ - fputs("Diffstat:\n", fp); - for (i = 0; i < ci->ndeltas; i++) { - delta = git_patch_get_delta(ci->deltas[i]->patch); - /* TODO: make file linkable */ - gphtext(fp, delta->old_file.path, strlen(delta->old_file.path)); - if (strcmp(delta->old_file.path, delta->new_file.path)) { - fputs(" -> ", fp); - gphtext(fp, delta->new_file.path, strlen(delta->new_file.path)); - } - - add = ci->deltas[i]->addcount; - del = ci->deltas[i]->delcount; - changed = add + del; - total = sizeof(linestr) - 2; - if (changed > total) { - if (add) - add = ((float)total / changed * add) + 1; - if (del) - del = ((float)total / changed * del) + 1; - } - memset(&linestr, '+', add); - memset(&linestr[add], '-', del); - - fprintf(fp, " | %zu ", - ci->deltas[i]->addcount + ci->deltas[i]->delcount); - fwrite(&linestr, 1, add, fp); - fwrite(&linestr[add], 1, del, fp); - fputs("\n", fp); - } - fprintf(fp, "\n%zu file%s changed, %zu insertion%s(+), %zu deletion%s(-)\n", - ci->filecount, ci->filecount == 1 ? "" : "s", - ci->addcount, ci->addcount == 1 ? "" : "s", - ci->delcount, ci->delcount == 1 ? "" : "s"); - - fputs("---\n", fp); - - for (i = 0; i < ci->ndeltas; i++) { - patch = ci->deltas[i]->patch; - delta = git_patch_get_delta(patch); - /* NOTE: only links to new path */ - fprintf(fp, "[1|diff --git a/%s b/%s", - delta->old_file.path, delta->new_file.path); - fprintf(fp, "|%sfile/%s.gph|server|port]\n", relpath, delta->new_file.path); - - /* check binary data */ - if (delta->flags & GIT_DIFF_FLAG_BINARY) { - fputs("Binary files differ.\n", fp); - continue; - } - - nhunks = git_patch_num_hunks(patch); - for (j = 0; j < nhunks; j++) { - if (git_patch_get_hunk(&hunk, &nhunklines, patch, j)) - break; - - gphtext(fp, hunk->header, hunk->header_len); - - for (k = 0; ; k++) { - if (git_patch_get_line_in_hunk(&line, patch, j, k)) - break; - if (line->old_lineno == -1) - fputs("+", fp); - else if (line->new_lineno == -1) - fputs("-", fp); - else - fputs(" ", fp); - gphtext(fp, line->content, line->content_len); - } - } - } -} - -void -writelogline(FILE *fp, struct commitinfo *ci) -{ - char buf[1024]; - - fputs("[1|", fp); - if (ci->author) - printtimeshort(fp, &(ci->author->when)); - fputs(" ", fp); - if (ci->summary) { - trim(buf, sizeof(buf), ci->summary); - printutf8pad(fp, buf, 50, ' '); - } - fputs(" ", fp); - if (ci->author) { - trim(buf, sizeof(buf), ci->author->name); - printutf8pad(fp, buf, 25, ' '); - } - fprintf(fp, "|%scommit/%s.gph", relpath, ci->oid); - fputs("|server|port]\n", fp); -} - -int -writelog(FILE *fp, const git_oid *oid) -{ - struct commitinfo *ci; - git_revwalk *w = NULL; - git_oid id; - char path[PATH_MAX]; - FILE *fpfile; - int r; - - git_revwalk_new(&w, repo); - git_revwalk_push(w, oid); - git_revwalk_sorting(w, GIT_SORT_TIME); - git_revwalk_simplify_first_parent(w); - - while (!git_revwalk_next(&id, w)) { - if (cachefile && !memcmp(&id, &lastoid, sizeof(id))) - break; - if (!(ci = commitinfo_getbyoid(&id))) - break; - - writelogline(fp, ci); - if (cachefile) - writelogline(wcachefp, ci); - - r = snprintf(path, sizeof(path), "commit/%s.gph", ci->oid); - if (r == -1 || (size_t)r >= sizeof(path)) - errx(1, "path truncated: 'commit/%s.gph'", ci->oid); - - /* check if file exists if so skip it */ - if (access(path, F_OK)) { - fpfile = efopen(path, "w"); - writeheader(fpfile, ci->summary); - printshowfile(fpfile, ci); - writefooter(fpfile); - fclose(fpfile); - } - commitinfo_free(ci); - } - git_revwalk_free(w); - - return 0; -} - -void -printcommitatom(FILE *fp, struct commitinfo *ci) -{ - fputs("<entry>\n", fp); - - fprintf(fp, "<id>%s</id>\n", ci->oid); - if (ci->author) { - fputs("<published>", fp); - printtimez(fp, &(ci->author->when)); - fputs("</published>\n", fp); - } - if (ci->committer) { - fputs("<updated>", fp); - printtimez(fp, &(ci->committer->when)); - fputs("</updated>\n", fp); - } - if (ci->summary) { - fputs("<title type=\"text\">", fp); - xmlencode(fp, ci->summary, strlen(ci->summary)); - fputs("</title>\n", fp); - } - fprintf(fp, "<link rel=\"alternate\" type=\"text/html\" href=\"commit/%s.gph\" />", - ci->oid); - - if (ci->author) { - fputs("<author><name>", fp); - xmlencode(fp, ci->author->name, strlen(ci->author->name)); - fputs("</name>\n<email>", fp); - xmlencode(fp, ci->author->email, strlen(ci->author->email)); - fputs("</email>\n</author>\n", fp); - } - - fputs("<content type=\"text\">", fp); - fprintf(fp, "commit %s\n", ci->oid); - if (ci->parentoid[0]) - fprintf(fp, "parent %s\n", ci->parentoid); - if (ci->author) { - fputs("Author: ", fp); - xmlencode(fp, ci->author->name, strlen(ci->author->name)); - fputs(" &lt;", fp); - xmlencode(fp, ci->author->email, strlen(ci->author->email)); - fputs("&gt;\nDate: ", fp); - printtime(fp, &(ci->author->when)); - fputc('\n', fp); - } - if (ci->msg) { - fputc('\n', fp); - xmlencode(fp, ci->msg, strlen(ci->msg)); - } - fputs("\n</content>\n</entry>\n", fp); -} - -int -writeatom(FILE *fp) -{ - struct commitinfo *ci; - git_revwalk *w = NULL; - git_oid id; - size_t i, m = 100; /* last 'm' commits */ - - fputs("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" - "<feed xmlns=\"http://www.w3.org/2005/Atom\">\n<title>", fp); - xmlencode(fp, strippedname, strlen(strippedname)); - fputs(", branch HEAD</title>\n<subtitle>", fp); - xmlencode(fp, description, strlen(description)); - fputs("</subtitle>\n", fp); - - git_revwalk_new(&w, repo); - git_revwalk_push_head(w); - git_revwalk_sorting(w, GIT_SORT_TIME); - git_revwalk_simplify_first_parent(w); - - for (i = 0; i < m && !git_revwalk_next(&id, w); i++) { - if (!(ci = commitinfo_getbyoid(&id))) - break; - printcommitatom(fp, ci); - commitinfo_free(ci); - } - git_revwalk_free(w); - - fputs("</feed>\n", fp); - - return 0; -} - -int -writeblob(git_object *obj, const char *fpath, const char *filename, git_off_t filesize) -{ - char tmp[PATH_MAX] = "", *d; - int lc = 0; - FILE *fp; - - if (strlcpy(tmp, fpath, sizeof(tmp)) >= sizeof(tmp)) - errx(1, "path truncated: '%s'", fpath); - if (!(d = dirname(tmp))) - err(1, "dirname"); - if (mkdirp(d)) - return -1; - - fp = efopen(fpath, "w"); - writeheader(fp, filename); - gphtext(fp, filename, strlen(filename)); - fprintf(fp, " (%juB)\n", (uintmax_t)filesize); - fputs("---\n", fp); - - if (git_blob_is_binary((git_blob *)obj)) { - fputs("Binary file.\n", fp); - } else { - lc = writeblobgph(fp, (git_blob *)obj); - if (ferror(fp)) - err(1, "fwrite"); - } - writefooter(fp); - fclose(fp); - - return lc; -} - -const char * -filemode(git_filemode_t m) -{ - static char mode[11]; - - memset(mode, '-', sizeof(mode) - 1); - mode[10] = '\0'; - - if (S_ISREG(m)) - mode[0] = '-'; - else if (S_ISBLK(m)) - mode[0] = 'b'; - else if (S_ISCHR(m)) - mode[0] = 'c'; - else if (S_ISDIR(m)) - mode[0] = 'd'; - else if (S_ISFIFO(m)) - mode[0] = 'p'; - else if (S_ISLNK(m)) - mode[0] = 'l'; - else if (S_ISSOCK(m)) - mode[0] = 's'; - else - mode[0] = '?'; - - if (m & S_IRUSR) mode[1] = 'r'; - if (m & S_IWUSR) mode[2] = 'w'; - if (m & S_IXUSR) mode[3] = 'x'; - if (m & S_IRGRP) mode[4] = 'r'; - if (m & S_IWGRP) mode[5] = 'w'; - if (m & S_IXGRP) mode[6] = 'x'; - if (m & S_IROTH) mode[7] = 'r'; - if (m & S_IWOTH) mode[8] = 'w'; - if (m & S_IXOTH) mode[9] = 'x'; - - if (m & S_ISUID) mode[3] = (mode[3] == 'x') ? 's' : 'S'; - if (m & S_ISGID) mode[6] = (mode[6] == 'x') ? 's' : 'S'; - if (m & S_ISVTX) mode[9] = (mode[9] == 'x') ? 't' : 'T'; - - return mode; -} - -int -writefilestree(FILE *fp, git_tree *tree, const char *path) -{ - const git_tree_entry *entry = NULL; - git_submodule *module = NULL; - git_object *obj = NULL; - git_off_t filesize; - const char *entryname; - char filepath[PATH_MAX], entrypath[PATH_MAX]; - char buf[1024]; - size_t count, i; - int lc, r, ret; - - count = git_tree_entrycount(tree); - for (i = 0; i < count; i++) { - if (!(entry = git_tree_entry_byindex(tree, i)) || - !(entryname = git_tree_entry_name(entry))) - return -1; - joinpath(entrypath, sizeof(entrypath), path, entryname); - - r = snprintf(filepath, sizeof(filepath), "file/%s.gph", - entrypath); - if (r == -1 || (size_t)r >= sizeof(filepath)) - errx(1, "path truncated: 'file/%s.gph'", entrypath); - - if (!git_tree_entry_to_object(&obj, repo, entry)) { - switch (git_object_type(obj)) { - case GIT_OBJ_BLOB: - break; - case GIT_OBJ_TREE: - /* NOTE: recurses */ - ret = writefilestree(fp, (git_tree *)obj, - entrypath); - git_object_free(obj); - if (ret) - return ret; - continue; - default: - git_object_free(obj); - continue; - } - - filesize = git_blob_rawsize((git_blob *)obj); - lc = writeblob(obj, filepath, entryname, filesize); - - fputs("[1|", fp); - fputs(filemode(git_tree_entry_filemode(entry)), fp); - fputs(" ", fp); - trim(buf, sizeof(buf), entrypath); - printutf8pad(fp, buf, 50, ' '); - fputs(" ", fp); - if (lc > 0) - fprintf(fp, "%7dL", lc); - else - fprintf(fp, "%7juB", (uintmax_t)filesize); - fprintf(fp, "|%s%s", relpath, filepath); - fputs("|server|port]\n", fp); - git_object_free(obj); - } else if (!git_submodule_lookup(&module, repo, entryname)) { - fputs("[1|m--------- ", fp); - trim(buf, sizeof(buf), entrypath); - printutf8pad(fp, buf, 50, ' '); - fprintf(fp, "|%sfile/.gitmodules.gph|server|port]\n", relpath); - /* NOTE: linecount omitted */ - git_submodule_free(module); - } - } - - return 0; -} - -int -writefiles(FILE *fp, const git_oid *id) -{ - git_tree *tree = NULL; - git_commit *commit = NULL; - int ret = -1; - - fprintf(fp, "%-10.10s ", "Mode"); - fprintf(fp, "%-50.50s ", "Name"); - fprintf(fp, "%8.8s\n", "Size"); - - if (!git_commit_lookup(&commit, repo, id) && - !git_commit_tree(&tree, commit)) - ret = writefilestree(fp, tree, ""); - - git_commit_free(commit); - git_tree_free(tree); - - return ret; -} - -int -refs_cmp(const void *v1, const void *v2) -{ - git_reference *r1 = (*(git_reference **)v1); - git_reference *r2 = (*(git_reference **)v2); - int r; - - if ((r = git_reference_is_branch(r1) - git_reference_is_branch(r2))) - return r; - - return strcmp(git_reference_shorthand(r1), - git_reference_shorthand(r2)); -} - -int -writerefs(FILE *fp) -{ - struct commitinfo *ci; - const git_oid *id = NULL; - git_object *obj = NULL; - git_reference *dref = NULL, *r, *ref = NULL; - git_reference_iterator *it = NULL; - git_reference **refs = NULL; - size_t count, i, j, refcount; - const char *titles[] = { "Branches", "Tags" }; - const char *name; - char buf[1024]; - - if (git_reference_iterator_new(&it, repo)) - return -1; - - for (refcount = 0; !git_reference_next(&ref, it); refcount++) { - if (!(refs = reallocarray(refs, refcount + 1, sizeof(git_reference *)))) - err(1, "realloc"); - refs[refcount] = ref; - } - git_reference_iterator_free(it); - - /* sort by type then shorthand name */ - qsort(refs, refcount, sizeof(git_reference *), refs_cmp); - - for (j = 0; j < 2; j++) { - for (i = 0, count = 0; i < refcount; i++) { - if (!(git_reference_is_branch(refs[i]) && j == 0) && - !(git_reference_is_tag(refs[i]) && j == 1)) - continue; - - switch (git_reference_type(refs[i])) { - case GIT_REF_SYMBOLIC: - if (git_reference_resolve(&dref, refs[i])) - goto err; - r = dref; - break; - case GIT_REF_OID: - r = refs[i]; - break; - default: - continue; - } - if (!git_reference_target(r) || - git_reference_peel(&obj, r, GIT_OBJ_ANY)) - goto err; - if (!(id = git_object_id(obj))) - goto err; - if (!(ci = commitinfo_getbyoid(id))) - break; - - /* print header if it has an entry (first). */ - if (++count == 1) { - fprintf(fp, "%s\n", titles[j]); - fprintf(fp, " %-20.20s", "Name"); - fprintf(fp, " %-16.16s", "Last commit date"); - fprintf(fp, " %-25.25s\n", "Author"); - } - - name = git_reference_shorthand(r); - - fputs(" ", fp); - trim(buf, sizeof(buf), name); - printutf8pad(fp, buf, 20, ' '); - fputs(" ", fp); - if (ci->author) - printtimeshort(fp, &(ci->author->when)); - fputs(" ", fp); - if (ci->author) { - trim(buf, sizeof(buf), ci->author->name); - printutf8pad(fp, buf, 25, ' '); - } - fputs("\n", fp); - - commitinfo_free(ci); - git_object_free(obj); - obj = NULL; - git_reference_free(dref); - dref = NULL; - } - /* table footer */ - if (count) - fputs("\n", fp); - } - -err: - git_object_free(obj); - git_reference_free(dref); - - for (i = 0; i < refcount; i++) - git_reference_free(refs[i]); - free(refs); - - return 0; -} - -void -usage(char *argv0) -{ - fprintf(stderr, "%s [-c cachefile] repodir\n", argv0); - exit(1); -} - -/* TODO: add base argument, gopher does not support relative urls, document it too */ -int -main(int argc, char *argv[]) -{ - git_object *obj = NULL; - const git_oid *head = NULL; - const git_error *e = NULL; - FILE *fp, *fpread; - char path[PATH_MAX], repodirabs[PATH_MAX + 1], *p; - char tmppath[64] = "cache.XXXXXXXXXXXX", buf[BUFSIZ]; - size_t n; - int i, fd; - - if (pledge("stdio rpath wpath cpath", NULL) == -1) - err(1, "pledge"); - - for (i = 1; i < argc; i++) { - if (argv[i][0] != '-') { - if (repodir) - usage(argv[0]); - repodir = argv[i]; - } else if (argv[i][1] == 'c') { - if (i + 1 >= argc) - usage(argv[0]); - cachefile = argv[++i]; - } else if (argv[i][1] == 'b') { - if (i + 1 >= argc) - usage(argv[0]); - relpath = argv[++i]; - } - } - if (!repodir) - usage(argv[0]); - - if (!realpath(repodir, repodirabs)) - err(1, "realpath"); - - git_libgit2_init(); - - if (git_repository_open_ext(&repo, repodir, - GIT_REPOSITORY_OPEN_NO_SEARCH, NULL) < 0) { - e = giterr_last(); - fprintf(stderr, "%s: %s\n", argv[0], e->message); - return 1; - } - - /* find HEAD */ - if (!git_revparse_single(&obj, repo, "HEAD")) - head = git_object_id(obj); - git_object_free(obj); - - /* don't cache if there is no HEAD */ - if (!head) - cachefile = NULL; - - /* use directory name as name */ - if ((name = strrchr(repodirabs, '/'))) - name++; - else - name = ""; - - /* strip .git suffix */ - if (!(strippedname = strdup(name))) - err(1, "strdup"); - if ((p = strrchr(strippedname, '.'))) - if (!strcmp(p, ".git")) - *p = '\0'; - - /* read description or .git/description */ - joinpath(path, sizeof(path), repodir, "description"); - if (!(fpread = fopen(path, "r"))) { - joinpath(path, sizeof(path), repodir, ".git/description"); - fpread = fopen(path, "r"); - } - if (fpread) { - if (!fgets(description, sizeof(description), fpread)) - description[0] = '\0'; - fclose(fpread); - } - - /* read url or .git/url */ - joinpath(path, sizeof(path), repodir, "url"); - if (!(fpread = fopen(path, "r"))) { - joinpath(path, sizeof(path), repodir, ".git/url"); - fpread = fopen(path, "r"); - } - if (fpread) { - if (!fgets(cloneurl, sizeof(cloneurl), fpread)) - cloneurl[0] = '\0'; - cloneurl[strcspn(cloneurl, "\n")] = '\0'; - fclose(fpread); - } - - /* check LICENSE */ - haslicense = (!git_revparse_single(&obj, repo, "HEAD:LICENSE") && - git_object_type(obj) == GIT_OBJ_BLOB); - git_object_free(obj); - - /* check README */ - hasreadme = (!git_revparse_single(&obj, repo, "HEAD:README") && - git_object_type(obj) == GIT_OBJ_BLOB); - git_object_free(obj); - - hassubmodules = (!git_revparse_single(&obj, repo, "HEAD:.gitmodules") && - git_object_type(obj) == GIT_OBJ_BLOB); - git_object_free(obj); - - /* log for HEAD */ - fp = efopen("log.gph", "w"); - mkdir("commit", 0755); - writeheader(fp, "Log"); - fprintf(fp, "%-16.16s ", "Date"); - fprintf(fp, "%-50.50s ", "Commit message"); - fprintf(fp, "%-25.25s\n", "Author"); - - if (cachefile) { - /* read from cache file (does not need to exist) */ - if ((rcachefp = fopen(cachefile, "r"))) { - if (!fgets(lastoidstr, sizeof(lastoidstr), rcachefp)) - errx(1, "%s: no object id", cachefile); - if (git_oid_fromstr(&lastoid, lastoidstr)) - errx(1, "%s: invalid object id", cachefile); - } - - /* write log to (temporary) cache */ - if ((fd = mkstemp(tmppath)) == -1) - err(1, "mkstemp"); - if (!(wcachefp = fdopen(fd, "w"))) - err(1, "fdopen"); - /* write last commit id (HEAD) */ - git_oid_tostr(buf, sizeof(buf), head); - fprintf(wcachefp, "%s\n", buf); - - writelog(fp, head); - - if (rcachefp) { - /* append previous log to log.gph and the new cache */ - while (!feof(rcachefp)) { - n = fread(buf, 1, sizeof(buf), rcachefp); - if (ferror(rcachefp)) - err(1, "fread"); - if (fwrite(buf, 1, n, fp) != n || - fwrite(buf, 1, n, wcachefp) != n) - err(1, "fwrite"); - } - fclose(rcachefp); - } - fclose(wcachefp); - } else { - if (head) - writelog(fp, head); - } - writefooter(fp); - fclose(fp); - - /* files for HEAD */ - fp = efopen("files.gph", "w"); - writeheader(fp, "Files"); - if (head) - writefiles(fp, head); - writefooter(fp); - fclose(fp); - - /* summary page with branches and tags */ - fp = efopen("refs.gph", "w"); - writeheader(fp, "Refs"); - writerefs(fp); - writefooter(fp); - fclose(fp); - - /* Atom feed */ - fp = efopen("atom.xml", "w"); - writeatom(fp); - fclose(fp); - - /* rename new cache file on success */ - if (cachefile && rename(tmppath, cachefile)) - err(1, "rename: '%s' to '%s'", tmppath, cachefile); - - /* cleanup */ - git_repository_free(repo); - git_libgit2_shutdown(); - - return 0; -}