Changed Bitreich stagit-gopher to gemini protocol - stagit-gemini - Stagit for gemini protocol Openbsd
 (HTM) git clone git://thinkerwim.org/stagit-gemini.git
 (DIR) Log
 (DIR) Files
 (DIR) Refs
 (DIR) README
 (DIR) LICENSE
       ---
 (DIR) commit b961d515cd77c6f8326d4c7e3f635f6a00fe50bf
 (DIR) parent d8c4c2c3aa23b1237f15fc3ab539419c2368989e
 (HTM) Author: Wim Stockman <wim@thinkerwim.org>
       Date:   Wed,  8 Mar 2023 21:52:34 +0100
       
       Changed Bitreich stagit-gopher to gemini protocol
       
       Diffstat:
         M Makefile                            |      22 +++++++++++-----------
         M README                              |      24 ++++++++++++------------
         M example_create.sh                   |      14 +++++++-------
         M example_post-receive.sh             |      16 ++++++++--------
         A file                                |       8 ++++++++
         A stagit-gemini-index.1               |      43 ++++++++++++++++++++++++++++++
         A stagit-gemini-index.c               |     310 +++++++++++++++++++++++++++++++
         A stagit-gemini.1                     |     119 +++++++++++++++++++++++++++++++
         A stagit-gemini.c                     |    1475 ++++++++++++++++++++++++++++++
       
       9 files changed, 1993 insertions(+), 38 deletions(-)
       ---
 (DIR) diff --git a/Makefile b/Makefile
       @@ -1,6 +1,6 @@
        .POSIX:
        
       -NAME = stagit-gopher
       +NAME = stagit-gemini
        VERSION = 1.2
        
        # paths
       @@ -21,17 +21,17 @@ STAGIT_CPPFLAGS = -D_XOPEN_SOURCE=700 -D_DEFAULT_SOURCE -D_BSD_SOURCE
        #STAGIT_CFLAGS += -DGIT_OPT_SET_OWNER_VALIDATION=-1
        
        SRC = \
       -        stagit-gopher.c\
       -        stagit-gopher-index.c
       +        stagit-gemini.c\
       +        stagit-gemini-index.c
        COMPATSRC = \
                reallocarray.c\
                strlcpy.c
        BIN = \
       -        stagit-gopher\
       -        stagit-gopher-index
       +        stagit-gemini\
       +        stagit-gemini-index
        MAN1 = \
       -        stagit-gopher.1\
       -        stagit-gopher-index.1
       +        stagit-gemini.1\
       +        stagit-gemini-index.1
        DOC = \
                LICENSE\
                README
       @@ -65,11 +65,11 @@ dist:
        
        ${OBJ}: ${HDR}
        
       -stagit-gopher: stagit-gopher.o ${COMPATOBJ}
       -        ${CC} -o $@ stagit-gopher.o ${COMPATOBJ} ${STAGIT_LDFLAGS}
       +stagit-gemini: stagit-gemini.o ${COMPATOBJ}
       +        ${CC} -o $@ stagit-gemini.o ${COMPATOBJ} ${STAGIT_LDFLAGS}
        
       -stagit-gopher-index: stagit-gopher-index.o ${COMPATOBJ}
       -        ${CC} -o $@ stagit-gopher-index.o ${COMPATOBJ} ${STAGIT_LDFLAGS}
       +stagit-gemini-index: stagit-gemini-index.o ${COMPATOBJ}
       +        ${CC} -o $@ stagit-gemini-index.o ${COMPATOBJ} ${STAGIT_LDFLAGS}
        
        clean:
                rm -f ${BIN} ${OBJ} ${NAME}-${VERSION}.tar.gz
 (DIR) diff --git a/README b/README
       @@ -1,9 +1,9 @@
       -stagit-gopher
       +stagit-gemini
        -------------
        
       -static git page generator for gopher.
       +static git page generator for gemini.
        
       -This generates pages in the geomyidae .gph file format:
       +This generates pages in the geomyidae .gmi file format:
        
                http://git.r-36.net/geomyidae
        
       @@ -13,17 +13,17 @@ Usage
        
        Make files per repository:
        
       -        $ mkdir -p gphroot/gphrepo1 && cd gphroot/gphrepo1
       -        $ stagit-gopher path/to/gitrepo1
       +        $ mkdir -p gmiroot/gmirepo1 && cd gmiroot/gmirepo1
       +        $ stagit-gemini path/to/gitrepo1
                repeat for other repositories
                $ ...
        
        Make index file for repositories:
        
       -        $ cd gphroot
       -        $ stagit-gopher-index path/to/gitrepo1 \
       +        $ cd gmiroot
       +        $ stagit-gemini-index path/to/gitrepo1 \
                               path/to/gitrepo2 \
       -                       path/to/gitrepo3 > index.gph
       +                       path/to/gitrepo3 > index.gmi
        
        
        Build and install
       @@ -39,14 +39,14 @@ Dependencies
        - C compiler (C99).
        - libc (tested with OpenBSD, FreeBSD, NetBSD, Linux: glibc and musl).
        - libgit2 (v0.22+).
       -- geomyidae (for .gph file serving).
       +- geomyidae (for .gmi file serving).
        - POSIX make (optional).
        
        
        Documentation
        -------------
        
       -See man pages: stagit-gopher(1) and stagit-gopher-index(1).
       +See man pages: stagit-gemini(1) and stagit-gemini-index(1).
        
        
        Building a static binary
       @@ -113,7 +113,7 @@ git post-receive hook (repo/.git/hooks/post-receive):
        Create .tar.gz archives by tag
        ------------------------------
                #!/bin/sh
       -        name="stagit-gopher"
       +        name="stagit-gemini"
                mkdir -p archives
                git tag -l | while read -r t; do
                        f="archives/${name}-$(echo "${t}" | tr '/' '_').tar.gz"
       @@ -138,7 +138,7 @@ Features
        - Detect submodules (.gitmodules file) from HEAD and link it as a page.
        - Atom feed of the commit log (atom.xml).
        - Atom feed of the tags/refs (tags.xml).
       -- Make index page for multiple repositories with stagit-gopher-index.
       +- Make index page for multiple repositories with stagit-gemini-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 geomyidae Gopher server is required.
 (DIR) diff --git a/example_create.sh b/example_create.sh
       @@ -8,20 +8,20 @@
        # - write description in "description" file.
        #
        # Usage:
       -# - mkdir -p gphdir && cd gphdir
       +# - mkdir -p gmidir && cd gmidir
        # - sh example_create.sh
        
        # path must be absolute.
        reposdir="/var/scm/git"
       -gopherdir="/var/gopher"
       +geminidir="/var/gemini"
        stagitdir="/scm"
       -destdir="${gopherdir}/${stagitdir}"
       +destdir="${geminidir}/${stagitdir}"
        
        # remove /'s at the end.
        stagitdir=$(printf "%s" "${stagitdir}" | sed 's@[/]*$@@g')
        
        # make index.
       -stagit-gopher-index -b "${stagitdir}" "${reposdir}/"*/ > "${destdir}/index.gph"
       +stagit-gemini-index -b "${stagitdir}" "${reposdir}/"*/ > "${destdir}/index.gmi"
        
        # make files per repo.
        for dir in "${reposdir}/"*/; do
       @@ -32,11 +32,11 @@ for dir in "${reposdir}/"*/; do
        
                mkdir -p "${destdir}/${d}"
                cd "${destdir}/${d}" || continue
       -        stagit-gopher -b "${stagitdir}/${d}" -c ".cache" \
       -                -u "gopher://codemadness.org/1/git/$d/" "${reposdir}/${r}"
       +        stagit-gemini -b "${stagitdir}/${d}" -c ".cache" \
       +                -u "gemini://codemadness.org/1/git/$d/" "${reposdir}/${r}"
        
                # symlinks
       -        ln -sf log.gph index.gph
       +        ln -sf log.gmi index.gmi
        
                echo "done"
        done
 (DIR) diff --git a/example_post-receive.sh b/example_post-receive.sh
       @@ -21,10 +21,10 @@ fi
        # paths must be absolute.
        reposdir="/home/src/src"
        dir="${reposdir}/${name}"
       -gopherdir="/home/www/gopher"
       +geminidir="/home/www/gemini"
        stagitdir="/"
       -destdir="${gopherdir}/${stagitdir}"
       -cachefile=".gphcache"
       +destdir="${geminidir}/${stagitdir}"
       +cachefile=".gmicache"
        # /config
        
        if ! test -d "${dir}"; then
       @@ -49,7 +49,7 @@ done
        # strip .git suffix.
        r=$(basename "${name}")
        d=$(basename "${name}" ".git")
       -printf "[%s] stagit .gph pages... " "${d}"
       +printf "[%s] stagit .gmi pages... " "${d}"
        
        mkdir -p "${destdir}/${d}"
        cd "${destdir}/${d}" || exit 1
       @@ -64,12 +64,12 @@ fi
        stagitdir=$(printf "%s" "${stagitdir}" | sed 's@[/]*$@@g')
        
        # make index.
       -stagit-gopher-index -b "${stagitdir}" "${reposdir}/"*/ > "${destdir}/index.gph"
       +stagit-gemini-index -b "${stagitdir}" "${reposdir}/"*/ > "${destdir}/index.gmi"
        
        # make pages.
       -stagit-gopher -b "${stagitdir}/${d}" -c "${cachefile}" \
       -        -u "gopher://codemadness.org/1/git/$d/" "${reposdir}/${r}"
       +stagit-gemini -b "${stagitdir}/${d}" -c "${cachefile}" \
       +        -u "gemini://codemadness.org/1/git/$d/" "${reposdir}/${r}"
        
       -ln -sf log.gph index.gph
       +ln -sf log.gmi index.gmi
        
        echo "done"
 (DIR) diff --git a/file b/file
       @@ -0,0 +1,8 @@
       +mv stagit-gemini         stagit-gemini
       +mv stagit-gemini-index   stagit-gemini-index
       +mv stagit-gemini-index.1 stagit-gemini-index.1
       +mv stagit-gemini-index.c stagit-gemini-index.c
       +mv stagit-gemini-index.o stagit-gemini-index.o
       +mv stagit-gemini.1       stagit-gemini.1
       +mv stagit-gemini.c       stagit-gemini.c
       +mv stagit-gemini.o       stagit-gemini.o
 (DIR) diff --git a/stagit-gemini-index.1 b/stagit-gemini-index.1
       @@ -0,0 +1,43 @@
       +.Dd August 2, 2021
       +.Dt STAGIT-GOPHER-INDEX 1
       +.Os
       +.Sh NAME
       +.Nm stagit-gemini-index
       +.Nd static git Gopher index generator
       +.Sh SYNOPSIS
       +.Nm
       +.Op Fl b Ar baseprefix
       +.Op Ar repodir...
       +.Sh DESCRIPTION
       +.Nm
       +will create a Gopher index for the repositories specified and writes
       +the Gopher data to stdout.
       +The repos in the index are in the same order as the arguments
       +.Ar repodir
       +specified.
       +.Pp
       +The options are as follows:
       +.Bl -tag -width Ds
       +.It Fl b Ar baseprefix
       +Use base prefix as the root.
       +By default this is "/".
       +.El
       +.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
       +.El
       +.Sh EXAMPLES
       +.Bd -literal
       +cd gmiroot
       +stagit-gemini-index path/to/gitrepo1 path/to/gitrepo2 > index.gmi
       +.Ed
       +.Sh SEE ALSO
       +.Xr stagit-gemini 1
       +.Sh AUTHORS
       +.An Hiltjo Posthuma Aq Mt hiltjo@codemadness.org
 (DIR) diff --git a/stagit-gemini-index.c b/stagit-gemini-index.c
       @@ -0,0 +1,310 @@
       +#include <err.h>
       +#include <locale.h>
       +#include <limits.h>
       +#include <stdio.h>
       +#include <stdlib.h>
       +#include <string.h>
       +#include <time.h>
       +#include <unistd.h>
       +#include <wchar.h>
       +
       +#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 = "";
       +
       +static char description[255] = "Repositories";
       +static char *name = "";
       +
       +/* Handle read or write errors for a FILE * stream */
       +void
       +checkfileerror(FILE *fp, const char *name, int mode)
       +{
       +        if (mode == 'r' && ferror(fp))
       +                errx(1, "read error: %s", name);
       +        else if (mode == 'w' && (fflush(fp) || ferror(fp)))
       +                errx(1, "write error: %s", name);
       +}
       +
       +/* 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 inc, rl, w;
       +
       +        if (!bufsiz)
       +                return -1;
       +        if (!len) {
       +                buf[0] = '\0';
       +                return 0;
       +        }
       +
       +        slen = strlen(s);
       +        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;
       +                } 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 + inc + 1 >= bufsiz)
       +                        return -1;
       +                memcpy(&buf[siz], &s[i], inc);
       +                siz += inc;
       +                buf[siz] = '\0';
       +                col += w;
       +        }
       +
       +        len -= col;
       +        if (siz + len + 1 >= bufsiz)
       +                return -1;
       +        memset(&buf[siz], pad, len);
       +        siz += len;
       +        buf[siz] = '\0';
       +
       +        return 0;
       +}
       +
       +/* Escape characters in text in geomyidae .gmi format,
       +   newlines are ignored */
       +void
       +gmitext(FILE *fp, const char *s, size_t len)
       +{
       +        size_t i;
       +
       +        for (i = 0; *s && i < len; s++, i++) {
       +                switch (*s) {
       +                case '\r': /* ignore CR */
       +                case '\n': /* ignore LF */
       +                        break;
       +                default:
       +                        putc(*s, fp);
       +                        break;
       +                }
       +        }
       +}
       +
       +/* Escape characters in links in geomyidae .gmi format */
       +void
       +gmilink(FILE *fp, const char *s, size_t len)
       +{
       +        size_t i;
       +
       +        for (i = 0; *s && i < len; s++, i++) {
       +                switch (*s) {
       +                case '\r': /* ignore CR */
       +                case '\n': /* ignore LF */
       +                        break;
       +                default:
       +                        putc(*s, fp);
       +                        break;
       +                }
       +        }
       +}
       +
       +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 < 0 || (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)
       +{
       +        if (description[0]) {
       +                fputs(description, fp);
       +                fputs("\n\n", fp);
       +        }
       +
       +        fprintf(fp, "%-20.20s  ", "Name");
       +        fprintf(fp, "%-39.39s  ", "Description");
       +        fprintf(fp, "%s\n", "Last commit");
       +}
       +
       +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);
       +
       +        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';
       +
       +        fprintf(fp, "=> %s/%s/log.gmi ", relpath, stripped_name);
       +        utf8pad(buf, sizeof(buf), stripped_name, 20, ' ');
       +        gmilink(fp, buf, strlen(buf));
       +        fputs("  ", fp);
       +        utf8pad(buf, sizeof(buf), description, 39, ' ');
       +        gmilink(fp, buf, strlen(buf));
       +        fputs("  ", fp);
       +        if (author)
       +                printtimeshort(fp, &(author->when));
       +  fputc('\n', fp);
       +
       +        git_commit_free(commit);
       +err:
       +        git_revwalk_free(w);
       +        free(stripped_name);
       +
       +        return ret;
       +}
       +
       +void
       +usage(const char *argv0)
       +{
       +        fprintf(stderr, "usage: %s [-b baseprefix] [repodir...]\n", argv0);
       +        exit(1);
       +}
       +
       +int
       +main(int argc, char *argv[])
       +{
       +        FILE *fp;
       +        char path[PATH_MAX], repodirabs[PATH_MAX + 1];
       +        const char *repodir = NULL;
       +        int i, r, ret = 0;
       +
       +        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, "");
       +        /* do not require the git repository to be owned by the current user */
       +        git_libgit2_opts(GIT_OPT_SET_OWNER_VALIDATION, 0);
       +
       +#ifdef __OpenBSD__
       +        if (pledge("stdio rpath", NULL) == -1)
       +                err(1, "pledge");
       +#endif
       +
       +        for (i = 1, r = 0; i < argc; i++) {
       +                if (argv[i][0] == '-') {
       +                        if (argv[i][1] != 'b' || i + 1 >= argc)
       +                                usage(argv[0]);
       +                        relpath = argv[++i];
       +                        continue;
       +                }
       +
       +                if (r++ == 0)
       +                        writeheader(stdout);
       +
       +                repodir = argv[i];
       +                if (!realpath(repodir, repodirabs))
       +                        err(1, "realpath");
       +
       +                if (git_repository_open_ext(&repo, repodir,
       +                    GIT_REPOSITORY_OPEN_NO_SEARCH, NULL)) {
       +                        fprintf(stderr, "%s: cannot open repository\n", argv[0]);
       +                        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[strcspn(description, "\t\r\n")] = '\0';
       +                        else
       +                                description[0] = '\0';
       +                        checkfileerror(fp, "description", 'r');
       +                        fclose(fp);
       +                }
       +
       +                writelog(stdout);
       +        }
       +        if (!repodir)
       +                usage(argv[0]);
       +
       +        /* cleanup */
       +        git_repository_free(repo);
       +        git_libgit2_shutdown();
       +
       +        checkfileerror(stdout, "<stdout>", 'w');
       +
       +        return ret;
       +}
 (DIR) diff --git a/stagit-gemini.1 b/stagit-gemini.1
       @@ -0,0 +1,119 @@
       +.Dd August 2, 2021
       +.Dt STAGIT-GOPHER 1
       +.Os
       +.Sh NAME
       +.Nm stagit-gemini
       +.Nd static git Gopher index generator
       +.Sh SYNOPSIS
       +.Nm
       +.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
       +writes Gopher indexes for the repository
       +.Ar repodir
       +to the current directory.
       +.Pp
       +The options are as follows:
       +.Bl -tag -width Ds
       +.It Fl b Ar baseprefix
       +Use base prefix as the root.
       +By default this is "".
       +.It Fl c Ar cachefile
       +Cache the entries of the log index up to the point of
       +the last commit.
       +The
       +.Ar cachefile
       +will store the last commit id and the entries in the Gopher index.
       +It is up to the user to make sure the state of the
       +.Ar cachefile
       +is in sync with the history of the repository.
       +.It Fl l Ar commits
       +Write a maximum number of
       +.Ar commits
       +to the log.gmi 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 gemini type.
       +For example: "gemini://codemadness.org/1/git/stagit-gemini/".
       +.El
       +.Pp
       +The options
       +.Fl c
       +and
       +.Fl l
       +cannot be used at the same time.
       +.Pp
       +The following files will be written:
       +.Bl -tag -width Ds
       +.It atom.xml
       +Atom XML feed of the last 100 commits.
       +.It tags.xml
       +Atom XML feed of the tags.
       +.It files.gmi
       +List of files in the latest tree, linking to the file.
       +.It log.gmi
       +List of commits in reverse chronological applied commit order, each commit
       +links to a page with a diffstat and diff of the commit.
       +.It refs.gmi
       +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.gmi.
       +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.gmi.
       +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 Gopher commit 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/url or url (bare repo).
       +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 gmiroot/gmirepo1 && cd gmiroot/gmirepo1
       +stagit-gemini path/to/gitrepo1
       +# repeat for other repositories.
       +.Ed
       +.Pp
       +To update the gmi 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-gemini-index 1
       +.Sh AUTHORS
       +.An Hiltjo Posthuma Aq Mt hiltjo@codemadness.org
 (DIR) diff --git a/stagit-gemini.c b/stagit-gemini.c
       @@ -0,0 +1,1475 @@
       +#include <sys/stat.h>
       +#include <sys/types.h>
       +
       +#include <err.h>
       +#include <errno.h>
       +#include <libgen.h>
       +#include <limits.h>
       +#include <locale.h>
       +#include <stdint.h>
       +#include <stdio.h>
       +#include <stdlib.h>
       +#include <string.h>
       +#include <time.h>
       +#include <unistd.h>
       +#include <wchar.h>
       +
       +#include <git2.h>
       +
       +#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;
       +
       +        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;
       +};
       +
       +/* reference and associated data for sorting */
       +struct referenceinfo {
       +        struct git_reference *ref;
       +        struct commitinfo *ci;
       +};
       +
       +static git_repository *repo;
       +
       +static const char *baseurl = ""; /* base URL to make absolute RSS/Atom URI */
       +static const char *relpath = "";
       +static const char *repodir;
       +
       +static char *name = "";
       +static char *strippedname = "";
       +static char description[255];
       +static char cloneurl[1024];
       +static char *submodules;
       +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; /* -1 indicates not used */
       +
       +/* cache */
       +static git_oid lastoid;
       +static char lastoidstr[GIT_OID_HEXSZ + 2]; /* id + newline + NUL byte */
       +static FILE *rcachefp, *wcachefp;
       +static const char *cachefile;
       +
       +/* Handle read or write errors for a FILE * stream */
       +void
       +checkfileerror(FILE *fp, const char *name, int mode)
       +{
       +        if (mode == 'r' && ferror(fp))
       +                errx(1, "read error: %s", name);
       +        else if (mode == 'w' && (fflush(fp) || ferror(fp)))
       +                errx(1, "write error: %s", name);
       +}
       +
       +/* 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 inc, rl, w;
       +
       +        if (!bufsiz)
       +                return -1;
       +        if (!len) {
       +                buf[0] = '\0';
       +                return 0;
       +        }
       +
       +        slen = strlen(s);
       +        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;
       +                } 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 + inc + 1 >= bufsiz)
       +                        return -1;
       +                memcpy(&buf[siz], &s[i], inc);
       +                siz += inc;
       +                buf[siz] = '\0';
       +                col += w;
       +        }
       +
       +        len -= col;
       +        if (siz + len + 1 >= bufsiz)
       +                return -1;
       +        memset(&buf[siz], pad, len);
       +        siz += len;
       +        buf[siz] = '\0';
       +
       +        return 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 < 0 || (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);
       +        memset(di, 0, sizeof(*di));
       +        free(di);
       +}
       +
       +int
       +commitinfo_getstats(struct commitinfo *ci)
       +{
       +        struct deltainfo *di;
       +        git_diff_options opts;
       +        git_diff_find_options fopts;
       +        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;
       +
       +        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 |
       +                      GIT_DIFF_IGNORE_SUBMODULES |
       +                      GIT_DIFF_INCLUDE_TYPECHANGE;
       +        if (git_diff_tree_to_tree(&(ci->diff), repo, ci->parent_tree, ci->commit_tree, &opts))
       +                goto err;
       +
       +        if (git_diff_find_init_options(&fopts, GIT_DIFF_FIND_OPTIONS_VERSION))
       +                goto err;
       +        /* find renames and copies, exact matches (no heuristic) for renames. */
       +        fopts.flags |= GIT_DIFF_FIND_RENAMES | GIT_DIFF_FIND_COPIES |
       +                       GIT_DIFF_FIND_EXACT_MATCH_ONLY;
       +        if (git_diff_find_similar(ci->diff, &fopts))
       +                goto err;
       +
       +        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:
       +        git_diff_free(ci->diff);
       +        ci->diff = NULL;
       +        git_tree_free(ci->commit_tree);
       +        ci->commit_tree = NULL;
       +        git_tree_free(ci->parent_tree);
       +        ci->parent_tree = NULL;
       +        git_commit_free(ci->parent);
       +        ci->parent = NULL;
       +        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);
       +        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);
       +        memset(ci, 0, sizeof(*ci));
       +        free(ci);
       +}
       +
       +struct commitinfo *
       +commitinfo_getbyoid(const git_oid *id)
       +{
       +        struct commitinfo *ci;
       +
       +        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);
       +
       +        return ci;
       +
       +err:
       +        commitinfo_free(ci);
       +
       +        return NULL;
       +}
       +
       +int
       +refs_cmp(const void *v1, const void *v2)
       +{
       +        const struct referenceinfo *r1 = v1, *r2 = v2;
       +        time_t t1, t2;
       +        int r;
       +
       +        if ((r = git_reference_is_tag(r1->ref) - git_reference_is_tag(r2->ref)))
       +                return r;
       +
       +        t1 = r1->ci->author ? r1->ci->author->when.time : 0;
       +        t2 = r2->ci->author ? r2->ci->author->when.time : 0;
       +        if ((r = t1 > t2 ? -1 : (t1 == t2 ? 0 : 1)))
       +                return r;
       +
       +        return strcmp(git_reference_shorthand(r1->ref),
       +                      git_reference_shorthand(r2->ref));
       +}
       +
       +int
       +getrefs(struct referenceinfo **pris, size_t *prefcount)
       +{
       +        struct referenceinfo *ris = NULL;
       +        struct commitinfo *ci = NULL;
       +        git_reference_iterator *it = NULL;
       +        const git_oid *id = NULL;
       +        git_object *obj = NULL;
       +        git_reference *dref = NULL, *r, *ref = NULL;
       +        size_t i, refcount;
       +
       +        *pris = NULL;
       +        *prefcount = 0;
       +
       +        if (git_reference_iterator_new(&it, repo))
       +                return -1;
       +
       +        for (refcount = 0; !git_reference_next(&ref, it); ) {
       +                if (!git_reference_is_branch(ref) && !git_reference_is_tag(ref)) {
       +                        git_reference_free(ref);
       +                        ref = NULL;
       +                        continue;
       +                }
       +
       +                switch (git_reference_type(ref)) {
       +                case GIT_REF_SYMBOLIC:
       +                        if (git_reference_resolve(&dref, ref))
       +                                goto err;
       +                        r = dref;
       +                        break;
       +                case GIT_REF_OID:
       +                        r = ref;
       +                        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;
       +
       +                if (!(ris = reallocarray(ris, refcount + 1, sizeof(*ris))))
       +                        err(1, "realloc");
       +                ris[refcount].ci = ci;
       +                ris[refcount].ref = r;
       +                refcount++;
       +
       +                git_object_free(obj);
       +                obj = NULL;
       +                git_reference_free(dref);
       +                dref = NULL;
       +        }
       +        git_reference_iterator_free(it);
       +
       +        /* sort by type, date then shorthand name */
       +        qsort(ris, refcount, sizeof(*ris), refs_cmp);
       +
       +        *pris = ris;
       +        *prefcount = refcount;
       +
       +        return 0;
       +
       +err:
       +        git_object_free(obj);
       +        git_reference_free(dref);
       +        commitinfo_free(ci);
       +        for (i = 0; i < refcount; i++) {
       +                commitinfo_free(ris[i].ci);
       +                git_reference_free(ris[i].ref);
       +        }
       +        free(ris);
       +
       +        return -1;
       +}
       +
       +FILE *
       +efopen(const char *filename, const char *flags)
       +{
       +        FILE *fp;
       +
       +        if (!(fp = fopen(filename, flags)))
       +                err(1, "fopen: '%s'", filename);
       +
       +        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:   putc(*s, fp);
       +                }
       +        }
       +}
       +
       +/* Escape characters in text in geomyidae .gmi format, with newlines */
       +void
       +gmitextnl(FILE *fp, const char *s, size_t len)
       +{
       +        size_t i, n = 0;
       +
       +        for (i = 0; s[i] && i < len; i++) {
       +                switch (s[i]) {
       +                case '\r': break;
       +                default: fputc(s[i], fp);
       +                }
       +                n = (s[i] != '\n');
       +        }
       +}
       +
       +/* Escape characters in text in geomyidae .gmi format,
       +   newlines are ignored */
       +void
       +gmitext(FILE *fp, const char *s, size_t len)
       +{
       +        size_t i;
       +
       +        for (i = 0; *s && i < len; s++, i++) {
       +                switch (*s) {
       +                case '\r': /* ignore CR */
       +                case '\n': /* ignore LF */
       +                        break;
       +                default:
       +                        putc(*s, fp);
       +                        break;
       +                }
       +        }
       +}
       +
       +/* Escape characters in links in geomyidae .gmi format */
       +void
       +gmilink(FILE *fp, const char *s, size_t len)
       +{
       +        size_t i;
       +
       +        for (i = 0; *s && i < len; s++, i++) {
       +                switch (*s) {
       +                case '\r': /* ignore CR */
       +                case '\n': /* ignore LF */
       +                        break;
       +                default:
       +                        putc(*s, 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)
       +{
       +        gmitext(fp, title, strlen(title));
       +        if (title[0] && strippedname[0])
       +                fputs(" - ", fp);
       +        gmitext(fp, strippedname, strlen(strippedname));
       +        if (description[0])
       +                fputs(" - ", fp);
       +        gmitext(fp, description, strlen(description));
       +        fputs("\n", fp);
       +        if (cloneurl[0]) {
       +    fputs("=> ", fp);
       +                gmilink(fp, cloneurl, strlen(cloneurl));
       +                fputs(" git clone:  ", fp);
       +                gmilink(fp, cloneurl, strlen(cloneurl));
       +                fputc('\n', fp);
       +        }
       +        fprintf(fp, "=> %s/log.gmi Log\n", relpath);
       +        fprintf(fp, "=> %s/files.gmi Files\n", relpath);
       +        fprintf(fp, "=> %s/refs.gmi Refs\n", relpath);
       +        if (submodules)
       +                fprintf(fp, "=> %s/file/%s.gmi Submodules\n",
       +                        relpath, submodules);
       +        if (readme)
       +                fprintf(fp, "=> %s/file/%s.gmi README\n",
       +                        relpath, readme);
       +        if (license)
       +                fprintf(fp, "=> %s/file/%s.gmi LICENSE\n",
       +                        relpath, license);
       +        fputs("---\n", fp);
       +}
       +
       +void
       +writefooter(FILE *fp)
       +{
       +  fputs("\n\ngenerated w/ stagit-gemini\n\n", fp);
       +}
       +
       +size_t
       +writeblobgmi(FILE *fp, const git_blob *blob)
       +{
       +        size_t n = 0, i, j, len, prev;
       +        const char *nfmt = "%6zu ";
       +        const char *s = git_blob_rawcontent(blob);
       +
       +        len = git_blob_rawsize(blob);
       +        if (len > 0) {
       +                for (i = 0, prev = 0; i < len; i++) {
       +                        if (s[i] != '\n')
       +                                continue;
       +                        n++;
       +                        fprintf(fp, nfmt, n, n, n);
       +                        for (j = prev; j <= i && s[j]; j++) {
       +                                switch (s[j]) {
       +                                case '\r': break;
       +                                default: putc(s[j], fp);
       +                                }
       +                        }
       +                        prev = i + 1;
       +                }
       +                /* trailing data */
       +                if ((len - prev) > 0) {
       +                        n++;
       +                        fprintf(fp, nfmt, n, n, n);
       +                        for (j = prev; j < len - prev && s[j]; j++) {
       +                                switch (s[j]) {
       +                                case '\r': break;
       +                                case '\t': fputs("        ", fp); break;
       +                                default: putc(s[j], fp);
       +                                }
       +                        }
       +                }
       +        }
       +
       +        return n;
       +}
       +
       +void
       +printcommit(FILE *fp, struct commitinfo *ci)
       +{
       +        fprintf(fp, "=> %s/commit/%s.gmi commit %s\n",
       +                relpath, ci->oid, ci->oid);
       +
       +        if (ci->parentoid[0])
       +                fprintf(fp, "=> %s/commit/%s.gmi parent %s\n",
       +                        relpath, ci->parentoid, ci->parentoid);
       +
       +        if (ci->author) {
       +                fputs(" Author: ", fp);
       +                gmilink(fp, ci->author->name, strlen(ci->author->name));
       +                fputs(" <", fp);
       +                gmilink(fp, ci->author->email, strlen(ci->author->email));
       +                fputs(">\n", fp);
       +                fputs("Date:   ", fp);
       +                printtime(fp, &(ci->author->when));
       +                putc('\n', fp);
       +        }
       +        if (ci->msg) {
       +                putc('\n', fp);
       +                gmitextnl(fp, ci->msg, strlen(ci->msg));
       +                putc('\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 buf[256], filename[256], linestr[32];
       +        int c;
       +
       +        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);
       +
       +                switch (delta->status) {
       +                case GIT_DELTA_ADDED:      c = 'A'; break;
       +                case GIT_DELTA_COPIED:     c = 'C'; break;
       +                case GIT_DELTA_DELETED:    c = 'D'; break;
       +                case GIT_DELTA_MODIFIED:   c = 'M'; break;
       +                case GIT_DELTA_RENAMED:    c = 'R'; break;
       +                case GIT_DELTA_TYPECHANGE: c = 'T'; break;
       +                default:                   c = ' '; break;
       +                }
       +
       +                if (strcmp(delta->old_file.path, delta->new_file.path)) {
       +                        snprintf(filename, sizeof(filename), "%s -> %s",
       +                                delta->old_file.path, delta->new_file.path);
       +                        utf8pad(buf, sizeof(buf), filename, 35, ' ');
       +                } else {
       +                        utf8pad(buf, sizeof(buf), delta->old_file.path, 35, ' ');
       +                }
       +                fprintf(fp, "  %c ", c);
       +                gmitext(fp, buf, strlen(buf));
       +
       +                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, " | %7zu ",
       +                        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, "=> %s/file/", relpath);
       +                gmilink(fp, delta->new_file.path, strlen(delta->new_file.path));
       +                fputs(".gmi diff --git a/", fp);
       +                gmilink(fp, delta->old_file.path, strlen(delta->old_file.path));
       +                fputs(" b/", fp);
       +                gmilink(fp, delta->new_file.path, strlen(delta->new_file.path));
       +    fputc('\n', fp);
       +
       +                /* 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;
       +
       +                        if (hunk->header_len > 0 && hunk->header[0] == '[')
       +                                fputc('t', fp);
       +                        gmitext(fp, hunk->header, hunk->header_len);
       +                        putc('\n', fp);
       +
       +                        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);
       +                                gmitext(fp, line->content, line->content_len);
       +                                putc('\n', fp);
       +                        }
       +                }
       +        }
       +}
       +
       +void
       +writelogline(FILE *fp, struct commitinfo *ci)
       +{
       +        char buf[256];
       +
       +        fprintf(fp, "=> %s/commit/%s.gmi ", relpath, ci->oid);
       +        if (ci->author)
       +                printtimeshort(fp, &(ci->author->when));
       +        else
       +                fputs("                ", fp);
       +        fputs("  ", fp);
       +        utf8pad(buf, sizeof(buf), ci->summary ? ci->summary : "", 40, ' ');
       +        gmilink(fp, buf, strlen(buf));
       +        fputs("  ", fp);
       +        utf8pad(buf, sizeof(buf), ci->author ? ci->author->name : "", 19, '\0');
       +        gmilink(fp, buf, strlen(buf));
       +  fputc('\n', fp);
       +}
       +
       +int
       +writelog(FILE *fp, const git_oid *oid)
       +{
       +        struct commitinfo *ci;
       +        git_revwalk *w = NULL;
       +        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);
       +
       +        while (!git_revwalk_next(&id, w)) {
       +                if (cachefile && !memcmp(&id, &lastoid, sizeof(id)))
       +                        break;
       +
       +                git_oid_tostr(oidstr, sizeof(oidstr), &id);
       +                r = snprintf(path, sizeof(path), "commit/%s.gmi", oidstr);
       +                if (r < 0 || (size_t)r >= sizeof(path))
       +                        errx(1, "path truncated: 'commit/%s.gmi'", oidstr);
       +                r = access(path, F_OK);
       +
       +                /* optimization: if there are no log lines to write and
       +                   the commit file already exists: skip the diffstat */
       +                if (!nlogcommits) {
       +                        remcommits++;
       +                        if (!r)
       +                                continue;
       +                }
       +
       +                if (!(ci = commitinfo_getbyoid(&id)))
       +                        break;
       +
       +                if (nlogcommits != 0) {
       +                        writelogline(fp, ci);
       +                        if (nlogcommits > 0)
       +                                nlogcommits--;
       +                }
       +
       +                if (cachefile)
       +                        writelogline(wcachefp, ci);
       +
       +                /* check if file exists if so skip it */
       +                if (r) {
       +                        /* lookup stats: only required here for gemini */
       +                        if (commitinfo_getstats(ci) == -1)
       +                                goto err;
       +
       +                        fpfile = efopen(path, "w");
       +                        writeheader(fpfile, ci->summary);
       +                        printshowfile(fpfile, ci);
       +                        writefooter(fpfile);
       +                        checkfileerror(fpfile, path, 'w');
       +                        fclose(fpfile);
       +                }
       +err:
       +                commitinfo_free(ci);
       +        }
       +        git_revwalk_free(w);
       +
       +        if (nlogcommits == 0 && remcommits != 0) {
       +                fprintf(fp, "%16.16s  "
       +                        "%zu more commits remaining, fetch the repository\n",
       +                        "", remcommits);
       +        }
       +
       +        return 0;
       +}
       +
       +void
       +printcommitatom(FILE *fp, struct commitinfo *ci, const char *tag)
       +{
       +        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);
       +                if (tag && tag[0]) {
       +                        fputs("[", fp);
       +                        xmlencode(fp, tag, strlen(tag));
       +                        fputs("] ", fp);
       +                }
       +                xmlencode(fp, ci->summary, strlen(ci->summary));
       +                fputs("</title>\n", fp);
       +        }
       +        fprintf(fp, "<link rel=\"alternate\" href=\"%scommit/%s.gmi\" />\n",
       +                baseurl, ci->oid);
       +
       +        if (ci->author) {
       +                fputs("<author>\n<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));
       +                putc('\n', fp);
       +        }
       +        if (ci->msg) {
       +                putc('\n', fp);
       +                xmlencode(fp, ci->msg, strlen(ci->msg));
       +        }
       +        fputs("\n</content>\n</entry>\n", fp);
       +}
       +
       +int
       +writeatom(FILE *fp, int all)
       +{
       +        struct referenceinfo *ris = NULL;
       +        size_t refcount = 0;
       +        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);
       +
       +        /* all commits or only tags? */
       +        if (all) {
       +                git_revwalk_new(&w, repo);
       +                git_revwalk_push_head(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);
       +        } else if (getrefs(&ris, &refcount) != -1) {
       +                /* references: tags */
       +                for (i = 0; i < refcount; i++) {
       +                        if (git_reference_is_tag(ris[i].ref))
       +                                printcommitatom(fp, ris[i].ci,
       +                                                git_reference_shorthand(ris[i].ref));
       +
       +                        commitinfo_free(ris[i].ci);
       +                        git_reference_free(ris[i].ref);
       +                }
       +                free(ris);
       +        }
       +
       +        fputs("</feed>\n", fp);
       +
       +        return 0;
       +}
       +
       +size_t
       +writeblob(git_object *obj, const char *fpath, const char *filename, size_t filesize)
       +{
       +        char tmp[PATH_MAX] = "", *d;
       +        size_t 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);
       +        if (filename[0] == '[')
       +                fputs("[|", fp);
       +        gmitext(fp, filename, strlen(filename));
       +        fprintf(fp, " (%zuB)\n", filesize);
       +        fputs("---\n", fp);
       +
       +        if (git_blob_is_binary((git_blob *)obj))
       +                fputs("Binary file.\n", fp);
       +        else
       +                lc = writeblobgmi(fp, (git_blob *)obj);
       +
       +        writefooter(fp);
       +        checkfileerror(fp, fpath, 'w');
       +        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_object *obj = NULL;
       +        const char *entryname;
       +        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++) {
       +                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.gmi",
       +                         entrypath);
       +                if (r < 0 || (size_t)r >= sizeof(filepath))
       +                        errx(1, "path truncated: 'file/%s.gmi'", 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);
       +
       +                        fprintf(fp, "=> %s/", relpath);
       +                        gmilink(fp, filepath, strlen(filepath));
       +                        fputc(' ', fp);
       +                        fputs(filemode(git_tree_entry_filemode(entry)), fp);
       +                        fputs("  ", fp);
       +                        utf8pad(buf, sizeof(buf), entrypath, 50, ' ');
       +                        gmilink(fp, buf, strlen(buf));
       +                        fputs("  ", fp);
       +                        if (lc > 0)
       +                                fprintf(fp, "%7zuL", lc);
       +                        else
       +                                fprintf(fp, "%7zuB", filesize);
       +                                                fputc('\n', fp);
       +                        git_object_free(obj);
       +                } else if (git_tree_entry_type(entry) == GIT_OBJ_COMMIT) {
       +                        /* commit object in tree is a submodule */        
       +        git_oid_tostr(oid, sizeof(oid), git_tree_entry_id(entry));
       +                        gmilink(fp, oid, strlen(oid));
       +                        fprintf(fp, "=> %s/file/.gitmodules.gmi ", relpath);
       +                        fputs("m---------  ", fp);
       +                        
       +                        fputc('\n', fp);
       +        
       +                        
       +                        /* NOTE: linecount omitted */
       +                }
       +        }
       +
       +        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
       +writerefs(FILE *fp)
       +{
       +        struct referenceinfo *ris = NULL;
       +        struct commitinfo *ci;
       +        size_t count, i, j, refcount;
       +        const char *titles[] = { "Branches", "Tags" };
       +        const char *s;
       +        char buf[256];
       +
       +        if (getrefs(&ris, &refcount) == -1)
       +                return -1;
       +
       +        for (i = 0, j = 0, count = 0; i < refcount; i++) {
       +                if (j == 0 && git_reference_is_tag(ris[i].ref)) {
       +                        /* table footer */
       +                        if (count)
       +                                fputs("\n", fp);
       +                        count = 0;
       +                        j = 1;
       +                }
       +
       +                /* print header if it has an entry (first). */
       +                if (++count == 1) {
       +                        fprintf(fp, "%s\n", titles[j]);
       +                        fprintf(fp, "  %-32.32s", "Name");
       +                        fprintf(fp, "  %-16.16s", "Last commit date");
       +                        fprintf(fp, "  %s\n", "Author");
       +                }
       +
       +                ci = ris[i].ci;
       +                s = git_reference_shorthand(ris[i].ref);
       +
       +                fputs("  ", fp);
       +                utf8pad(buf, sizeof(buf), s, 32, ' ');
       +                gmilink(fp, buf, strlen(buf));
       +                fputs("  ", fp);
       +                if (ci->author)
       +                        printtimeshort(fp, &(ci->author->when));
       +                else
       +                        fputs("                ", fp);
       +                fputs("  ", fp);
       +                if (ci->author) {
       +                        utf8pad(buf, sizeof(buf), ci->author->name, 25, '\0');
       +                        gmilink(fp, buf, strlen(buf));
       +                }
       +                fputs("\n", fp);
       +        }
       +        /* table footer */
       +        if (count)
       +                fputs("\n", fp);
       +
       +        for (i = 0; i < refcount; i++) {
       +                commitinfo_free(ris[i].ci);
       +                git_reference_free(ris[i].ref);
       +        }
       +        free(ris);
       +
       +        return 0;
       +}
       +
       +void
       +usage(char *argv0)
       +{
       +        fprintf(stderr, "usage: %s [-b baseprefix] [-c cachefile | -l commits] "
       +                "[-u baseurl] repodir\n", argv0);
       +        exit(1);
       +}
       +
       +int
       +main(int argc, char *argv[])
       +{
       +        git_object *obj = NULL;
       +        const git_oid *head = NULL;
       +        mode_t mask;
       +        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;
       +
       +        setlocale(LC_CTYPE, "");
       +
       +        for (i = 1; i < argc; i++) {
       +                if (argv[i][0] != '-') {
       +                        if (repodir)
       +                                usage(argv[0]);
       +                        repodir = argv[i];
       +                } else if (argv[i][1] == 'b') {
       +                        if (i + 1 >= argc)
       +                                usage(argv[0]);
       +                        relpath = argv[++i];
       +                } else if (argv[i][1] == 'c') {
       +                        if (nlogcommits > 0 || i + 1 >= argc)
       +                                usage(argv[0]);
       +                        cachefile = argv[++i];
       +                } else if (argv[i][1] == 'l') {
       +                        if (cachefile || i + 1 >= argc)
       +                                usage(argv[0]);
       +                        errno = 0;
       +                        nlogcommits = strtoll(argv[++i], &p, 10);
       +                        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)
       +                usage(argv[0]);
       +
       +        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, "");
       +        /* do not require the git repository to be owned by the current user */
       +        git_libgit2_opts(GIT_OPT_SET_OWNER_VALIDATION, 0);
       +
       +#ifdef __OpenBSD__
       +        if (unveil(repodir, "r") == -1)
       +                err(1, "unveil: %s", repodir);
       +        if (unveil(".", "rwc") == -1)
       +                err(1, "unveil: .");
       +        if (cachefile && unveil(cachefile, "rwc") == -1)
       +                err(1, "unveil: %s", cachefile);
       +
       +        if (cachefile) {
       +                if (pledge("stdio rpath wpath cpath fattr", NULL) == -1)
       +                        err(1, "pledge");
       +        } else {
       +                if (pledge("stdio rpath wpath cpath", NULL) == -1)
       +                        err(1, "pledge");
       +        }
       +#endif
       +
       +        if (git_repository_open_ext(&repo, repodir,
       +                GIT_REPOSITORY_OPEN_NO_SEARCH, NULL) < 0) {
       +                fprintf(stderr, "%s: cannot open repository\n", argv[0]);
       +                return 1;
       +        }
       +
       +        /* find HEAD */
       +        if (!git_revparse_single(&obj, repo, "HEAD"))
       +                head = git_object_id(obj);
       +        git_object_free(obj);
       +
       +        /* 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';
       +                checkfileerror(fpread, path, 'r');
       +                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';
       +                checkfileerror(fpread, path, 'r');
       +                fclose(fpread);
       +                cloneurl[strcspn(cloneurl, "\n")] = '\0';
       +        }
       +
       +        /* check LICENSE */
       +        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:");
       +                git_object_free(obj);
       +        }
       +
       +        /* check README */
       +        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:");
       +                git_object_free(obj);
       +        }
       +
       +        if (!git_revparse_single(&obj, repo, "HEAD:.gitmodules") &&
       +            git_object_type(obj) == GIT_OBJ_BLOB)
       +                submodules = ".gitmodules";
       +        git_object_free(obj);
       +
       +        /* log for HEAD */
       +        fp = efopen("log.gmi", "w");
       +        mkdir("commit", S_IRWXU | S_IRWXG | S_IRWXO);
       +        writeheader(fp, "Log");
       +
       +        fprintf(fp, "%-16.16s  ", "Date");
       +        fprintf(fp, "%-40.40s  ", "Commit message");
       +        fprintf(fp, "%s\n", "Author");
       +
       +        if (cachefile && head) {
       +                /* 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: '%s'", tmppath);
       +                /* 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.gmi and the new cache */
       +                        while (!feof(rcachefp)) {
       +                                n = fread(buf, 1, sizeof(buf), rcachefp);
       +                                if (ferror(rcachefp))
       +                                        break;
       +                                if (fwrite(buf, 1, n, fp) != n ||
       +                                    fwrite(buf, 1, n, wcachefp) != n)
       +                                        break;
       +                        }
       +                        checkfileerror(rcachefp, cachefile, 'r');
       +                        fclose(rcachefp);
       +                }
       +                checkfileerror(wcachefp, tmppath, 'w');
       +                fclose(wcachefp);
       +        } else {
       +                if (head)
       +                        writelog(fp, head);
       +        }
       +        fprintf(fp, "\n=> %s/atom.xml Atom Feed\n", relpath);
       +        writefooter(fp);
       +        checkfileerror(fp, "log.gmi", 'w');
       +        fclose(fp);
       +
       +        /* files for HEAD */
       +        fp = efopen("files.gmi", "w");
       +        writeheader(fp, "Files");
       +        if (head)
       +                writefiles(fp, head);
       +        writefooter(fp);
       +        checkfileerror(fp, "files.gmi", 'w');
       +        fclose(fp);
       +
       +        /* summary page with branches and tags */
       +        fp = efopen("refs.gmi", "w");
       +        writeheader(fp, "Refs");
       +        writerefs(fp);
       +        writefooter(fp);
       +        checkfileerror(fp, "refs.gmi", 'w');
       +        fclose(fp);
       +
       +        /* Atom feed */
       +        fp = efopen("atom.xml", "w");
       +        writeatom(fp, 1);
       +        checkfileerror(fp, "atom.xml", 'w');
       +        fclose(fp);
       +
       +        /* Atom feed for tags / releases */
       +        fp = efopen("tags.xml", "w");
       +        writeatom(fp, 0);
       +        checkfileerror(fp, "tags.xml", 'w');
       +        fclose(fp);
       +
       +        /* rename new cache file on success */
       +        if (cachefile && head) {
       +                if (rename(tmppath, cachefile))
       +                        err(1, "rename: '%s' to '%s'", tmppath, cachefile);
       +                umask((mask = umask(0)));
       +                if (chmod(cachefile,
       +                    (S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH) & ~mask))
       +                        err(1, "chmod: '%s'", cachefile);
       +        }
       +
       +        /* cleanup */
       +        git_repository_free(repo);
       +        git_libgit2_shutdown();
       +
       +        return 0;
       +}