adding a archive download possibility - stagit - default description
 (HTM) git clone git://thinkerwim.org/stagit.git
 (DIR) Log
 (DIR) Files
 (DIR) Refs
 (DIR) README
 (DIR) LICENSE
       ---
 (DIR) commit 3c590dcf157985bdc31e18de614bcd115b7c2163
 (HTM) Author: Wim Stockman <wim@yasendfile.org>
       Date:   Sat, 11 Mar 2023 16:53:09 +0100
       
       adding a archive download possibility
       
       Diffstat:
         A LICENSE                             |      21 +++++++++++++++++++++
         A Makefile                            |     113 +++++++++++++++++++++++++++++++
         A README                              |     186 +++++++++++++++++++++++++++++++
         A compat.h                            |       6 ++++++
         A example_create.sh                   |      43 ++++++++++++++++++++++++++++++
         A example_post-receive.sh             |      73 +++++++++++++++++++++++++++++++
         A favicon.png                         |       0 
         A logo.png                            |       0 
         A reallocarray.c                      |      39 +++++++++++++++++++++++++++++++
         A stagit-index.1                      |      47 +++++++++++++++++++++++++++++++
         A stagit-index.c                      |     258 +++++++++++++++++++++++++++++++
         A stagit.1                            |     125 +++++++++++++++++++++++++++++++
         A stagit.c                            |    1465 +++++++++++++++++++++++++++++++
         A strlcat.c                           |      57 +++++++++++++++++++++++++++++++
         A strlcpy.c                           |      52 +++++++++++++++++++++++++++++++
         A style.css                           |     154 +++++++++++++++++++++++++++++++
       
       16 files changed, 2639 insertions(+), 0 deletions(-)
       ---
 (DIR) diff --git a/LICENSE b/LICENSE
       @@ -0,0 +1,21 @@
       +MIT/X Consortium License
       +
       +(c) 2015-2022 Hiltjo Posthuma <hiltjo@codemadness.org>
       +
       +Permission is hereby granted, free of charge, to any person obtaining a
       +copy of this software and associated documentation files (the "Software"),
       +to deal in the Software without restriction, including without limitation
       +the rights to use, copy, modify, merge, publish, distribute, sublicense,
       +and/or sell copies of the Software, and to permit persons to whom the
       +Software is furnished to do so, subject to the following conditions:
       +
       +The above copyright notice and this permission notice shall be included in
       +all copies or substantial portions of the Software.
       +
       +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
       +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
       +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
       +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
       +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
       +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
       +DEALINGS IN THE SOFTWARE.
 (DIR) diff --git a/Makefile b/Makefile
       @@ -0,0 +1,113 @@
       +.POSIX:
       +
       +NAME = stagit
       +VERSION = 1.2
       +
       +# paths
       +PREFIX = /usr/local
       +MANPREFIX = ${PREFIX}/man
       +DOCPREFIX = ${PREFIX}/share/doc/${NAME}
       +
       +LIBGIT_INC = -I/usr/local/include
       +LIBGIT_LIB = -L/usr/local/lib -lgit2
       +
       +# use system flags.
       +STAGIT_CFLAGS = ${LIBGIT_INC} ${CFLAGS} -g
       +STAGIT_LDFLAGS = ${LIBGIT_LIB} ${LDFLAGS}
       +STAGIT_CPPFLAGS = -D_XOPEN_SOURCE=700 -D_DEFAULT_SOURCE -D_BSD_SOURCE
       +
       +# Uncomment to enable workaround for older libgit2 which don't support this
       +# option. This workaround will be removed in the future *pinky promise*.
       +#STAGIT_CFLAGS += -DGIT_OPT_SET_OWNER_VALIDATION=-1
       +
       +SRC = \
       +        stagit.c\
       +        stagit-index.c
       +COMPATSRC = \
       +        reallocarray.c\
       +        strlcat.c\
       +        strlcpy.c
       +BIN = \
       +        stagit\
       +        stagit-index
       +MAN1 = \
       +        stagit.1\
       +        stagit-index.1
       +DOC = \
       +        LICENSE\
       +        README
       +HDR = compat.h
       +
       +COMPATOBJ = \
       +        reallocarray.o\
       +        strlcat.o\
       +        strlcpy.o
       +
       +OBJ = ${SRC:.c=.o} ${COMPATOBJ}
       +
       +all: ${BIN}
       +
       +.o:
       +        ${CC} -o $@ ${LDFLAGS}
       +
       +.c.o:
       +        ${CC} -o $@ -c $< ${STAGIT_CFLAGS} ${STAGIT_CPPFLAGS}
       +
       +dist:
       +        rm -rf ${NAME}-${VERSION}
       +        mkdir -p ${NAME}-${VERSION}
       +        cp -f ${MAN1} ${HDR} ${SRC} ${COMPATSRC} ${DOC} \
       +                Makefile favicon.png logo.png style.css \
       +                example_create.sh example_post-receive.sh \
       +                ${NAME}-${VERSION}
       +        # make tarball
       +        tar -cf - ${NAME}-${VERSION} | \
       +                gzip -c > ${NAME}-${VERSION}.tar.gz
       +        rm -rf ${NAME}-${VERSION}
       +
       +${OBJ}: ${HDR}
       +
       +stagit: stagit.o ${COMPATOBJ}
       +        ${CC} -o $@ stagit.o ${COMPATOBJ} ${STAGIT_LDFLAGS}
       +
       +stagit-index: stagit-index.o ${COMPATOBJ}
       +        ${CC} -o $@ stagit-index.o ${COMPATOBJ} ${STAGIT_LDFLAGS}
       +
       +clean:
       +        rm -f ${BIN} ${OBJ} ${NAME}-${VERSION}.tar.gz
       +
       +install: all
       +        # installing executable files.
       +        mkdir -p ${DESTDIR}${PREFIX}/bin
       +        cp -f ${BIN} ${DESTDIR}${PREFIX}/bin
       +        for f in ${BIN}; do chmod 755 ${DESTDIR}${PREFIX}/bin/$$f; done
       +        # installing example files.
       +        mkdir -p ${DESTDIR}${DOCPREFIX}
       +        cp -f style.css\
       +                favicon.png\
       +                logo.png\
       +                example_create.sh\
       +                example_post-receive.sh\
       +                README\
       +                ${DESTDIR}${DOCPREFIX}
       +        # installing manual pages.
       +        mkdir -p ${DESTDIR}${MANPREFIX}/man1
       +        cp -f ${MAN1} ${DESTDIR}${MANPREFIX}/man1
       +        for m in ${MAN1}; do chmod 644 ${DESTDIR}${MANPREFIX}/man1/$$m; done
       +
       +uninstall:
       +        # removing executable files.
       +        for f in ${BIN}; do rm -f ${DESTDIR}${PREFIX}/bin/$$f; done
       +        # removing example files.
       +        rm -f \
       +                ${DESTDIR}${DOCPREFIX}/style.css\
       +                ${DESTDIR}${DOCPREFIX}/favicon.png\
       +                ${DESTDIR}${DOCPREFIX}/logo.png\
       +                ${DESTDIR}${DOCPREFIX}/example_create.sh\
       +                ${DESTDIR}${DOCPREFIX}/example_post-receive.sh\
       +                ${DESTDIR}${DOCPREFIX}/README
       +        -rmdir ${DESTDIR}${DOCPREFIX}
       +        # removing manual pages.
       +        for m in ${MAN1}; do rm -f ${DESTDIR}${MANPREFIX}/man1/$$m; done
       +
       +.PHONY: all clean dist install uninstall
 (DIR) diff --git a/README b/README
       @@ -0,0 +1,186 @@
       +stagit
       +------
       +
       +static git page generator.
       +
       +It generates static HTML pages for a git repository.
       +
       +
       +Usage
       +-----
       +
       +Make files per repository:
       +
       +        $ mkdir -p htmlroot/htmlrepo1 && cd htmlroot/htmlrepo1
       +        $ stagit path/to/gitrepo1
       +        repeat for other repositories
       +        $ ...
       +
       +Make index file for repositories:
       +
       +        $ cd htmlroot
       +        $ stagit-index path/to/gitrepo1 \
       +                       path/to/gitrepo2 \
       +                       path/to/gitrepo3 > index.html
       +
       +
       +Build and install
       +-----------------
       +
       +$ make
       +# make install
       +
       +
       +Dependencies
       +------------
       +
       +- C compiler (C99).
       +- libc (tested with OpenBSD, FreeBSD, NetBSD, Linux: glibc and musl).
       +- libgit2 (v0.22+).
       +- POSIX make (optional).
       +
       +
       +Documentation
       +-------------
       +
       +See man pages: stagit(1) and stagit-index(1).
       +
       +
       +Building a static binary
       +------------------------
       +
       +It may be useful to build static binaries, for example to run in a chroot.
       +
       +It can be done like this at the time of writing (v0.24):
       +
       +cd libgit2-src
       +
       +# change the options in the CMake file: CMakeLists.txt
       +BUILD_SHARED_LIBS to OFF (static)
       +CURL to OFF              (not needed)
       +USE_SSH OFF              (not needed)
       +THREADSAFE OFF           (not needed)
       +USE_OPENSSL OFF          (not needed, use builtin)
       +
       +mkdir -p build && cd build
       +cmake ../
       +make
       +make install
       +
       +
       +Extract owner field from git config
       +-----------------------------------
       +
       +A way to extract the gitweb owner for example in the format:
       +
       +        [gitweb]
       +                owner = Name here
       +
       +Script:
       +
       +        #!/bin/sh
       +        awk '/^[         ]*owner[         ]=/ {
       +                sub(/^[^=]*=[         ]*/, "");
       +                print $0;
       +        }'
       +
       +
       +Set clone URL for a directory of repos
       +--------------------------------------
       +        #!/bin/sh
       +        cd "$dir"
       +        for i in *; do
       +                test -d "$i" && echo "git://git.codemadness.org/$i" > "$i/url"
       +        done
       +
       +
       +Update files on git push
       +------------------------
       +
       +Using a post-receive hook the static files can be automatically updated.
       +Keep in mind git push -f can change the history and the commits may need
       +to be recreated. This is because stagit checks if a commit file already
       +exists. It also has a cache (-c) option which can conflict with the new
       +history. See stagit(1).
       +
       +git post-receive hook (repo/.git/hooks/post-receive):
       +
       +        #!/bin/sh
       +        # detect git push -f
       +        force=0
       +        while read -r old new ref; do
       +                hasrevs=$(git rev-list "$old" "^$new" | sed 1q)
       +                if test -n "$hasrevs"; then
       +                        force=1
       +                        break
       +                fi
       +        done
       +
       +        # remove commits and .cache on git push -f
       +        #if test "$force" = "1"; then
       +        # ...
       +        #fi
       +
       +        # see example_create.sh for normal creation of the files.
       +
       +
       +Create .tar.gz archives by tag
       +------------------------------
       +        #!/bin/sh
       +        name="stagit"
       +        mkdir -p archives
       +        git tag -l | while read -r t; do
       +                f="archives/${name}-$(echo "${t}" | tr '/' '_').tar.gz"
       +                test -f "${f}" && continue
       +                git archive \
       +                        --format tar.gz \
       +                        --prefix "${t}/" \
       +                        -o "${f}" \
       +                        -- \
       +                        "${t}"
       +        done
       +
       +
       +Features
       +--------
       +
       +- Log of all commits from HEAD.
       +- 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.
       +- Atom feed of the commit log (atom.xml).
       +- Atom feed of the tags/refs (tags.xml).
       +- Make index page for multiple repositories with stagit-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.
       +- Usable with text-browsers such as dillo, links, lynx and w3m.
       +
       +
       +Cons
       +----
       +
       +- Not suitable for large repositories (2000+ commits), because diffstats are
       +  an expensive operation, the cache (-c flag) is a workaround for this in
       +  some cases.
       +- Not suitable for large repositories with many files, because all files are
       +  written for each execution of stagit. This is because stagit shows the lines
       +  of textfiles and there is no "cache" for file metadata (this would add more
       +  complexity to the code).
       +- Not suitable for repositories with many branches, a quite linear history is
       +  assumed (from HEAD).
       +
       +  In these cases it is better to just use cgit or possibly change stagit to
       +  run as a CGI program.
       +
       +- Relatively slow to run the first time (about 3 seconds for sbase,
       +  1500+ commits), incremental updates are faster.
       +- Does not support some of the dynamic features cgit has, like:
       +  - Snapshot tarballs per commit.
       +  - File tree per commit.
       +  - History log of branches diverged from HEAD.
       +  - Stats (git shortlog -s).
       +
       +  This is by design, just use git locally.
 (DIR) diff --git a/compat.h b/compat.h
       @@ -0,0 +1,6 @@
       +#undef strlcat
       +size_t strlcat(char *, const char *, size_t);
       +#undef strlcpy
       +size_t strlcpy(char *, const char *, size_t);
       +#undef reallocarray
       +void *reallocarray(void *, size_t, size_t);
 (DIR) diff --git a/example_create.sh b/example_create.sh
       @@ -0,0 +1,43 @@
       +#!/bin/sh
       +# - Makes index for repositories in a single directory.
       +# - Makes static pages for each repository directory.
       +#
       +# NOTE, things to do manually (once) before running this script:
       +# - copy style.css, logo.png and favicon.png manually, a style.css example
       +#   is included.
       +#
       +# - write clone URL, for example "git://git.codemadness.org/dir" to the "url"
       +#   file for each repo.
       +# - write owner of repo to the "owner" file.
       +# - write description in "description" file.
       +#
       +# Usage:
       +# - mkdir -p htmldir && cd htmldir
       +# - sh example_create.sh
       +
       +# path must be absolute.
       +reposdir="/var/www/domains/git.codemadness.nl/home/src"
       +curdir="$(pwd)"
       +
       +# make index.
       +stagit-index "${reposdir}/"*/ > "${curdir}/index.html"
       +
       +# make files per repo.
       +for dir in "${reposdir}/"*/; do
       +        # strip .git suffix.
       +        r=$(basename "${dir}")
       +        d=$(basename "${dir}" ".git")
       +        printf "%s... " "${d}"
       +
       +        mkdir -p "${curdir}/${d}"
       +        cd "${curdir}/${d}" || continue
       +        stagit -c ".cache" -u "https://git.codemadness.nl/$d/" "${reposdir}/${r}"
       +
       +        # symlinks
       +        ln -sf log.html index.html
       +        ln -sf ../style.css style.css
       +        ln -sf ../logo.png logo.png
       +        ln -sf ../favicon.png favicon.png
       +
       +        echo "done"
       +done
 (DIR) diff --git a/example_post-receive.sh b/example_post-receive.sh
       @@ -0,0 +1,73 @@
       +#!/bin/sh
       +# generic git post-receive hook.
       +# change the config options below and call this script in your post-receive
       +# hook or symlink it.
       +#
       +# usage: $0 [name]
       +#
       +# if name is not set the basename of the current directory is used,
       +# this is the directory of the repo when called from the post-receive script.
       +
       +# NOTE: needs to be set for correct locale (expects UTF-8) otherwise the
       +#       default is LC_CTYPE="POSIX".
       +export LC_CTYPE="en_US.UTF-8"
       +
       +name="$1"
       +if test "${name}" = ""; then
       +        name=$(basename "$(pwd)")
       +fi
       +
       +# config
       +# paths must be absolute.
       +reposdir="/home/src/src"
       +dir="${reposdir}/${name}"
       +htmldir="/home/www/domains/git.codemadness.org/htdocs"
       +stagitdir="/"
       +destdir="${htmldir}${stagitdir}"
       +cachefile=".htmlcache"
       +# /config
       +
       +if ! test -d "${dir}"; then
       +        echo "${dir} does not exist" >&2
       +        exit 1
       +fi
       +cd "${dir}" || exit 1
       +
       +# detect git push -f
       +force=0
       +while read -r old new ref; do
       +        test "${old}" = "0000000000000000000000000000000000000000" && continue
       +        test "${new}" = "0000000000000000000000000000000000000000" && continue
       +
       +        hasrevs=$(git rev-list "${old}" "^${new}" | sed 1q)
       +        if test -n "${hasrevs}"; then
       +                force=1
       +                break
       +        fi
       +done
       +
       +# strip .git suffix.
       +r=$(basename "${name}")
       +d=$(basename "${name}" ".git")
       +printf "[%s] stagit HTML pages... " "${d}"
       +
       +mkdir -p "${destdir}/${d}"
       +cd "${destdir}/${d}" || exit 1
       +
       +# remove commits and ${cachefile} on git push -f, this recreated later on.
       +if test "${force}" = "1"; then
       +        rm -f "${cachefile}"
       +        rm -rf "commit"
       +fi
       +
       +# make index.
       +stagit-index "${reposdir}/"*/ > "${destdir}/index.html"
       +
       +# make pages.
       +stagit -c "${cachefile}" -u "https://git.codemadness.nl/$d/" "${reposdir}/${r}"
       +
       +ln -sf log.html index.html
       +ln -sf ../style.css style.css
       +ln -sf ../logo.png logo.png
       +
       +echo "done"
 (DIR) diff --git a/favicon.png b/favicon.png
       Binary files differ.
 (DIR) diff --git a/logo.png b/logo.png
       Binary files differ.
 (DIR) diff --git a/reallocarray.c b/reallocarray.c
       @@ -0,0 +1,39 @@
       +/*
       + * Copyright (c) 2008 Otto Moerbeek <otto@drijf.net>
       + *
       + * Permission to use, copy, modify, and distribute this software for any
       + * purpose with or without fee is hereby granted, provided that the above
       + * copyright notice and this permission notice appear in all copies.
       + *
       + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
       + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
       + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
       + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
       + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
       + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
       + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
       + */
       +
       +#include <sys/types.h>
       +#include <errno.h>
       +#include <stdint.h>
       +#include <stdlib.h>
       +
       +#include "compat.h"
       +
       +/*
       + * This is sqrt(SIZE_MAX+1), as s1*s2 <= SIZE_MAX
       + * if both s1 < MUL_NO_OVERFLOW and s2 < MUL_NO_OVERFLOW
       + */
       +#define MUL_NO_OVERFLOW        (1UL << (sizeof(size_t) * 4))
       +
       +void *
       +reallocarray(void *optr, size_t nmemb, size_t size)
       +{
       +        if ((nmemb >= MUL_NO_OVERFLOW || size >= MUL_NO_OVERFLOW) &&
       +            nmemb > 0 && SIZE_MAX / nmemb < size) {
       +                errno = ENOMEM;
       +                return NULL;
       +        }
       +        return realloc(optr, size * nmemb);
       +}
 (DIR) diff --git a/stagit-index.1 b/stagit-index.1
       @@ -0,0 +1,47 @@
       +.Dd August 2, 2021
       +.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 EXAMPLES
       +.Bd -literal
       +cd htmlroot
       +stagit-index path/to/gitrepo1 path/to/gitrepo2 > index.html
       +.Ed
       +.Sh SEE ALSO
       +.Xr stagit 1
       +.Sh AUTHORS
       +.An Hiltjo Posthuma Aq Mt hiltjo@codemadness.org
 (DIR) diff --git a/stagit-index.c b/stagit-index.c
       @@ -0,0 +1,258 @@
       +#include <err.h>
       +#include <limits.h>
       +#include <stdio.h>
       +#include <stdlib.h>
       +#include <string.h>
       +#include <time.h>
       +#include <unistd.h>
       +
       +#include <git2.h>
       +
       +static git_repository *repo;
       +
       +static const char *relpath = "";
       +
       +static char description[255] = "Repositories";
       +static char *name = "";
       +static char owner[255];
       +
       +/* 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);
       +}
       +
       +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);
       +}
       +
       +/* Percent-encode, see RFC3986 section 2.1. */
       +void
       +percentencode(FILE *fp, const char *s, size_t len)
       +{
       +        static char tab[] = "0123456789ABCDEF";
       +        unsigned char uc;
       +        size_t i;
       +
       +        for (i = 0; *s && i < len; s++, i++) {
       +                uc = *s;
       +                /* NOTE: do not encode '/' for paths or ",-." */
       +                if (uc < ',' || uc >= 127 || (uc >= ':' && uc <= '@') ||
       +                    uc == '[' || uc == ']') {
       +                        putc('%', fp);
       +                        putc(tab[(uc >> 4) & 0x0f], fp);
       +                        putc(tab[uc & 0x0f], fp);
       +                } else {
       +                        putc(uc, 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);
       +                }
       +        }
       +}
       +
       +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)
       +{
       +        fputs("<!DOCTYPE html>\n"
       +                "<html>\n<head>\n"
       +                "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n"
       +                "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n"
       +                "<title>", fp);
       +        xmlencode(fp, description, strlen(description));
       +        fprintf(fp, "</title>\n<link rel=\"icon\" type=\"image/png\" href=\"%sfavicon.png\" />\n", relpath);
       +        fprintf(fp, "<link rel=\"stylesheet\" type=\"text/css\" href=\"%sstyle.css\" />\n", relpath);
       +        fputs("</head>\n<body>\n", fp);
       +        fprintf(fp, "<table>\n<tr><td><img src=\"%slogo.png\" alt=\"\" width=\"32\" height=\"32\" /></td>\n"
       +                "<td><span class=\"desc\">", relpath);
       +        xmlencode(fp, description, strlen(description));
       +        fputs("</span></td></tr><tr><td></td><td>\n"
       +                "</td></tr>\n</table>\n<hr/>\n<div id=\"content\">\n"
       +                "<table id=\"index\"><thead>\n"
       +                "<tr><td><b>Name</b></td><td><b>Description</b></td><td><b>Owner</b></td>"
       +                "<td><b>Last commit</b></td></tr>"
       +                "</thead><tbody>\n", fp);
       +}
       +
       +void
       +writefooter(FILE *fp)
       +{
       +        fputs("</tbody>\n</table>\n</div>\n</body>\n</html>\n", 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;
       +        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';
       +
       +        fputs("<tr><td><a href=\"", fp);
       +        percentencode(fp, stripped_name, strlen(stripped_name));
       +        fputs("/log.html\">", fp);
       +        xmlencode(fp, stripped_name, strlen(stripped_name));
       +        fputs("</a></td><td>", fp);
       +        xmlencode(fp, description, strlen(description));
       +        fputs("</td><td>", fp);
       +        xmlencode(fp, owner, strlen(owner));
       +        fputs("</td><td>", fp);
       +        if (author)
       +                printtimeshort(fp, &(author->when));
       +        fputs("</td></tr>", fp);
       +
       +        git_commit_free(commit);
       +err:
       +        git_revwalk_free(w);
       +        free(stripped_name);
       +
       +        return ret;
       +}
       +
       +int
       +main(int argc, char *argv[])
       +{
       +        FILE *fp;
       +        char path[PATH_MAX], repodirabs[PATH_MAX + 1];
       +        const char *repodir;
       +        int i, ret = 0;
       +
       +        if (argc < 2) {
       +                fprintf(stderr, "usage: %s [repodir...]\n", argv[0]);
       +                return 1;
       +        }
       +
       +        /* 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
       +
       +        writeheader(stdout);
       +
       +        for (i = 1; i < argc; i++) {
       +                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[0] = '\0';
       +                        checkfileerror(fp, "description", 'r');
       +                        fclose(fp);
       +                }
       +
       +                /* read owner or .git/owner */
       +                joinpath(path, sizeof(path), repodir, "owner");
       +                if (!(fp = fopen(path, "r"))) {
       +                        joinpath(path, sizeof(path), repodir, ".git/owner");
       +                        fp = fopen(path, "r");
       +                }
       +                owner[0] = '\0';
       +                if (fp) {
       +                        if (!fgets(owner, sizeof(owner), fp))
       +                                owner[0] = '\0';
       +                        checkfileerror(fp, "owner", 'r');
       +                        fclose(fp);
       +                        owner[strcspn(owner, "\n")] = '\0';
       +                }
       +                writelog(stdout);
       +        }
       +        writefooter(stdout);
       +
       +        /* cleanup */
       +        git_repository_free(repo);
       +        git_libgit2_shutdown();
       +
       +        checkfileerror(stdout, "<stdout>", 'w');
       +
       +        return ret;
       +}
 (DIR) diff --git a/stagit.1 b/stagit.1
       @@ -0,0 +1,125 @@
       +.Dd August 2, 2021
       +.Dt STAGIT 1
       +.Os
       +.Sh NAME
       +.Nm stagit
       +.Nd static git page generator
       +.Sh SYNOPSIS
       +.Nm
       +.Op Fl c Ar cachefile
       +.Op Fl l Ar commits
       +.Op Fl u Ar baseurl
       +.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.
       +.It Fl l Ar commits
       +Write a maximum number of
       +.Ar commits
       +to the log.html 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.
       +For example: "https://git.codemadness.org/stagit/".
       +.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.html
       +List of files in the latest tree, linking to the file.
       +.It log.html
       +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.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.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 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 EXIT STATUS
       +.Ex -std
       +.Sh EXAMPLES
       +.Bd -literal
       +mkdir -p htmlroot/htmlrepo1 && cd htmlroot/htmlrepo1
       +stagit path/to/gitrepo1
       +# repeat for other repositories.
       +.Ed
       +.Pp
       +To update the HTML 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-index 1
       +.Sh AUTHORS
       +.An Hiltjo Posthuma Aq Mt hiltjo@codemadness.org
 (DIR) diff --git a/stagit.c b/stagit.c
       @@ -0,0 +1,1465 @@
       +#include <sys/stat.h>
       +#include <sys/types.h>
       +
       +#include <err.h>
       +#include <errno.h>
       +#include <libgen.h>
       +#include <limits.h>
       +#include <stdint.h>
       +#include <stdio.h>
       +#include <stdlib.h>
       +#include <string.h>
       +#include <time.h>
       +#include <unistd.h>
       +
       +#include <git2.h>
       +
       +#include "compat.h"
       +
       +#define LEN(s)    (sizeof(s)/sizeof(*s))
       +
       +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);
       +}
       +
       +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;
       +}
       +
       +/* Percent-encode, see RFC3986 section 2.1. */
       +void
       +percentencode(FILE *fp, const char *s, size_t len)
       +{
       +        static char tab[] = "0123456789ABCDEF";
       +        unsigned char uc;
       +        size_t i;
       +
       +        for (i = 0; *s && i < len; s++, i++) {
       +                uc = *s;
       +                /* NOTE: do not encode '/' for paths or ",-." */
       +                if (uc < ',' || uc >= 127 || (uc >= ':' && uc <= '@') ||
       +                    uc == '[' || uc == ']') {
       +                        putc('%', fp);
       +                        putc(tab[(uc >> 4) & 0x0f], fp);
       +                        putc(tab[uc & 0x0f], fp);
       +                } else {
       +                        putc(uc, 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 below as HTML 2.0 / XML 1.0, ignore printing '\r', '\n' */
       +void
       +xmlencodeline(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;
       +                case '\r': break; /* ignore CR */
       +                case '\n': break; /* ignore LF */
       +                default:   putc(*s, fp);
       +                }
       +        }
       +}
       +
       +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)
       +{
       +        fputs("<!DOCTYPE html>\n"
       +                "<html>\n<head>\n"
       +                "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n"
       +                "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n"
       +                "<title>", fp);
       +        xmlencode(fp, title, strlen(title));
       +        if (title[0] && strippedname[0])
       +                fputs(" - ", fp);
       +        xmlencode(fp, strippedname, strlen(strippedname));
       +        if (description[0])
       +                fputs(" - ", fp);
       +        xmlencode(fp, description, strlen(description));
       +        fprintf(fp, "</title>\n<link rel=\"icon\" type=\"image/png\" href=\"%sfavicon.png\" />\n", relpath);
       +        fputs("<link rel=\"alternate\" type=\"application/atom+xml\" title=\"", fp);
       +        xmlencode(fp, name, strlen(name));
       +        fprintf(fp, " Atom Feed\" href=\"%satom.xml\" />\n", relpath);
       +        fputs("<link rel=\"alternate\" type=\"application/atom+xml\" title=\"", fp);
       +        xmlencode(fp, name, strlen(name));
       +        fprintf(fp, " Atom Feed (tags)\" href=\"%stags.xml\" />\n", relpath);
       +        fprintf(fp, "<link rel=\"stylesheet\" type=\"text/css\" href=\"%sstyle.css\" />\n", relpath);
       +        fputs("</head>\n<body>\n<table><tr><td>", fp);
       +        fprintf(fp, "<a href=\"../%s\"><img src=\"%slogo.png\" alt=\"\" width=\"32\" height=\"32\" /></a>",
       +                relpath, relpath);
       +        fputs("</td><td><h1>", fp);
       +        xmlencode(fp, strippedname, strlen(strippedname));
       +        fputs("</h1><span class=\"desc\">", fp);
       +        xmlencode(fp, description, strlen(description));
       +        fputs("</span></td></tr>", fp);
       +        if (cloneurl[0]) {
       +                fputs("<tr class=\"url\"><td></td><td>git clone <a href=\"", fp);
       +                xmlencode(fp, cloneurl, strlen(cloneurl)); /* not percent-encoded */
       +                fputs("\">", fp);
       +                xmlencode(fp, cloneurl, strlen(cloneurl));
       +                fputs("</a></td></tr>", fp);
       +        }
       +        fputs("<tr><td></td><td>\n", fp);
       +        fprintf(fp, "<a href=\"%slog.html\">Log</a> | ", relpath);
       +        fprintf(fp, "<a href=\"%sfiles.html\">Files</a> | ", relpath);
       +        fprintf(fp, "<a href=\"%srefs.html\">Refs</a>", relpath);
       +        if (submodules)
       +                fprintf(fp, " | <a href=\"%sfile/%s.html\">Submodules</a>",
       +                        relpath, submodules);
       +        if (readme)
       +                fprintf(fp, " | <a href=\"%sfile/%s.html\">README</a>",
       +                        relpath, readme);
       +        if (license)
       +                fprintf(fp, " | <a href=\"%sfile/%s.html\">LICENSE</a>",
       +                        relpath, license);
       +        fprintf(fp, " | <a href=\"%sreleases.html\">Releases</a>", relpath);
       +        fputs("</td></tr></table>\n<hr/>\n<div id=\"content\">\n", fp);
       +}
       +
       +void
       +writefooter(FILE *fp)
       +{
       +        fputs("</div>\n</body>\n</html>\n", fp);
       +}
       +
       +size_t
       +writeblobhtml(FILE *fp, const git_blob *blob)
       +{
       +        size_t n = 0, i, len, prev;
       +        const char *nfmt = "<a href=\"#l%zu\" class=\"line\" id=\"l%zu\">%7zu</a> ";
       +        const char *s = git_blob_rawcontent(blob);
       +
       +        len = git_blob_rawsize(blob);
       +        fputs("<pre id=\"blob\">\n", fp);
       +
       +        if (len > 0) {
       +                for (i = 0, prev = 0; i < len; i++) {
       +                        if (s[i] != '\n')
       +                                continue;
       +                        n++;
       +                        fprintf(fp, nfmt, n, n, n);
       +                        xmlencodeline(fp, &s[prev], i - prev + 1);
       +                        putc('\n', fp);
       +                        prev = i + 1;
       +                }
       +                /* trailing data */
       +                if ((len - prev) > 0) {
       +                        n++;
       +                        fprintf(fp, nfmt, n, n, n);
       +                        xmlencodeline(fp, &s[prev], len - prev);
       +                }
       +        }
       +
       +        fputs("</pre>\n", fp);
       +
       +        return n;
       +}
       +
       +void
       +printcommit(FILE *fp, struct commitinfo *ci)
       +{
       +        fprintf(fp, "<b>commit</b> <a href=\"%scommit/%s.html\">%s</a>\n",
       +                relpath, ci->oid, ci->oid);
       +
       +        if (ci->parentoid[0])
       +                fprintf(fp, "<b>parent</b> <a href=\"%scommit/%s.html\">%s</a>\n",
       +                        relpath, ci->parentoid, ci->parentoid);
       +
       +        if (ci->author) {
       +                fputs("<b>Author:</b> ", fp);
       +                xmlencode(fp, ci->author->name, strlen(ci->author->name));
       +                fputs(" &lt;<a href=\"mailto:", fp);
       +                xmlencode(fp, ci->author->email, strlen(ci->author->email)); /* not percent-encoded */
       +                fputs("\">", fp);
       +                xmlencode(fp, ci->author->email, strlen(ci->author->email));
       +                fputs("</a>&gt;\n<b>Date:</b>   ", fp);
       +                printtime(fp, &(ci->author->when));
       +                putc('\n', fp);
       +        }
       +        if (ci->msg) {
       +                putc('\n', fp);
       +                xmlencode(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 linestr[80];
       +        int c;
       +
       +        printcommit(fp, ci);
       +
       +        if (!ci->deltas)
       +                return;
       +
       +        if (ci->filecount > 1000   ||
       +            ci->ndeltas   > 1000   ||
       +            ci->addcount  > 100000 ||
       +            ci->delcount  > 100000) {
       +                fputs("Diff is too large, output suppressed.\n", fp);
       +                return;
       +        }
       +
       +        /* diff stat */
       +        fputs("<b>Diffstat:</b>\n<table>", 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 (c == ' ')
       +                        fprintf(fp, "<tr><td>%c", c);
       +                else
       +                        fprintf(fp, "<tr><td class=\"%c\">%c", c, c);
       +
       +                fprintf(fp, "</td><td><a href=\"#h%zu\">", i);
       +                xmlencode(fp, delta->old_file.path, strlen(delta->old_file.path));
       +                if (strcmp(delta->old_file.path, delta->new_file.path)) {
       +                        fputs(" -&gt; ", fp);
       +                        xmlencode(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, "</a></td><td> | </td><td class=\"num\">%zu</td><td><span class=\"i\">",
       +                        ci->deltas[i]->addcount + ci->deltas[i]->delcount);
       +                fwrite(&linestr, 1, add, fp);
       +                fputs("</span><span class=\"d\">", fp);
       +                fwrite(&linestr[add], 1, del, fp);
       +                fputs("</span></td></tr>\n", fp);
       +        }
       +        fprintf(fp, "</table></pre><pre>%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("<hr/>", fp);
       +
       +        for (i = 0; i < ci->ndeltas; i++) {
       +                patch = ci->deltas[i]->patch;
       +                delta = git_patch_get_delta(patch);
       +                fprintf(fp, "<b>diff --git a/<a id=\"h%zu\" href=\"%sfile/", i, relpath);
       +                percentencode(fp, delta->old_file.path, strlen(delta->old_file.path));
       +                fputs(".html\">", fp);
       +                xmlencode(fp, delta->old_file.path, strlen(delta->old_file.path));
       +                fprintf(fp, "</a> b/<a href=\"%sfile/", relpath);
       +                percentencode(fp, delta->new_file.path, strlen(delta->new_file.path));
       +                fprintf(fp, ".html\">");
       +                xmlencode(fp, delta->new_file.path, strlen(delta->new_file.path));
       +                fprintf(fp, "</a></b>\n");
       +
       +                /* 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;
       +
       +                        fprintf(fp, "<a href=\"#h%zu-%zu\" id=\"h%zu-%zu\" class=\"h\">", i, j, i, j);
       +                        xmlencode(fp, hunk->header, hunk->header_len);
       +                        fputs("</a>", fp);
       +
       +                        for (k = 0; ; k++) {
       +                                if (git_patch_get_line_in_hunk(&line, patch, j, k))
       +                                        break;
       +                                if (line->old_lineno == -1)
       +                                        fprintf(fp, "<a href=\"#h%zu-%zu-%zu\" id=\"h%zu-%zu-%zu\" class=\"i\">+",
       +                                                i, j, k, i, j, k);
       +                                else if (line->new_lineno == -1)
       +                                        fprintf(fp, "<a href=\"#h%zu-%zu-%zu\" id=\"h%zu-%zu-%zu\" class=\"d\">-",
       +                                                i, j, k, i, j, k);
       +                                else
       +                                        putc(' ', fp);
       +                                xmlencodeline(fp, line->content, line->content_len);
       +                                putc('\n', fp);
       +                                if (line->old_lineno == -1 || line->new_lineno == -1)
       +                                        fputs("</a>", fp);
       +                        }
       +                }
       +        }
       +}
       +
       +void
       +writelogline(FILE *fp, struct commitinfo *ci)
       +{
       +        fputs("<tr><td>", fp);
       +        if (ci->author)
       +                printtimeshort(fp, &(ci->author->when));
       +        fputs("</td><td>", fp);
       +        if (ci->summary) {
       +                fprintf(fp, "<a href=\"%scommit/%s.html\">", relpath, ci->oid);
       +                xmlencode(fp, ci->summary, strlen(ci->summary));
       +                fputs("</a>", fp);
       +        }
       +        fputs("</td><td>", fp);
       +        if (ci->author)
       +                xmlencode(fp, ci->author->name, strlen(ci->author->name));
       +        fputs("</td><td class=\"num\" align=\"right\">", fp);
       +        fprintf(fp, "%zu", ci->filecount);
       +        fputs("</td><td class=\"num\" align=\"right\">", fp);
       +        fprintf(fp, "+%zu", ci->addcount);
       +        fputs("</td><td class=\"num\" align=\"right\">", fp);
       +        fprintf(fp, "-%zu", ci->delcount);
       +        fputs("</td></tr>\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)) {
       +                relpath = "";
       +
       +                if (cachefile && !memcmp(&id, &lastoid, sizeof(id)))
       +                        break;
       +
       +                git_oid_tostr(oidstr, sizeof(oidstr), &id);
       +                r = snprintf(path, sizeof(path), "commit/%s.html", oidstr);
       +                if (r < 0 || (size_t)r >= sizeof(path))
       +                        errx(1, "path truncated: 'commit/%s.html'", 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;
       +                /* diffstat: for stagit HTML required for the log.html line */
       +                if (commitinfo_getstats(ci) == -1)
       +                        goto err;
       +
       +                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) {
       +                        relpath = "../";
       +                        fpfile = efopen(path, "w");
       +                        writeheader(fpfile, ci->summary);
       +                        fputs("<pre>", fpfile);
       +                        printshowfile(fpfile, ci);
       +                        fputs("</pre>\n", fpfile);
       +                        writefooter(fpfile);
       +                        checkfileerror(fpfile, path, 'w');
       +                        fclose(fpfile);
       +                }
       +err:
       +                commitinfo_free(ci);
       +        }
       +        git_revwalk_free(w);
       +
       +        if (nlogcommits == 0 && remcommits != 0) {
       +                fprintf(fp, "<tr><td></td><td colspan=\"5\">"
       +                        "%zu more commits remaining, fetch the repository"
       +                        "</td></tr>\n", remcommits);
       +        }
       +
       +        relpath = "";
       +
       +        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\" type=\"text/html\" href=\"%scommit/%s.html\" />\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;
       +        const char *p;
       +        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;
       +
       +        for (p = fpath, tmp[0] = '\0'; *p; p++) {
       +                if (*p == '/' && strlcat(tmp, "../", sizeof(tmp)) >= sizeof(tmp))
       +                        errx(1, "path truncated: '../%s'", tmp);
       +        }
       +        relpath = tmp;
       +
       +        fp = efopen(fpath, "w");
       +        writeheader(fp, filename);
       +        fputs("<p> ", fp);
       +        xmlencode(fp, filename, strlen(filename));
       +        fprintf(fp, " (%zuB)", filesize);
       +        fputs("</p><hr/>", fp);
       +
       +        if (git_blob_is_binary((git_blob *)obj))
       +                fputs("<p>Binary file.</p>\n", fp);
       +        else
       +                lc = writeblobhtml(fp, (git_blob *)obj);
       +
       +        writefooter(fp);
       +        checkfileerror(fp, fpath, 'w');
       +        fclose(fp);
       +
       +        relpath = "";
       +
       +        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
       +writereleases(FILE *fp)
       +{
       +fputs("<tr><td>HEllo WOlrd Releas",fp);
       +}
       +
       +
       +int
       +writefilestree(FILE *fp, git_tree *tree, const char *path)
       +{
       +        const git_tree_entry *entry = NULL;
       +        git_object *obj = NULL;
       +        const char *entryname;
       +        char 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.html",
       +                         entrypath);
       +                if (r < 0 || (size_t)r >= sizeof(filepath))
       +                        errx(1, "path truncated: 'file/%s.html'", 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("<tr><td>", fp);
       +                        fputs(filemode(git_tree_entry_filemode(entry)), fp);
       +                        fprintf(fp, "</td><td><a href=\"%s", relpath);
       +                        percentencode(fp, filepath, strlen(filepath));
       +                        fputs("\">", fp);
       +                        xmlencode(fp, entrypath, strlen(entrypath));
       +                        fputs("</a></td><td class=\"num\" align=\"right\">", fp);
       +                        if (lc > 0)
       +                                fprintf(fp, "%zuL", lc);
       +                        else
       +                                fprintf(fp, "%zuB", filesize);
       +                        fputs("</td></tr>\n", fp);
       +                        git_object_free(obj);
       +                } else if (git_tree_entry_type(entry) == GIT_OBJ_COMMIT) {
       +                        /* commit object in tree is a submodule */
       +                        fprintf(fp, "<tr><td>m---------</td><td><a href=\"%sfile/.gitmodules.html\">",
       +                                relpath);
       +                        xmlencode(fp, entrypath, strlen(entrypath));
       +                        fputs("</a> @ ", fp);
       +                        git_oid_tostr(oid, sizeof(oid), git_tree_entry_id(entry));
       +                        xmlencode(fp, oid, strlen(oid));
       +                        fputs("</td><td class=\"num\" align=\"right\"></td></tr>\n", fp);
       +                }
       +        }
       +
       +        return 0;
       +}
       +
       +int
       +writefiles(FILE *fp, const git_oid *id)
       +{
       +        git_tree *tree = NULL;
       +        git_commit *commit = NULL;
       +        int ret = -1;
       +
       +        fputs("<table id=\"files\"><thead>\n<tr>"
       +              "<td><b>Mode</b></td><td><b>Name</b></td>"
       +              "<td class=\"num\" align=\"right\"><b>Size</b></td>"
       +              "</tr>\n</thead><tbody>\n", fp);
       +
       +        if (!git_commit_lookup(&commit, repo, id) &&
       +            !git_commit_tree(&tree, commit))
       +                ret = writefilestree(fp, tree, "");
       +
       +        fputs("</tbody></table>", fp);
       +
       +        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 *ids[] = { "branches", "tags" };
       +        const char *s;
       +
       +        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)) {
       +                        if (count)
       +                                fputs("</tbody></table><br/>\n", fp);
       +                        count = 0;
       +                        j = 1;
       +                }
       +
       +                /* print header if it has an entry (first). */
       +                if (++count == 1) {
       +                        if (j) {
       +                        fprintf(fp, "<h2>%s</h2><table id=\"%s\">"
       +                                "<thead>\n<tr><td><b>Name</b></td>"
       +                                "<td><b>Download</b></td>"
       +                                "<td><b>Commit date</b></td>"
       +                                "<td><b>Author</b></td>\n</tr>\n"
       +                                "</thead><tbody>\n",
       +                                 titles[j], ids[j]);
       +                        }
       +                        else {
       +                        fprintf(fp, "<h2>%s</h2><table id=\"%s\">"
       +                                "<thead>\n<tr><td><b>Name</b></td>"
       +                                "<td><b>Commit message</b></td>"
       +                                "<td><b>Last commit date</b></td>"
       +                                "<td><b>Author</b></td>\n</tr>\n"
       +                                "</thead><tbody>\n",
       +                                 titles[j], ids[j]);
       +                        }
       +                }
       +
       +                ci = ris[i].ci;
       +                s = git_reference_shorthand(ris[i].ref);
       +
       +                fputs("<tr><td>", fp);
       +                xmlencode(fp, s, strlen(s));
       +                fputs("</td><td>", fp);
       +                if(j) 
       +                        //create archive
       +                        git_archive_options options = GIT_ARCHIVE_OPTIONS_INIT;
       +                        
       +                        fprintf(fp, "<a href=\"%ssnapshot/\%s-%s.tar.gz\">%s-%s.tar.gz</a> ", relpath,name,s,name,s);
       +                if(!j)  
       +                        xmlencode(fp, ci->summary, strlen(ci->summary));
       +                fputs("</td><td>", fp);
       +                if (ci->author)
       +                        printtimeshort(fp, &(ci->author->when));
       +                fputs("</td><td>", fp);
       +                if (ci->author)
       +                        xmlencode(fp, ci->author->name, strlen(ci->author->name));
       +                fputs("</td></tr>\n", fp);
       +        }
       +        /* table footer */
       +        if (count)
       +                fputs("</tbody></table><br/>\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 [-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;
       +
       +        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 (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.html", "w");
       +        relpath = "";
       +        mkdir("commit", S_IRWXU | S_IRWXG | S_IRWXO);
       +        writeheader(fp, "Log");
       +        fputs("<table id=\"log\"><thead>\n<tr><td><b>Date</b></td>"
       +              "<td><b>Commit message</b></td>"
       +              "<td><b>Author</b></td><td class=\"num\" align=\"right\"><b>Files</b></td>"
       +              "<td class=\"num\" align=\"right\"><b>+</b></td>"
       +              "<td class=\"num\" align=\"right\"><b>-</b></td></tr>\n</thead><tbody>\n", fp);
       +
       +        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.html 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);
       +        }
       +
       +        fputs("</tbody></table>", fp);
       +        writefooter(fp);
       +        checkfileerror(fp, "log.html", 'w');
       +        fclose(fp);
       +
       +        /* files for HEAD */
       +        fp = efopen("files.html", "w");
       +        writeheader(fp, "Files");
       +        if (head)
       +                writefiles(fp, head);
       +        writefooter(fp);
       +        checkfileerror(fp, "files.html", 'w');
       +        fclose(fp);
       +
       +        /* summary page with branches and tags */
       +        fp = efopen("refs.html", "w");
       +        writeheader(fp, "Refs");
       +        writerefs(fp);
       +        writefooter(fp);
       +        checkfileerror(fp, "refs.html", 'w');
       +        fclose(fp);
       +
       +        /* summary page with releases */
       +        fp = efopen("releases.html", "w");
       +        writeheader(fp, "Releases");
       +        writereleases(fp);
       +        writefooter(fp);
       +        checkfileerror(fp, "releases.html", '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;
       +}
 (DIR) diff --git a/strlcat.c b/strlcat.c
       @@ -0,0 +1,57 @@
       +/*        $OpenBSD: strlcat.c,v 1.15 2015/03/02 21:41:08 millert Exp $        */
       +
       +/*
       + * Copyright (c) 1998, 2015 Todd C. Miller <Todd.Miller@courtesan.com>
       + *
       + * Permission to use, copy, modify, and distribute this software for any
       + * purpose with or without fee is hereby granted, provided that the above
       + * copyright notice and this permission notice appear in all copies.
       + *
       + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
       + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
       + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
       + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
       + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
       + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
       + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
       + */
       +
       +#include <sys/types.h>
       +#include <string.h>
       +
       +#include "compat.h"
       +
       +/*
       + * Appends src to string dst of size dsize (unlike strncat, dsize is the
       + * full size of dst, not space left).  At most dsize-1 characters
       + * will be copied.  Always NUL terminates (unless dsize <= strlen(dst)).
       + * Returns strlen(src) + MIN(dsize, strlen(initial dst)).
       + * If retval >= dsize, truncation occurred.
       + */
       +size_t
       +strlcat(char *dst, const char *src, size_t dsize)
       +{
       +        const char *odst = dst;
       +        const char *osrc = src;
       +        size_t n = dsize;
       +        size_t dlen;
       +
       +        /* Find the end of dst and adjust bytes left but don't go past end. */
       +        while (n-- != 0 && *dst != '\0')
       +                dst++;
       +        dlen = dst - odst;
       +        n = dsize - dlen;
       +
       +        if (n-- == 0)
       +                return(dlen + strlen(src));
       +        while (*src != '\0') {
       +                if (n != 0) {
       +                        *dst++ = *src;
       +                        n--;
       +                }
       +                src++;
       +        }
       +        *dst = '\0';
       +
       +        return(dlen + (src - osrc));        /* count does not include NUL */
       +}
 (DIR) diff --git a/strlcpy.c b/strlcpy.c
       @@ -0,0 +1,52 @@
       +/*        $OpenBSD: strlcpy.c,v 1.12 2015/01/15 03:54:12 millert Exp $        */
       +
       +/*
       + * Copyright (c) 1998, 2015 Todd C. Miller <Todd.Miller@courtesan.com>
       + *
       + * Permission to use, copy, modify, and distribute this software for any
       + * purpose with or without fee is hereby granted, provided that the above
       + * copyright notice and this permission notice appear in all copies.
       + *
       + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
       + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
       + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
       + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
       + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
       + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
       + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
       + */
       +
       +#include <sys/types.h>
       +#include <string.h>
       +
       +#include "compat.h"
       +
       +/*
       + * Copy string src to buffer dst of size dsize.  At most dsize-1
       + * chars will be copied.  Always NUL terminates (unless dsize == 0).
       + * Returns strlen(src); if retval >= dsize, truncation occurred.
       + */
       +size_t
       +strlcpy(char *dst, const char *src, size_t dsize)
       +{
       +        const char *osrc = src;
       +        size_t nleft = dsize;
       +
       +        /* Copy as many bytes as will fit. */
       +        if (nleft != 0) {
       +                while (--nleft != 0) {
       +                        if ((*dst++ = *src++) == '\0')
       +                                break;
       +                }
       +        }
       +
       +        /* Not enough room in dst, add NUL and traverse rest of src. */
       +        if (nleft == 0) {
       +                if (dsize != 0)
       +                        *dst = '\0';                /* NUL-terminate dst */
       +                while (*src++)
       +                        ;
       +        }
       +
       +        return(src - osrc - 1);        /* count does not include NUL */
       +}
 (DIR) diff --git a/style.css b/style.css
       @@ -0,0 +1,154 @@
       +body {
       +        color: #000;
       +        background-color: #fff;
       +        font-family: monospace;
       +}
       +
       +h1, h2, h3, h4, h5, h6 {
       +        font-size: 1em;
       +        margin: 0;
       +}
       +
       +img, h1, h2 {
       +        vertical-align: middle;
       +}
       +
       +img {
       +        border: 0;
       +}
       +
       +a:target {
       +        background-color: #ccc;
       +}
       +
       +a.d,
       +a.h,
       +a.i,
       +a.line {
       +        text-decoration: none;
       +}
       +
       +#blob a {
       +        color: #555;
       +}
       +
       +#blob a:hover {
       +        color: blue;
       +        text-decoration: none;
       +}
       +
       +table thead td {
       +        font-weight: bold;
       +}
       +
       +table td {
       +        padding: 0 0.4em;
       +}
       +
       +#content table td {
       +        vertical-align: top;
       +        white-space: nowrap;
       +}
       +
       +#branches tr:hover td,
       +#tags tr:hover td,
       +#index tr:hover td,
       +#log tr:hover td,
       +#files tr:hover td {
       +        background-color: #eee;
       +}
       +
       +#index tr td:nth-child(2),
       +#tags tr td:nth-child(3),
       +#branches tr td:nth-child(3),
       +#log tr td:nth-child(2) {
       +        white-space: normal;
       +}
       +
       +td.num {
       +        text-align: right;
       +}
       +
       +.desc {
       +        color: #555;
       +}
       +
       +hr {
       +        border: 0;
       +        border-top: 1px solid #555;
       +        height: 1px;
       +}
       +
       +pre {
       +        font-family: monospace;
       +}
       +
       +pre a.h {
       +        color: #00a;
       +}
       +
       +.A,
       +span.i,
       +pre a.i {
       +        color: #070;
       +}
       +
       +.D,
       +span.d,
       +pre a.d {
       +        color: #e00;
       +}
       +
       +pre a.h:hover,
       +pre a.i:hover,
       +pre a.d:hover {
       +        text-decoration: none;
       +}
       +
       +@media (prefers-color-scheme: dark) {
       +        body {
       +                background-color: #000;
       +                color: #bdbdbd;
       +        }
       +        hr {
       +                border-color: #222;
       +        }
       +        a {
       +                color: #56c8ff;
       +        }
       +        a:target {
       +                background-color: #222;
       +        }
       +        .desc {
       +                color: #aaa;
       +        }
       +        #blob a {
       +                color: #555;
       +        }
       +        #blob a:target {
       +                color: #eee;
       +        }
       +        #blob a:hover {
       +                color: #56c8ff;
       +        }
       +        pre a.h {
       +                color: #00cdcd;
       +        }
       +        .A,
       +        span.i,
       +        pre a.i {
       +                color: #00cd00;
       +        }
       +        .D,
       +        span.d,
       +        pre a.d {
       +                color: #cd0000;
       +        }
       +        #branches tr:hover td,
       +        #tags tr:hover td,
       +        #index tr:hover td,
       +        #log tr:hover td,
       +        #files tr:hover td {
       +                background-color: #111;
       +        }
       +}