STAGIT_CPPFLAGS = -D_XOPEN_SOURCE=700 -D_DEFAULT_SOURCE -D_BSD_SOURCE
SRC = \
- stagit.c\
- stagit-index.c
+ src/stagit.c\
+ src/stagit-index.c
COMPATSRC = \
- reallocarray.c\
- strlcat.c\
- strlcpy.c
+ src/reallocarray.c\
+ src/strlcat.c\
+ src/strlcpy.c
BIN = \
stagit\
stagit-index
MAN1 = \
- stagit.1\
- stagit-index.1
+ man/stagit.1\
+ man/stagit-index.1
DOC = \
LICENSE\
README
-HDR = compat.h
+HDR = src/compat.h
COMPATOBJ = \
- reallocarray.o\
- strlcat.o\
- strlcpy.o
+ src/reallocarray.o\
+ src/strlcat.o\
+ src/strlcpy.o
OBJ = ${SRC:.c=.o} ${COMPATOBJ}
rm -rf ${NAME}-${VERSION}
mkdir -p ${NAME}-${VERSION}
cp -f ${MAN1} ${HDR} ${SRC} ${COMPATSRC} ${DOC} \
- Makefile favicon.png logo.png style.min.css syntax.css highlight.py \
- example_create.sh example_post-receive.sh \
+ Makefile resources/* src/highlight.py contrib/* \
${NAME}-${VERSION}
# make tarball
tar -cf - ${NAME}-${VERSION} | \
${OBJ}: ${HDR}
-stagit: stagit.o ${COMPATOBJ} requirements.txt
+stagit: src/stagit.o ${COMPATOBJ} requirements.txt
pip3 install -r requirements.txt
- ${CC} -o $@ stagit.o ${COMPATOBJ} ${STAGIT_LDFLAGS}
+ ${CC} -o $@ src/stagit.o ${COMPATOBJ} ${STAGIT_LDFLAGS}
-stagit-index: stagit-index.o ${COMPATOBJ}
- ${CC} -o $@ stagit-index.o ${COMPATOBJ} ${STAGIT_LDFLAGS}
+stagit-index: src/stagit-index.o ${COMPATOBJ}
+ ${CC} -o $@ src/stagit-index.o ${COMPATOBJ} ${STAGIT_LDFLAGS}
clean:
rm -f ${BIN} ${OBJ} ${NAME}-${VERSION}.tar.gz
for f in ${BIN}; do chmod 755 ${DESTDIR}${PREFIX}/bin/$$f; done
# installing example files.
mkdir -p ${DESTDIR}${DOCPREFIX}
- cp -f favicon.png\
- logo.png\
- example_create.sh\
- example_post-receive.sh\
+ cp -f resources/*.png\
+ contrib/*\
README\
${DESTDIR}${DOCPREFIX}
mkdir -p ${DESTDIR}${SHAREPREFIX}
- cp -f style.min.css\
- syntax.css\
- highlight.py\
+ cp -f resources/*\
+ src/highlight.py\
${DESTDIR}${SHAREPREFIX}
# 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
+ install -Dm644 man/* ${DESTDIR}${MANPREFIX}/man1
uninstall:
# removing executable files.
for f in ${BIN}; do rm -f ${DESTDIR}${PREFIX}/bin/$$f; done
# removing example files.
- rm -f \
- ${DESTDIR}${SHAREPREFIX}/style.min.css\
- ${DESTDIR}${SHAREPREFIX}/syntax.css\
- ${DESTDIR}${SHAREPREFIX}/highlight.py\
- ${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}
+ rm -f ${DESTDIR}${SHAREPREFIX}/*
+ rm -f ${DESTDIR}${DOCPREFIX}/*
+ rmdir ${DESTDIR}${SHAREPREFIX}
+ rmdir ${DESTDIR}${DOCPREFIX}
# removing manual pages.
for m in ${MAN1}; do rm -f ${DESTDIR}${MANPREFIX}/man1/$$m; done
+++ /dev/null
-#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);
--- /dev/null
+#!/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" "${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
--- /dev/null
+#!/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}" "${reposdir}/${r}"
+
+ln -sf log.html index.html
+ln -sf ../style.css style.css
+ln -sf ../logo.png logo.png
+
+echo "done"
--- /dev/null
+#!/bin/sh
+
+fresh=false
+unique=false
+
+for arg in "$@"; do
+ [ "$unique" = true ] && unique="$arg"
+ [ "$arg" = "--only" ] && unique=true
+ [ "$arg" = "--fresh" ] && fresh=true
+done
+
+[ "$unique" = true ] && {
+ echo "Expected argument after \`--only\`.";
+ exit 1;
+}
+
+STAGIT=/var/www/git/stagit.out
+[ ! -f "$STAGIT" ] && STAGIT=stagit
+
+STAGIT_INDEX=/var/www/git/stagit-index.out
+[ ! -f "$STAGIT_INDEX" ] && STAGIT_INDEX=stagit-index
+
+build_html () {
+ repo="$1"
+ repo="$(basename "$repo" | sed 's/\.git$//g')"
+
+ [ "$fresh" = true ] && {
+ echo "Deleting HTML for $repo.";
+ rm -fr "/var/www/git/$repo";
+ }
+ mkdir -p "/var/www/git/$repo"
+ cd "/var/www/git/$repo" || { echo "Couldn't cd."; exit 1; }
+
+ [ ! -f style.css ] && ln -s ../style.css ./
+ [ ! -f favicon.png ] && ln -s ../favicon.png ./
+ [ ! -f logo.png ] && ln -s ../logo.png ./
+ [ ! -f highlight ] && ln -s ../highlight ./
+ [ ! -f index.html ] && ln -s log.html index.html
+
+ echo "git://git.knutsen.co/$repo" > "/srv/git/$repo.git/url"
+
+ COMMAND="$STAGIT -c ./cachefile /srv/git/$repo.git"
+ echo "Building web-page for $repo."
+ echo "$COMMAND"
+ $COMMAND
+}
+
+if [ "$unique" = false ]; then
+ for repo in /srv/git/*.git; do
+ build_html "$repo"
+ done
+else
+ build_html "$unique"
+fi
+
+echo "Generating index.html with \`$STAGIT_INDEX\`."
+"$STAGIT_INDEX" /srv/git/*.git > /var/www/git/index.html
+
+# Correct ownership of the web files.
+chown git:www-data -R /var/www/git -f
+chmod g+rw -R /var/www/git -f
+++ /dev/null
-#!/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" "${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
+++ /dev/null
-#!/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}" "${reposdir}/${r}"
-
-ln -sf log.html index.html
-ln -sf ../style.css style.css
-ln -sf ../logo.png logo.png
-
-echo "done"
+++ /dev/null
-#!/usr/bin/env python3
-
-import pygments
-from pygments import highlight
-from pygments.formatters import HtmlFormatter
-import pygments.lexers
-from markdown import markdown
-
-from sys import stdin, stderr
-
-filename = stdin.readline().strip()
-contents = stdin.read()
-lexer = None
-
-try:
- lexer = pygments.lexers.guess_lexer_for_filename(filename, contents)
-except pygments.util.ClassNotFound:
- try:
- lexer = pygments.lexers.guess_lexer(contents)
- except pygments.util.ClassNotFound:
- pass
-
-if lexer is None:
- lexer = pygments.lexers.special.TextLexer
-
-rendered = (
- markdown(
- contents,
- extensions=[
- "codehilite",
- "extra",
- "sane_lists",
- "smarty",
- "pymdownx.tasklist",
- ],
- )
- if lexer.__class__ is pygments.lexers.MarkdownLexer
- else None
-)
-
-formatter = HtmlFormatter(
- style="monokai",
- cssclass="highlight",
- linenos="table",
- lineanchors="loc",
- anchorlinenos=True,
-)
-
-outp = ""
-if rendered:
- outp += '<article class="markup markdown">'
- outp += rendered
- outp += "</article>"
-outp += f'<div id="blob">{highlight(contents, lexer, formatter)}</div>"'
-outp += '<link rel="stylesheet" href="/syntax.css"'
-
-print(outp)
-
-print(f"Filename: {filename}; Lexer: {lexer}.", file=stderr)
-
-if rendered:
- print("Markdown was rendered in addition.", file=stderr)
--- /dev/null
+.Dd March 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 SEE ALSO
+.Xr stagit 1
+.Sh AUTHORS
+.An Hiltjo Posthuma Aq Mt hiltjo@codemadness.org
+.An Armaan Bhojwani Aq Mt me@armaanb.net
--- /dev/null
+.Dd March 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
+.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.
+.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
+.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.2f30.org/stagit
+.El
+.Pp
+When a README or LICENSE file exists in HEAD or a .gitmodules submodules file
+exists in HEAD a direct link in the menu is made.
+.Pp
+For changing the style of the page you can use the following files:
+.Bl -tag -width Ds
+.It favicon.png
+favicon image.
+.It logo.png
+32x32 logo.
+.It style.css
+CSS stylesheet.
+.El
+.Sh SEE ALSO
+.Xr stagit-index 1
+.Sh AUTHORS
+.An Hiltjo Posthuma Aq Mt hiltjo@codemadness.org
+.An Armaan Bhojwani Aq Mt me@armaanb.net
+++ /dev/null
-/*
- * 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);
-}
+++ /dev/null
-#!/bin/sh
-
-fresh=false
-unique=false
-
-for arg in "$@"; do
- [ "$unique" = true ] && unique="$arg"
- [ "$arg" = "--only" ] && unique=true
- [ "$arg" = "--fresh" ] && fresh=true
-done
-
-[ "$unique" = true ] && {
- echo "Expected argument after \`--only\`.";
- exit 1;
-}
-
-STAGIT=/var/www/git/stagit.out
-[ ! -f "$STAGIT" ] && STAGIT=stagit
-
-STAGIT_INDEX=/var/www/git/stagit-index.out
-[ ! -f "$STAGIT_INDEX" ] && STAGIT_INDEX=stagit-index
-
-build_html () {
- repo="$1"
- repo="$(basename "$repo" | sed 's/\.git$//g')"
-
- [ "$fresh" = true ] && {
- echo "Deleting HTML for $repo.";
- rm -fr "/var/www/git/$repo";
- }
- mkdir -p "/var/www/git/$repo"
- cd "/var/www/git/$repo" || { echo "Couldn't cd."; exit 1; }
-
- [ ! -f style.css ] && ln -s ../style.css ./
- [ ! -f favicon.png ] && ln -s ../favicon.png ./
- [ ! -f logo.png ] && ln -s ../logo.png ./
- [ ! -f highlight ] && ln -s ../highlight ./
- [ ! -f index.html ] && ln -s log.html index.html
-
- echo "git://git.knutsen.co/$repo" > "/srv/git/$repo.git/url"
-
- COMMAND="$STAGIT -c ./cachefile /srv/git/$repo.git"
- echo "Building web-page for $repo."
- echo "$COMMAND"
- $COMMAND
-}
-
-if [ "$unique" = false ]; then
- for repo in /srv/git/*.git; do
- build_html "$repo"
- done
-else
- build_html "$unique"
-fi
-
-echo "Generating index.html with \`$STAGIT_INDEX\`."
-"$STAGIT_INDEX" /srv/git/*.git > /var/www/git/index.html
-
-# Correct ownership of the web files.
-chown git:www-data -R /var/www/git -f
-chmod g+rw -R /var/www/git -f
--- /dev/null
+body {
+ font-size: 0.8rem;
+ font-family: monospace;
+ color: #ebdbb2;
+ background-color: #282828;
+ max-width: max-content;
+ min-width: min-content;
+}
+
+pre {
+ tab-size: 4;
+}
+
+h1, h2, h3, h4, h5, h6 {
+ margin: 0;
+}
+
+img, h1, h2 {
+ vertical-align: middle;
+}
+
+img {
+ border: 0;
+}
+
+a:target {
+ background-color: #eee;
+}
+
+a.d,
+a.h,
+a.i,
+a.line {
+ text-decoration: none;
+}
+
+#blob {
+ display: block;
+ max-width: 100%;
+}
+
+article.markup {
+ font-size: 15px;
+ border: 2px solid #eee;
+ border-radius: 10px;
+ font-family: sans-serif;
+ padding: 2.5em;
+ margin: 2em 0;
+}
+
+article.markup code {
+ font-size: 0.9em;
+ border: 1px solid #dbdbdb;
+ background-color: #f7f7f7;
+ padding: 0 0.3em;
+ border-radius: 0.3em
+}
+
+article.markup pre code {
+ border: none;
+ background: none;
+ padding: 0;
+ border-radius: 0;
+}
+
+article.markup pre {
+ background-color: #f7f7f7;
+ padding: 1em;
+ border: 1px solid #dbdbdb;
+ border-radius: 0.3em;
+}
+
+article.markup h1 {
+ font-size: 2.4em;
+ padding-bottom: 6px;
+ border-bottom: 5px solid #0000000a;
+}
+
+article.markup h2 {
+ font-size: 1.9em;
+ padding-bottom: 5px;
+ border-bottom: 2px solid #00000014;
+}
+
+article.markup h3 {
+ font-size: 1.5em;
+}
+
+article.markup h4 {
+ font-size: 1.3em;
+}
+
+article.markup h5 {
+ font-size: 1.1em;
+}
+
+article.markup h6 {
+ font-size: 1em;
+}
+
+article img {
+ max-width: 100%;
+}
+
+.linenos {
+ margin-right: 0;
+ border-right: 1px solid rgb(0 0 0 / 8%);
+ user-select: none;
+}
+
+.linenos a {
+ margin-right: 0.9em;
+ user-select: none;
+ text-decoration: none;
+}
+
+#blob a {
+ color: #777;
+}
+
+#blob a:hover {
+ color: #eee;
+ text-decoration: none;
+}
+
+a {
+ color: #83a598;
+}
+
+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: #333;
+}
+
+#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: #928374;
+}
+
+hr {
+ border: 0;
+ border-top: 1px solid #928374;
+ height: 1px;
+}
+
+pre {
+ font-family: monospace;
+}
+
+pre a.h {
+ color: #fe8019;
+}
+
+.A,
+span.i,
+pre a.i {
+ color: #b8bb26;
+}
+
+.D,
+span.d,
+pre a.d {
+ color: #fb4934;
+}
+
+pre a.h:hover,
+pre a.i:hover,
+pre a.d:hover {
+ text-decoration: none;
+}
+
+.url td:nth-child(2) {
+ padding-top: 0.2em;
+ padding-bottom: 0.9em;
+}
+
+.url td:nth-child(2) span {
+ padding: 1px 5px;
+ border: 1px solid #ebdbb2;
+ border-radius: 5px;
+}
+
+.url td:nth-child(2) span a {
+ color: #ebdbb2;
+}
--- /dev/null
+body{font-size:.8rem;font-family:monospace;color:#ebdbb2;background-color:#282828;max-width:max-content;min-width:min-content}pre{tab-size:4}h1,h2,h3,h4,h5,h6{margin:0}img,h1,h2{vertical-align:middle}img{border:0}a:target{background-color:#eee}a.d,a.h,a.i,a.line{text-decoration:none}#blob{display:block;max-width:100%}article.markup{font-size:15px;border:2px solid #eee;border-radius:10px;font-family:sans-serif;padding:2.5em;margin:2em 0}article.markup code{font-size:.9em;border:1px solid #dbdbdb;background-color:#f7f7f7;padding:0 .3em;border-radius:.3em}article.markup pre code{border:none;background:0 0;padding:0;border-radius:0}article.markup pre{background-color:#f7f7f7;padding:1em;border:1px solid #dbdbdb;border-radius:.3em}article.markup h1{font-size:2.4em;padding-bottom:6px;border-bottom:5px solid #0000000a}article.markup h2{font-size:1.9em;padding-bottom:5px;border-bottom:2px solid #00000014}article.markup h3{font-size:1.5em}article.markup h4{font-size:1.3em}article.markup h5{font-size:1.1em}article.markup h6{font-size:1em}article img{max-width:100%}.linenos{margin-right:0;border-right:1px solid rgb(0 0 0/8%);user-select:none}.linenos a{margin-right:.9em;user-select:none;text-decoration:none}#blob a{color:#777}#blob a:hover{color:#eee;text-decoration:none}a{color:#83a598}table thead td{font-weight:700}table td{padding: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:#333}#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:#928374}hr{border:0;border-top:1px solid #928374;height:1px}pre{font-family:monospace}pre a.h{color:#fe8019}.A,span.i,pre a.i{color:#b8bb26}.D,span.d,pre a.d{color:#fb4934}pre a.h:hover,pre a.i:hover,pre a.d:hover{text-decoration:none}.url td:nth-child(2){padding-top:.2em;padding-bottom:.9em}.url td:nth-child(2) span{padding:1px 5px;border:1px solid #ebdbb2;border-radius:5px}.url td:nth-child(2) span a{color:#ebdbb2}
\ No newline at end of file
--- /dev/null
+.highlight .hll{background-color:#ffc}.highlight{background:#282828;color:#ebdbb2;background-color:#282828}.highlight .c{color:#928374;font-style:italic;background-color:#282828}.highlight .err{color:#ebdbb2;background-color:#282828}.highlight .esc{color:#ebdbb2;background-color:#282828}.highlight .g{color:#ebdbb2;background-color:#282828}.highlight .k{color:#fe8019;background-color:#282828}.highlight .l{color:#ebdbb2;background-color:#282828}.highlight .n{color:#ebdbb2;background-color:#282828}.highlight .o{color:#fe8019;background-color:#282828}.highlight .x{color:#ebdbb2;background-color:#282828}.highlight .p{color:#ebdbb2;background-color:#282828}.highlight .ch{color:#928374;font-style:italic;background-color:#282828}.highlight .cm{color:#928374;font-style:italic;background-color:#282828}.highlight .cp{color:#8ec07c;background-color:#282828}.highlight .c1{color:#928374;font-style:italic;background-color:#282828}.highlight .cs{color:#928374;font-style:italic;background-color:#282828}.highlight .gd{color:#282828;background-color:#fb4934}.highlight .ge{color:#83a598;text-decoration:underline;background-color:#282828}.highlight .gr{color:#ebdbb2;font-weight:700;background-color:#fb4934}.highlight .gh{color:#b8bb26;font-weight:700;background-color:#282828}.highlight .gi{color:#282828;background-color:#b8bb26}.highlight .go{color:#504945;background-color:#282828}.highlight .gp{color:#ebdbb2;background-color:#282828}.highlight .gs{color:#ebdbb2;background-color:#282828}.highlight .gu{color:#b8bb26;font-weight:700;background-color:#282828}.highlight .gt{color:#ebdbb2;font-weight:700;background-color:#fb4934}.highlight .kc{color:#fe8019;background-color:#282828}.highlight .kd{color:#fe8019;background-color:#282828}.highlight .kn{color:#fe8019;background-color:#282828}.highlight .kp{color:#fe8019;background-color:#282828}.highlight .kr{color:#fe8019;background-color:#282828}.highlight .kt{color:#fabd2f;background-color:#282828}.highlight .ld{color:#ebdbb2;background-color:#282828}.highlight .m{color:#d3869b;background-color:#282828}.highlight .s{color:#b8bb26;background-color:#282828}.highlight .na{color:#b8bb26;font-weight:700;background-color:#282828}.highlight .nb{color:#fabd2f;background-color:#282828}.highlight .nc{color:#ebdbb2;background-color:#282828}.highlight .no{color:#d3869b;background-color:#282828}.highlight .nd{color:#ebdbb2;background-color:#282828}.highlight .ni{color:#fabd2f;background-color:#282828}.highlight .ne{color:#fb4934;background-color:#282828}.highlight .nf{color:#fabd2f;background-color:#282828}.highlight .nl{color:#fb4934;background-color:#282828}.highlight .nn{color:#ebdbb2;background-color:#282828}.highlight .nx{color:#ebdbb2;background-color:#282828}.highlight .py{color:#ebdbb2;background-color:#282828}.highlight .nt{color:#fb4934;background-color:#282828}.highlight .nv{color:#ebdbb2;background-color:#282828}.highlight .ow{color:#fe8019;background-color:#282828}.highlight .w{color:#ebdbb2;background-color:#282828}.highlight .mb{color:#d3869b;background-color:#282828}.highlight .mf{color:#d3869b;background-color:#282828}.highlight .mh{color:#d3869b;background-color:#282828}.highlight .mi{color:#d3869b;background-color:#282828}.highlight .mo{color:#d3869b;background-color:#282828}.highlight .sb{color:#b8bb26;background-color:#282828}.highlight .sc{color:#b8bb26;background-color:#282828}.highlight .sd{color:#b8bb26;background-color:#282828}.highlight .s2{color:#b8bb26;background-color:#282828}.highlight .se{color:#b8bb26;background-color:#282828}.highlight .sh{color:#b8bb26;background-color:#282828}.highlight .si{color:#b8bb26;background-color:#282828}.highlight .sx{color:#b8bb26;background-color:#282828}.highlight .sr{color:#b8bb26;background-color:#282828}.highlight .s1{color:#b8bb26;background-color:#282828}.highlight .ss{color:#83a598;background-color:#282828}.highlight .bp{color:#fabd2f;background-color:#282828}.highlight .vc{color:#ebdbb2;background-color:#282828}.highlight .vg{color:#ebdbb2;background-color:#282828}.highlight .vi{color:#ebdbb2;background-color:#282828}.highlight .il{color:#d3869b;background-color:#282828}
\ No newline at end of file
--- /dev/null
+#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);
--- /dev/null
+#!/usr/bin/env python3
+
+import pygments
+from pygments import highlight
+from pygments.formatters import HtmlFormatter
+import pygments.lexers
+from markdown import markdown
+
+from sys import stdin, stderr
+
+filename = stdin.readline().strip()
+contents = stdin.read()
+lexer = None
+
+try:
+ lexer = pygments.lexers.guess_lexer_for_filename(filename, contents)
+except pygments.util.ClassNotFound:
+ try:
+ lexer = pygments.lexers.guess_lexer(contents)
+ except pygments.util.ClassNotFound:
+ pass
+
+if lexer is None:
+ lexer = pygments.lexers.special.TextLexer
+
+rendered = (
+ markdown(
+ contents,
+ extensions=[
+ "codehilite",
+ "extra",
+ "sane_lists",
+ "smarty",
+ "pymdownx.tasklist",
+ ],
+ )
+ if lexer.__class__ is pygments.lexers.MarkdownLexer
+ else None
+)
+
+formatter = HtmlFormatter(
+ style="monokai",
+ cssclass="highlight",
+ linenos="table",
+ lineanchors="loc",
+ anchorlinenos=True,
+)
+
+outp = ""
+if rendered:
+ outp += '<article class="markup markdown">'
+ outp += rendered
+ outp += "</article>"
+outp += f'<div id="blob">{highlight(contents, lexer, formatter)}</div>"'
+outp += '<link rel="stylesheet" href="/syntax.css"'
+
+print(outp)
+
+print(f"Filename: {filename}; Lexer: {lexer}.", file=stderr)
+
+if rendered:
+ print("Markdown was rendered in addition.", file=stderr)
--- /dev/null
+/*
+ * 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);
+}
--- /dev/null
+#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];
+
+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);
+}
+
+/* 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("<", fp); break;
+ case '>': fputs(">", fp); break;
+ case '\'': fputs("'" , fp); break;
+ case '&': fputs("&", fp); break;
+ case '"': fputs(""", fp); break;
+ default: fputc(*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"
+ "<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);
+ git_revwalk_simplify_first_parent(w);
+
+ if (git_revwalk_next(&id, w) ||
+ git_commit_lookup(&commit, repo, &id)) {
+ ret = -1;
+ goto err;
+ }
+
+ author = git_commit_author(commit);
+
+ /* strip .git suffix */
+ if (!(stripped_name = strdup(name)))
+ err(1, "strdup");
+ if ((p = strrchr(stripped_name, '.')))
+ if (!strcmp(p, ".git"))
+ *p = '\0';
+
+ fputs("<tr><td><a href=\"", fp);
+ xmlencode(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, "%s [repodir...]\n", argv[0]);
+ return 1;
+ }
+
+ git_libgit2_init();
+
+#ifdef __OpenBSD__
+ for (i = 1; i < argc; i++)
+ if (unveil(argv[i], "r") == -1)
+ err(1, "unveil: %s", argv[i]);
+
+ 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';
+ 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';
+ owner[strcspn(owner, "\n")] = '\0';
+ fclose(fp);
+ }
+ writelog(stdout);
+ }
+ writefooter(stdout);
+
+ /* cleanup */
+ git_repository_free(repo);
+ git_libgit2_shutdown();
+
+ return ret;
+}
--- /dev/null
+#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"
+
+struct deltainfo {
+ git_patch *patch;
+
+ size_t addcount;
+ size_t delcount;
+};
+
+struct commitinfo {
+ const git_oid *id;
+
+ char oid[GIT_OID_HEXSZ + 1];
+ char parentoid[GIT_OID_HEXSZ + 1];
+
+ const git_signature *author;
+ const git_signature *committer;
+ const char *summary;
+ const char *msg;
+
+ git_diff *diff;
+ git_commit *commit;
+ git_commit *parent;
+ git_tree *commit_tree;
+ git_tree *parent_tree;
+
+ size_t addcount;
+ size_t delcount;
+ size_t filecount;
+
+ struct deltainfo **deltas;
+ size_t ndeltas;
+};
+
+static git_repository *repo;
+
+static const char *relpath = "";
+static const char *repodir;
+
+static char *name = "";
+static char *strippedname = "";
+static char description[255];
+static char cloneurl[1024];
+static 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; /* < 0 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;
+
+int cp(char fileSource[], char fileDestination[])
+{
+ int c;
+ FILE *stream_R, *stream_W;
+
+ stream_R = fopen(fileSource, "r");
+ if (stream_R == NULL)
+ return -1;
+ stream_W = fopen(fileDestination, "w"); //create and write to file
+ if (stream_W == NULL)
+ {
+ fclose(stream_R);
+ return -2;
+ }
+ while ((c = fgetc(stream_R)) != EOF)
+ fputc(c, stream_W);
+ fclose(stream_R);
+ fclose(stream_W);
+
+ return 0;
+}
+
+void
+joinpath(char *buf, size_t bufsiz, const char *path, const char *path2)
+{
+ int r;
+
+ r = snprintf(buf, bufsiz, "%s%s%s",
+ path, path[0] && path[strlen(path) - 1] != '/' ? "/" : "", path2);
+ if (r < 0 || (size_t)r >= bufsiz)
+ errx(1, "path truncated: '%s%s%s'",
+ path, path[0] && path[strlen(path) - 1] != '/' ? "/" : "", path2);
+}
+
+void
+deltainfo_free(struct deltainfo *di)
+{
+ if (!di)
+ return;
+ git_patch_free(di->patch);
+ memset(di, 0, sizeof(*di));
+ free(di);
+}
+
+int
+commitinfo_getstats(struct commitinfo *ci)
+{
+ struct deltainfo *di;
+ git_diff_options opts;
+ git_diff_find_options fopts;
+ const git_diff_delta *delta;
+ const git_diff_hunk *hunk;
+ const git_diff_line *line;
+ git_patch *patch = NULL;
+ size_t ndeltas, nhunks, nhunklines;
+ size_t i, j, k;
+
+ if (git_tree_lookup(&(ci->commit_tree), repo, git_commit_tree_id(ci->commit)))
+ goto err;
+ if (!git_commit_parent(&(ci->parent), ci->commit, 0)) {
+ if (git_tree_lookup(&(ci->parent_tree), repo, git_commit_tree_id(ci->parent))) {
+ ci->parent = NULL;
+ ci->parent_tree = NULL;
+ }
+ }
+
+ git_diff_init_options(&opts, GIT_DIFF_OPTIONS_VERSION);
+ opts.flags |= GIT_DIFF_DISABLE_PATHSPEC_MATCH |
+ GIT_DIFF_IGNORE_SUBMODULES |
+ GIT_DIFF_INCLUDE_TYPECHANGE;
+ if (git_diff_tree_to_tree(&(ci->diff), repo, ci->parent_tree, ci->commit_tree, &opts))
+ goto err;
+
+ if (git_diff_find_init_options(&fopts, GIT_DIFF_FIND_OPTIONS_VERSION))
+ goto err;
+ /* find renames and copies, exact matches (no heuristic) for renames. */
+ fopts.flags |= GIT_DIFF_FIND_RENAMES | GIT_DIFF_FIND_COPIES |
+ GIT_DIFF_FIND_EXACT_MATCH_ONLY;
+ if (git_diff_find_similar(ci->diff, &fopts))
+ goto err;
+
+ ndeltas = git_diff_num_deltas(ci->diff);
+ if (ndeltas && !(ci->deltas = calloc(ndeltas, sizeof(struct deltainfo *))))
+ err(1, "calloc");
+
+ for (i = 0; i < ndeltas; i++) {
+ if (git_patch_from_diff(&patch, ci->diff, i))
+ goto err;
+
+ if (!(di = calloc(1, sizeof(struct deltainfo))))
+ err(1, "calloc");
+ di->patch = patch;
+ ci->deltas[i] = di;
+
+ delta = git_patch_get_delta(patch);
+
+ /* skip stats for binary data */
+ if (delta->flags & GIT_DIFF_FLAG_BINARY)
+ continue;
+
+ nhunks = git_patch_num_hunks(patch);
+ for (j = 0; j < nhunks; j++) {
+ if (git_patch_get_hunk(&hunk, &nhunklines, patch, j))
+ break;
+ for (k = 0; ; k++) {
+ if (git_patch_get_line_in_hunk(&line, patch, j, k))
+ break;
+ if (line->old_lineno == -1) {
+ di->addcount++;
+ ci->addcount++;
+ } else if (line->new_lineno == -1) {
+ di->delcount++;
+ ci->delcount++;
+ }
+ }
+ }
+ }
+ ci->ndeltas = i;
+ ci->filecount = i;
+
+ return 0;
+
+err:
+ git_diff_free(ci->diff);
+ ci->diff = NULL;
+ git_tree_free(ci->commit_tree);
+ ci->commit_tree = NULL;
+ git_tree_free(ci->parent_tree);
+ ci->parent_tree = NULL;
+ git_commit_free(ci->parent);
+ ci->parent = NULL;
+
+ if (ci->deltas)
+ for (i = 0; i < ci->ndeltas; i++)
+ deltainfo_free(ci->deltas[i]);
+ free(ci->deltas);
+ ci->deltas = NULL;
+ ci->ndeltas = 0;
+ ci->addcount = 0;
+ ci->delcount = 0;
+ ci->filecount = 0;
+
+ return -1;
+}
+
+void
+commitinfo_free(struct commitinfo *ci)
+{
+ size_t i;
+
+ if (!ci)
+ return;
+ if (ci->deltas)
+ for (i = 0; i < ci->ndeltas; i++)
+ deltainfo_free(ci->deltas[i]);
+
+ free(ci->deltas);
+ git_diff_free(ci->diff);
+ git_tree_free(ci->commit_tree);
+ git_tree_free(ci->parent_tree);
+ git_commit_free(ci->commit);
+ git_commit_free(ci->parent);
+ memset(ci, 0, sizeof(*ci));
+ free(ci);
+}
+
+struct commitinfo *
+commitinfo_getbyoid(const git_oid *id)
+{
+ struct commitinfo *ci;
+
+ if (!(ci = calloc(1, sizeof(struct commitinfo))))
+ err(1, "calloc");
+
+ if (git_commit_lookup(&(ci->commit), repo, id))
+ goto err;
+ ci->id = id;
+
+ git_oid_tostr(ci->oid, sizeof(ci->oid), git_commit_id(ci->commit));
+ git_oid_tostr(ci->parentoid, sizeof(ci->parentoid), git_commit_parent_id(ci->commit, 0));
+
+ ci->author = git_commit_author(ci->commit);
+ ci->committer = git_commit_committer(ci->commit);
+ ci->summary = git_commit_summary(ci->commit);
+ ci->msg = git_commit_message(ci->commit);
+
+ return ci;
+
+err:
+ commitinfo_free(ci);
+
+ return NULL;
+}
+
+FILE *
+efopen(const char *name, const char *flags)
+{
+ FILE *fp;
+
+ if (!(fp = fopen(name, flags)))
+ err(1, "fopen: '%s'", name);
+
+ return fp;
+}
+
+/* Escape characters below as HTML 2.0 / XML 1.0. */
+void
+xmlencode(FILE *fp, const char *s, size_t len)
+{
+ size_t i;
+
+ for (i = 0; *s && i < len; s++, i++) {
+ switch(*s) {
+ case '<': fputs("<", fp); break;
+ case '>': fputs(">", fp); break;
+ case '\'': fputs("'", fp); break;
+ case '&': fputs("&", fp); break;
+ case '"': fputs(""", fp); break;
+ default: fputc(*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.0\" />\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);
+ fprintf(fp, "<link rel=\"alternate\" type=\"application/atom+xml\" title=\"%s Atom Feed\" href=\"%satom.xml\" />\n",
+ name, relpath);
+ fprintf(fp, "<link rel=\"stylesheet\" type=\"text/css\" href=\"%sstyle.min.css\" />\n", relpath);
+ fprintf(fp, "<link rel=\"stylesheet\" type=\"text/css\" href=\"%ssyntax.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><span class=\"clone\">git clone <a href=\"", fp);
+ xmlencode(fp, cloneurl, strlen(cloneurl));
+ fputs("\">", fp);
+ xmlencode(fp, cloneurl, strlen(cloneurl));
+ fputs("</a></span></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);
+ fputs("</td></tr></table>\n<hr/>\n<div id=\"content\">\n", fp);
+}
+
+void
+writefooter(FILE *fp)
+{
+ fputs("</div>\n</body>\n</html>\n", fp);
+}
+
+int
+call_py(const char *filename, FILE *fp, const char *s, size_t len)
+{
+ // Flush HTML-file
+ fflush(fp);
+ // Copy STDOUT
+ int stdout_copy = dup(1);
+ // Redirect STDOUT
+ dup2(fileno(fp), 1);
+
+ // Python Pygments script for syntax highlighting.
+ FILE *child = popen("/usr/local/share/stagit/highlight.py", "w");
+ if (child == NULL) {
+ printf("child is null: %s", strerror(errno));
+ exit(1);
+ }
+ // Give filename through STDIN:
+ fprintf(child, "%s\n", filename);
+ // Give code to highlight through STDIN:
+ int lc;
+ size_t i;
+ for (i = 0; *s && i < len; s++, i++) {
+ if (*s == '\n') lc++;
+ fprintf(child, "%c", *s);
+ }
+
+ pclose(child);
+ fflush(stdout);
+ // Give back STDOUT.
+ dup2(stdout_copy, 1);
+ return lc;
+}
+
+int
+writeblobhtml(const char *filename, FILE *fp, const git_blob *blob)
+{
+ int lc = 0;
+ const char *s = git_blob_rawcontent(blob);
+ git_off_t len = git_blob_rawsize(blob);
+
+ if (len > 0) {
+ lc = call_py(filename, fp, s, len);
+ }
+
+ return lc;
+}
+
+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(" <<a href=\"mailto:", fp);
+ xmlencode(fp, ci->author->email, strlen(ci->author->email));
+ fputs("\">", fp);
+ xmlencode(fp, ci->author->email, strlen(ci->author->email));
+ fputs("</a>>\n<b>Date:</b> ", fp);
+ printtime(fp, &(ci->author->when));
+ fputc('\n', fp);
+ }
+ if (ci->msg) {
+ fputc('\n', fp);
+ xmlencode(fp, ci->msg, strlen(ci->msg));
+ fputc('\n', fp);
+ }
+}
+
+void
+printshowfile(FILE *fp, struct commitinfo *ci)
+{
+ const git_diff_delta *delta;
+ const git_diff_hunk *hunk;
+ const git_diff_line *line;
+ git_patch *patch;
+ size_t nhunks, nhunklines, changed, add, del, total, i, j, k;
+ char linestr[80];
+ 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(" -> ", 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);
+ xmlencode(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);
+ xmlencode(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
+ fputc(' ', fp);
+ xmlencode(fp, line->content, line->content_len);
+ 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;
+ int r;
+
+ git_revwalk_new(&w, repo);
+ git_revwalk_push(w, oid);
+ git_revwalk_simplify_first_parent(w);
+
+ 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 && !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);
+ } else if (nlogcommits > 0) {
+ writelogline(fp, ci);
+ nlogcommits--;
+ if (!nlogcommits && ci->parentoid[0])
+ fputs("<tr><td></td><td colspan=\"5\">"
+ "More commits remaining [...]</td>"
+ "</tr>\n", fp);
+ }
+
+ 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);
+ fclose(fpfile);
+ }
+err:
+ commitinfo_free(ci);
+ }
+ git_revwalk_free(w);
+
+ relpath = "";
+
+ return 0;
+}
+
+void
+printcommitatom(FILE *fp, struct commitinfo *ci)
+{
+ fputs("<entry>\n", fp);
+
+ fprintf(fp, "<id>%s</id>\n", ci->oid);
+ if (ci->author) {
+ fputs("<published>", fp);
+ printtimez(fp, &(ci->author->when));
+ fputs("</published>\n", fp);
+ }
+ if (ci->committer) {
+ fputs("<updated>", fp);
+ printtimez(fp, &(ci->committer->when));
+ fputs("</updated>\n", fp);
+ }
+ if (ci->summary) {
+ fputs("<title type=\"text\">", fp);
+ xmlencode(fp, ci->summary, strlen(ci->summary));
+ fputs("</title>\n", fp);
+ }
+ fprintf(fp, "<link rel=\"alternate\" type=\"text/html\" href=\"commit/%s.html\" />\n",
+ 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(" <", fp);
+ xmlencode(fp, ci->author->email, strlen(ci->author->email));
+ fputs(">\nDate: ", fp);
+ printtime(fp, &(ci->author->when));
+ fputc('\n', fp);
+ }
+ if (ci->msg) {
+ fputc('\n', fp);
+ xmlencode(fp, ci->msg, strlen(ci->msg));
+ }
+ fputs("\n</content>\n</entry>\n", fp);
+}
+
+int
+writeatom(FILE *fp)
+{
+ struct commitinfo *ci;
+ git_revwalk *w = NULL;
+ git_oid id;
+ size_t i, m = 100; /* last 'm' commits */
+
+ fputs("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+ "<feed xmlns=\"http://www.w3.org/2005/Atom\">\n<title>", fp);
+ xmlencode(fp, strippedname, strlen(strippedname));
+ fputs(", branch HEAD</title>\n<subtitle>", fp);
+ xmlencode(fp, description, strlen(description));
+ fputs("</subtitle>\n", fp);
+
+ git_revwalk_new(&w, repo);
+ git_revwalk_push_head(w);
+ git_revwalk_simplify_first_parent(w);
+
+ for (i = 0; i < m && !git_revwalk_next(&id, w); i++) {
+ if (!(ci = commitinfo_getbyoid(&id)))
+ break;
+ printcommitatom(fp, ci);
+ commitinfo_free(ci);
+ }
+ git_revwalk_free(w);
+
+ fputs("</feed>\n", fp);
+
+ return 0;
+}
+
+int
+writeblob(git_object *obj, const char *fpath, const char *filename, git_off_t filesize)
+{
+ char tmp[PATH_MAX] = "", *d;
+ const char *p;
+ int lc = 0;
+ FILE *fp;
+
+ if (strlcpy(tmp, fpath, sizeof(tmp)) >= sizeof(tmp))
+ errx(1, "path truncated: '%s'", fpath);
+ if (!(d = dirname(tmp)))
+ err(1, "dirname");
+ if (mkdirp(d))
+ return -1;
+
+ 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, " (%juB)", (uintmax_t)filesize);
+ fputs("</p><hr/>", fp);
+
+ if (git_blob_is_binary((git_blob *)obj)) {
+ fputs("<p>Binary file.</p>\n", fp);
+ } else {
+ lc = writeblobhtml(filename, fp, (git_blob *)obj);
+ if (ferror(fp))
+ err(1, "fwrite");
+ }
+ writefooter(fp);
+ 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
+writefilestree(FILE *fp, git_tree *tree, const char *path)
+{
+ const git_tree_entry *entry = NULL;
+ git_submodule *module = NULL;
+ git_object *obj = NULL;
+ git_off_t filesize;
+ const char *entryname;
+ char filepath[PATH_MAX], entrypath[PATH_MAX];
+ size_t count, i;
+ int lc, r, ret;
+
+ count = git_tree_entrycount(tree);
+ for (i = 0; i < count; i++) {
+ if (!(entry = git_tree_entry_byindex(tree, i)) ||
+ !(entryname = git_tree_entry_name(entry)))
+ return -1;
+ joinpath(entrypath, sizeof(entrypath), path, entryname);
+
+ r = snprintf(filepath, sizeof(filepath), "file/%s.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);
+ xmlencode(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, "%dL", lc);
+ else
+ fprintf(fp, "%juB", (uintmax_t)filesize);
+ fputs("</td></tr>\n", fp);
+ git_object_free(obj);
+ } else if (!git_submodule_lookup(&module, repo, entryname)) {
+ fprintf(fp, "<tr><td>m---------</td><td><a href=\"%sfile/.gitmodules.html\">",
+ relpath);
+ xmlencode(fp, entrypath, strlen(entrypath));
+ git_submodule_free(module);
+ fputs("</a></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
+refs_cmp(const void *v1, const void *v2)
+{
+ git_reference *r1 = (*(git_reference **)v1);
+ git_reference *r2 = (*(git_reference **)v2);
+ int r;
+
+ if ((r = git_reference_is_branch(r1) - git_reference_is_branch(r2)))
+ return r;
+
+ return strcmp(git_reference_shorthand(r1),
+ git_reference_shorthand(r2));
+}
+
+int
+writerefs(FILE *fp)
+{
+ struct commitinfo *ci;
+ const git_oid *id = NULL;
+ git_object *obj = NULL;
+ git_reference *dref = NULL, *r, *ref = NULL;
+ git_reference_iterator *it = NULL;
+ git_reference **refs = NULL;
+ size_t count, i, j, refcount;
+ const char *titles[] = { "Branches", "Tags" };
+ const char *ids[] = { "branches", "tags" };
+ const char *name;
+
+ if (git_reference_iterator_new(&it, repo))
+ return -1;
+
+ for (refcount = 0; !git_reference_next(&ref, it); refcount++) {
+ if (!(refs = reallocarray(refs, refcount + 1, sizeof(git_reference *))))
+ err(1, "realloc");
+ refs[refcount] = ref;
+ }
+ git_reference_iterator_free(it);
+
+ /* sort by type then shorthand name */
+ qsort(refs, refcount, sizeof(git_reference *), refs_cmp);
+
+ for (j = 0; j < 2; j++) {
+ for (i = 0, count = 0; i < refcount; i++) {
+ if (!(git_reference_is_branch(refs[i]) && j == 0) &&
+ !(git_reference_is_tag(refs[i]) && j == 1))
+ continue;
+
+ switch (git_reference_type(refs[i])) {
+ case GIT_REF_SYMBOLIC:
+ if (git_reference_resolve(&dref, refs[i]))
+ goto err;
+ r = dref;
+ break;
+ case GIT_REF_OID:
+ r = refs[i];
+ break;
+ default:
+ continue;
+ }
+ if (!git_reference_target(r) ||
+ git_reference_peel(&obj, r, GIT_OBJ_ANY))
+ goto err;
+ if (!(id = git_object_id(obj)))
+ goto err;
+ if (!(ci = commitinfo_getbyoid(id)))
+ break;
+
+ /* print header if it has an entry (first). */
+ if (++count == 1) {
+ fprintf(fp, "<h2>%s</h2><table id=\"%s\">"
+ "<thead>\n<tr><td><b>Name</b></td>"
+ "<td><b>Last commit date</b></td>"
+ "<td><b>Author</b></td>\n</tr>\n"
+ "</thead><tbody>\n",
+ titles[j], ids[j]);
+ }
+
+ relpath = "";
+ name = git_reference_shorthand(r);
+
+ fputs("<tr><td>", fp);
+ xmlencode(fp, name, strlen(name));
+ 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);
+
+ relpath = "../";
+
+ commitinfo_free(ci);
+ git_object_free(obj);
+ obj = NULL;
+ git_reference_free(dref);
+ dref = NULL;
+ }
+ /* table footer */
+ if (count)
+ fputs("</tbody></table><br/>", fp);
+ }
+
+err:
+ git_object_free(obj);
+ git_reference_free(dref);
+
+ for (i = 0; i < refcount; i++)
+ git_reference_free(refs[i]);
+ free(refs);
+
+ return 0;
+}
+
+void
+usage(char *argv0)
+{
+ fprintf(stderr, "%s [-c cachefile | -l commits] 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]);
+ }
+ }
+ if (!repodir)
+ usage(argv[0]);
+
+ if (!realpath(repodir, repodirabs))
+ err(1, "realpath");
+
+ git_libgit2_init();
+
+#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 = "";
+
+ /* copy css */
+ char cwd[PATH_MAX];
+ strcpy(cwd, getcwd(cwd, sizeof(cwd)));
+ cp("/usr/local/share/stagit/syntax.css", strcat(cwd, "/syntax.css"));
+ strcpy(cwd, getcwd(cwd, sizeof(cwd)));
+ cp("/usr/local/share/stagit/style.min.css", strcat(cwd, "/style.min.css"));
+
+ /* strip .git suffix */
+ if (!(strippedname = strdup(name)))
+ err(1, "strdup");
+ if ((p = strrchr(strippedname, '.')))
+ if (!strcmp(p, ".git"))
+ *p = '\0';
+
+ /* read description or .git/description */
+ joinpath(path, sizeof(path), repodir, "description");
+ if (!(fpread = fopen(path, "r"))) {
+ joinpath(path, sizeof(path), repodir, ".git/description");
+ fpread = fopen(path, "r");
+ }
+ if (fpread) {
+ if (!fgets(description, sizeof(description), fpread))
+ description[0] = '\0';
+ fclose(fpread);
+ }
+
+ /* read url or .git/url */
+ joinpath(path, sizeof(path), repodir, "url");
+ if (!(fpread = fopen(path, "r"))) {
+ joinpath(path, sizeof(path), repodir, ".git/url");
+ fpread = fopen(path, "r");
+ }
+ if (fpread) {
+ if (!fgets(cloneurl, sizeof(cloneurl), fpread))
+ cloneurl[0] = '\0';
+ cloneurl[strcspn(cloneurl, "\n")] = '\0';
+ fclose(fpread);
+ }
+
+ /* check LICENSE */
+ for (i = 0; i < sizeof(licensefiles) / sizeof(*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 < sizeof(readmefiles) / sizeof(*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</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))
+ err(1, "fread");
+ if (fwrite(buf, 1, n, fp) != n ||
+ fwrite(buf, 1, n, wcachefp) != n)
+ err(1, "fwrite");
+ }
+ fclose(rcachefp);
+ }
+ fclose(wcachefp);
+ } else {
+ if (head)
+ writelog(fp, head);
+ }
+
+ fputs("</tbody></table>", fp);
+ writefooter(fp);
+ fclose(fp);
+
+ /* files for HEAD */
+ fp = efopen("files.html", "w");
+ writeheader(fp, "Files");
+ if (head)
+ writefiles(fp, head);
+ writefooter(fp);
+ fclose(fp);
+
+ /* summary page with branches and tags */
+ fp = efopen("refs.html", "w");
+ writeheader(fp, "Refs");
+ writerefs(fp);
+ writefooter(fp);
+ fclose(fp);
+
+ /* Atom feed */
+ fp = efopen("atom.xml", "w");
+ writeatom(fp);
+ fclose(fp);
+
+ /* rename new cache file on success */
+ if (cachefile && 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;
+}
--- /dev/null
+/* $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 */
+}
--- /dev/null
+/* $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 */
+}
+++ /dev/null
-.Dd March 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 SEE ALSO
-.Xr stagit 1
-.Sh AUTHORS
-.An Hiltjo Posthuma Aq Mt hiltjo@codemadness.org
-.An Armaan Bhojwani Aq Mt me@armaanb.net
+++ /dev/null
-#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];
-
-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);
-}
-
-/* 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("<", fp); break;
- case '>': fputs(">", fp); break;
- case '\'': fputs("'" , fp); break;
- case '&': fputs("&", fp); break;
- case '"': fputs(""", fp); break;
- default: fputc(*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"
- "<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);
- git_revwalk_simplify_first_parent(w);
-
- if (git_revwalk_next(&id, w) ||
- git_commit_lookup(&commit, repo, &id)) {
- ret = -1;
- goto err;
- }
-
- author = git_commit_author(commit);
-
- /* strip .git suffix */
- if (!(stripped_name = strdup(name)))
- err(1, "strdup");
- if ((p = strrchr(stripped_name, '.')))
- if (!strcmp(p, ".git"))
- *p = '\0';
-
- fputs("<tr><td><a href=\"", fp);
- xmlencode(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, "%s [repodir...]\n", argv[0]);
- return 1;
- }
-
- git_libgit2_init();
-
-#ifdef __OpenBSD__
- for (i = 1; i < argc; i++)
- if (unveil(argv[i], "r") == -1)
- err(1, "unveil: %s", argv[i]);
-
- 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';
- 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';
- owner[strcspn(owner, "\n")] = '\0';
- fclose(fp);
- }
- writelog(stdout);
- }
- writefooter(stdout);
-
- /* cleanup */
- git_repository_free(repo);
- git_libgit2_shutdown();
-
- return ret;
-}
+++ /dev/null
-.Dd March 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
-.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.
-.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
-.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.2f30.org/stagit
-.El
-.Pp
-When a README or LICENSE file exists in HEAD or a .gitmodules submodules file
-exists in HEAD a direct link in the menu is made.
-.Pp
-For changing the style of the page you can use the following files:
-.Bl -tag -width Ds
-.It favicon.png
-favicon image.
-.It logo.png
-32x32 logo.
-.It style.css
-CSS stylesheet.
-.El
-.Sh SEE ALSO
-.Xr stagit-index 1
-.Sh AUTHORS
-.An Hiltjo Posthuma Aq Mt hiltjo@codemadness.org
-.An Armaan Bhojwani Aq Mt me@armaanb.net
+++ /dev/null
-#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"
-
-struct deltainfo {
- git_patch *patch;
-
- size_t addcount;
- size_t delcount;
-};
-
-struct commitinfo {
- const git_oid *id;
-
- char oid[GIT_OID_HEXSZ + 1];
- char parentoid[GIT_OID_HEXSZ + 1];
-
- const git_signature *author;
- const git_signature *committer;
- const char *summary;
- const char *msg;
-
- git_diff *diff;
- git_commit *commit;
- git_commit *parent;
- git_tree *commit_tree;
- git_tree *parent_tree;
-
- size_t addcount;
- size_t delcount;
- size_t filecount;
-
- struct deltainfo **deltas;
- size_t ndeltas;
-};
-
-static git_repository *repo;
-
-static const char *relpath = "";
-static const char *repodir;
-
-static char *name = "";
-static char *strippedname = "";
-static char description[255];
-static char cloneurl[1024];
-static 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; /* < 0 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;
-
-int cp(char fileSource[], char fileDestination[])
-{
- int c;
- FILE *stream_R, *stream_W;
-
- stream_R = fopen(fileSource, "r");
- if (stream_R == NULL)
- return -1;
- stream_W = fopen(fileDestination, "w"); //create and write to file
- if (stream_W == NULL)
- {
- fclose(stream_R);
- return -2;
- }
- while ((c = fgetc(stream_R)) != EOF)
- fputc(c, stream_W);
- fclose(stream_R);
- fclose(stream_W);
-
- return 0;
-}
-
-void
-joinpath(char *buf, size_t bufsiz, const char *path, const char *path2)
-{
- int r;
-
- r = snprintf(buf, bufsiz, "%s%s%s",
- path, path[0] && path[strlen(path) - 1] != '/' ? "/" : "", path2);
- if (r < 0 || (size_t)r >= bufsiz)
- errx(1, "path truncated: '%s%s%s'",
- path, path[0] && path[strlen(path) - 1] != '/' ? "/" : "", path2);
-}
-
-void
-deltainfo_free(struct deltainfo *di)
-{
- if (!di)
- return;
- git_patch_free(di->patch);
- memset(di, 0, sizeof(*di));
- free(di);
-}
-
-int
-commitinfo_getstats(struct commitinfo *ci)
-{
- struct deltainfo *di;
- git_diff_options opts;
- git_diff_find_options fopts;
- const git_diff_delta *delta;
- const git_diff_hunk *hunk;
- const git_diff_line *line;
- git_patch *patch = NULL;
- size_t ndeltas, nhunks, nhunklines;
- size_t i, j, k;
-
- if (git_tree_lookup(&(ci->commit_tree), repo, git_commit_tree_id(ci->commit)))
- goto err;
- if (!git_commit_parent(&(ci->parent), ci->commit, 0)) {
- if (git_tree_lookup(&(ci->parent_tree), repo, git_commit_tree_id(ci->parent))) {
- ci->parent = NULL;
- ci->parent_tree = NULL;
- }
- }
-
- git_diff_init_options(&opts, GIT_DIFF_OPTIONS_VERSION);
- opts.flags |= GIT_DIFF_DISABLE_PATHSPEC_MATCH |
- GIT_DIFF_IGNORE_SUBMODULES |
- GIT_DIFF_INCLUDE_TYPECHANGE;
- if (git_diff_tree_to_tree(&(ci->diff), repo, ci->parent_tree, ci->commit_tree, &opts))
- goto err;
-
- if (git_diff_find_init_options(&fopts, GIT_DIFF_FIND_OPTIONS_VERSION))
- goto err;
- /* find renames and copies, exact matches (no heuristic) for renames. */
- fopts.flags |= GIT_DIFF_FIND_RENAMES | GIT_DIFF_FIND_COPIES |
- GIT_DIFF_FIND_EXACT_MATCH_ONLY;
- if (git_diff_find_similar(ci->diff, &fopts))
- goto err;
-
- ndeltas = git_diff_num_deltas(ci->diff);
- if (ndeltas && !(ci->deltas = calloc(ndeltas, sizeof(struct deltainfo *))))
- err(1, "calloc");
-
- for (i = 0; i < ndeltas; i++) {
- if (git_patch_from_diff(&patch, ci->diff, i))
- goto err;
-
- if (!(di = calloc(1, sizeof(struct deltainfo))))
- err(1, "calloc");
- di->patch = patch;
- ci->deltas[i] = di;
-
- delta = git_patch_get_delta(patch);
-
- /* skip stats for binary data */
- if (delta->flags & GIT_DIFF_FLAG_BINARY)
- continue;
-
- nhunks = git_patch_num_hunks(patch);
- for (j = 0; j < nhunks; j++) {
- if (git_patch_get_hunk(&hunk, &nhunklines, patch, j))
- break;
- for (k = 0; ; k++) {
- if (git_patch_get_line_in_hunk(&line, patch, j, k))
- break;
- if (line->old_lineno == -1) {
- di->addcount++;
- ci->addcount++;
- } else if (line->new_lineno == -1) {
- di->delcount++;
- ci->delcount++;
- }
- }
- }
- }
- ci->ndeltas = i;
- ci->filecount = i;
-
- return 0;
-
-err:
- git_diff_free(ci->diff);
- ci->diff = NULL;
- git_tree_free(ci->commit_tree);
- ci->commit_tree = NULL;
- git_tree_free(ci->parent_tree);
- ci->parent_tree = NULL;
- git_commit_free(ci->parent);
- ci->parent = NULL;
-
- if (ci->deltas)
- for (i = 0; i < ci->ndeltas; i++)
- deltainfo_free(ci->deltas[i]);
- free(ci->deltas);
- ci->deltas = NULL;
- ci->ndeltas = 0;
- ci->addcount = 0;
- ci->delcount = 0;
- ci->filecount = 0;
-
- return -1;
-}
-
-void
-commitinfo_free(struct commitinfo *ci)
-{
- size_t i;
-
- if (!ci)
- return;
- if (ci->deltas)
- for (i = 0; i < ci->ndeltas; i++)
- deltainfo_free(ci->deltas[i]);
-
- free(ci->deltas);
- git_diff_free(ci->diff);
- git_tree_free(ci->commit_tree);
- git_tree_free(ci->parent_tree);
- git_commit_free(ci->commit);
- git_commit_free(ci->parent);
- memset(ci, 0, sizeof(*ci));
- free(ci);
-}
-
-struct commitinfo *
-commitinfo_getbyoid(const git_oid *id)
-{
- struct commitinfo *ci;
-
- if (!(ci = calloc(1, sizeof(struct commitinfo))))
- err(1, "calloc");
-
- if (git_commit_lookup(&(ci->commit), repo, id))
- goto err;
- ci->id = id;
-
- git_oid_tostr(ci->oid, sizeof(ci->oid), git_commit_id(ci->commit));
- git_oid_tostr(ci->parentoid, sizeof(ci->parentoid), git_commit_parent_id(ci->commit, 0));
-
- ci->author = git_commit_author(ci->commit);
- ci->committer = git_commit_committer(ci->commit);
- ci->summary = git_commit_summary(ci->commit);
- ci->msg = git_commit_message(ci->commit);
-
- return ci;
-
-err:
- commitinfo_free(ci);
-
- return NULL;
-}
-
-FILE *
-efopen(const char *name, const char *flags)
-{
- FILE *fp;
-
- if (!(fp = fopen(name, flags)))
- err(1, "fopen: '%s'", name);
-
- return fp;
-}
-
-/* Escape characters below as HTML 2.0 / XML 1.0. */
-void
-xmlencode(FILE *fp, const char *s, size_t len)
-{
- size_t i;
-
- for (i = 0; *s && i < len; s++, i++) {
- switch(*s) {
- case '<': fputs("<", fp); break;
- case '>': fputs(">", fp); break;
- case '\'': fputs("'", fp); break;
- case '&': fputs("&", fp); break;
- case '"': fputs(""", fp); break;
- default: fputc(*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.0\" />\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);
- fprintf(fp, "<link rel=\"alternate\" type=\"application/atom+xml\" title=\"%s Atom Feed\" href=\"%satom.xml\" />\n",
- name, relpath);
- fprintf(fp, "<link rel=\"stylesheet\" type=\"text/css\" href=\"%sstyle.min.css\" />\n", relpath);
- fprintf(fp, "<link rel=\"stylesheet\" type=\"text/css\" href=\"%ssyntax.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><span class=\"clone\">git clone <a href=\"", fp);
- xmlencode(fp, cloneurl, strlen(cloneurl));
- fputs("\">", fp);
- xmlencode(fp, cloneurl, strlen(cloneurl));
- fputs("</a></span></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);
- fputs("</td></tr></table>\n<hr/>\n<div id=\"content\">\n", fp);
-}
-
-void
-writefooter(FILE *fp)
-{
- fputs("</div>\n</body>\n</html>\n", fp);
-}
-
-int
-call_py(const char *filename, FILE *fp, const char *s, size_t len)
-{
- // Flush HTML-file
- fflush(fp);
- // Copy STDOUT
- int stdout_copy = dup(1);
- // Redirect STDOUT
- dup2(fileno(fp), 1);
-
- // Python Pygments script for syntax highlighting.
- FILE *child = popen("/usr/local/share/stagit/highlight.py", "w");
- if (child == NULL) {
- printf("child is null: %s", strerror(errno));
- exit(1);
- }
- // Give filename through STDIN:
- fprintf(child, "%s\n", filename);
- // Give code to highlight through STDIN:
- int lc;
- size_t i;
- for (i = 0; *s && i < len; s++, i++) {
- if (*s == '\n') lc++;
- fprintf(child, "%c", *s);
- }
-
- pclose(child);
- fflush(stdout);
- // Give back STDOUT.
- dup2(stdout_copy, 1);
- return lc;
-}
-
-int
-writeblobhtml(const char *filename, FILE *fp, const git_blob *blob)
-{
- int lc = 0;
- const char *s = git_blob_rawcontent(blob);
- git_off_t len = git_blob_rawsize(blob);
-
- if (len > 0) {
- lc = call_py(filename, fp, s, len);
- }
-
- return lc;
-}
-
-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(" <<a href=\"mailto:", fp);
- xmlencode(fp, ci->author->email, strlen(ci->author->email));
- fputs("\">", fp);
- xmlencode(fp, ci->author->email, strlen(ci->author->email));
- fputs("</a>>\n<b>Date:</b> ", fp);
- printtime(fp, &(ci->author->when));
- fputc('\n', fp);
- }
- if (ci->msg) {
- fputc('\n', fp);
- xmlencode(fp, ci->msg, strlen(ci->msg));
- fputc('\n', fp);
- }
-}
-
-void
-printshowfile(FILE *fp, struct commitinfo *ci)
-{
- const git_diff_delta *delta;
- const git_diff_hunk *hunk;
- const git_diff_line *line;
- git_patch *patch;
- size_t nhunks, nhunklines, changed, add, del, total, i, j, k;
- char linestr[80];
- 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(" -> ", 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);
- xmlencode(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);
- xmlencode(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
- fputc(' ', fp);
- xmlencode(fp, line->content, line->content_len);
- 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;
- int r;
-
- git_revwalk_new(&w, repo);
- git_revwalk_push(w, oid);
- git_revwalk_simplify_first_parent(w);
-
- 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 && !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);
- } else if (nlogcommits > 0) {
- writelogline(fp, ci);
- nlogcommits--;
- if (!nlogcommits && ci->parentoid[0])
- fputs("<tr><td></td><td colspan=\"5\">"
- "More commits remaining [...]</td>"
- "</tr>\n", fp);
- }
-
- 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);
- fclose(fpfile);
- }
-err:
- commitinfo_free(ci);
- }
- git_revwalk_free(w);
-
- relpath = "";
-
- return 0;
-}
-
-void
-printcommitatom(FILE *fp, struct commitinfo *ci)
-{
- fputs("<entry>\n", fp);
-
- fprintf(fp, "<id>%s</id>\n", ci->oid);
- if (ci->author) {
- fputs("<published>", fp);
- printtimez(fp, &(ci->author->when));
- fputs("</published>\n", fp);
- }
- if (ci->committer) {
- fputs("<updated>", fp);
- printtimez(fp, &(ci->committer->when));
- fputs("</updated>\n", fp);
- }
- if (ci->summary) {
- fputs("<title type=\"text\">", fp);
- xmlencode(fp, ci->summary, strlen(ci->summary));
- fputs("</title>\n", fp);
- }
- fprintf(fp, "<link rel=\"alternate\" type=\"text/html\" href=\"commit/%s.html\" />\n",
- 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(" <", fp);
- xmlencode(fp, ci->author->email, strlen(ci->author->email));
- fputs(">\nDate: ", fp);
- printtime(fp, &(ci->author->when));
- fputc('\n', fp);
- }
- if (ci->msg) {
- fputc('\n', fp);
- xmlencode(fp, ci->msg, strlen(ci->msg));
- }
- fputs("\n</content>\n</entry>\n", fp);
-}
-
-int
-writeatom(FILE *fp)
-{
- struct commitinfo *ci;
- git_revwalk *w = NULL;
- git_oid id;
- size_t i, m = 100; /* last 'm' commits */
-
- fputs("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
- "<feed xmlns=\"http://www.w3.org/2005/Atom\">\n<title>", fp);
- xmlencode(fp, strippedname, strlen(strippedname));
- fputs(", branch HEAD</title>\n<subtitle>", fp);
- xmlencode(fp, description, strlen(description));
- fputs("</subtitle>\n", fp);
-
- git_revwalk_new(&w, repo);
- git_revwalk_push_head(w);
- git_revwalk_simplify_first_parent(w);
-
- for (i = 0; i < m && !git_revwalk_next(&id, w); i++) {
- if (!(ci = commitinfo_getbyoid(&id)))
- break;
- printcommitatom(fp, ci);
- commitinfo_free(ci);
- }
- git_revwalk_free(w);
-
- fputs("</feed>\n", fp);
-
- return 0;
-}
-
-int
-writeblob(git_object *obj, const char *fpath, const char *filename, git_off_t filesize)
-{
- char tmp[PATH_MAX] = "", *d;
- const char *p;
- int lc = 0;
- FILE *fp;
-
- if (strlcpy(tmp, fpath, sizeof(tmp)) >= sizeof(tmp))
- errx(1, "path truncated: '%s'", fpath);
- if (!(d = dirname(tmp)))
- err(1, "dirname");
- if (mkdirp(d))
- return -1;
-
- 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, " (%juB)", (uintmax_t)filesize);
- fputs("</p><hr/>", fp);
-
- if (git_blob_is_binary((git_blob *)obj)) {
- fputs("<p>Binary file.</p>\n", fp);
- } else {
- lc = writeblobhtml(filename, fp, (git_blob *)obj);
- if (ferror(fp))
- err(1, "fwrite");
- }
- writefooter(fp);
- 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
-writefilestree(FILE *fp, git_tree *tree, const char *path)
-{
- const git_tree_entry *entry = NULL;
- git_submodule *module = NULL;
- git_object *obj = NULL;
- git_off_t filesize;
- const char *entryname;
- char filepath[PATH_MAX], entrypath[PATH_MAX];
- size_t count, i;
- int lc, r, ret;
-
- count = git_tree_entrycount(tree);
- for (i = 0; i < count; i++) {
- if (!(entry = git_tree_entry_byindex(tree, i)) ||
- !(entryname = git_tree_entry_name(entry)))
- return -1;
- joinpath(entrypath, sizeof(entrypath), path, entryname);
-
- r = snprintf(filepath, sizeof(filepath), "file/%s.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);
- xmlencode(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, "%dL", lc);
- else
- fprintf(fp, "%juB", (uintmax_t)filesize);
- fputs("</td></tr>\n", fp);
- git_object_free(obj);
- } else if (!git_submodule_lookup(&module, repo, entryname)) {
- fprintf(fp, "<tr><td>m---------</td><td><a href=\"%sfile/.gitmodules.html\">",
- relpath);
- xmlencode(fp, entrypath, strlen(entrypath));
- git_submodule_free(module);
- fputs("</a></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
-refs_cmp(const void *v1, const void *v2)
-{
- git_reference *r1 = (*(git_reference **)v1);
- git_reference *r2 = (*(git_reference **)v2);
- int r;
-
- if ((r = git_reference_is_branch(r1) - git_reference_is_branch(r2)))
- return r;
-
- return strcmp(git_reference_shorthand(r1),
- git_reference_shorthand(r2));
-}
-
-int
-writerefs(FILE *fp)
-{
- struct commitinfo *ci;
- const git_oid *id = NULL;
- git_object *obj = NULL;
- git_reference *dref = NULL, *r, *ref = NULL;
- git_reference_iterator *it = NULL;
- git_reference **refs = NULL;
- size_t count, i, j, refcount;
- const char *titles[] = { "Branches", "Tags" };
- const char *ids[] = { "branches", "tags" };
- const char *name;
-
- if (git_reference_iterator_new(&it, repo))
- return -1;
-
- for (refcount = 0; !git_reference_next(&ref, it); refcount++) {
- if (!(refs = reallocarray(refs, refcount + 1, sizeof(git_reference *))))
- err(1, "realloc");
- refs[refcount] = ref;
- }
- git_reference_iterator_free(it);
-
- /* sort by type then shorthand name */
- qsort(refs, refcount, sizeof(git_reference *), refs_cmp);
-
- for (j = 0; j < 2; j++) {
- for (i = 0, count = 0; i < refcount; i++) {
- if (!(git_reference_is_branch(refs[i]) && j == 0) &&
- !(git_reference_is_tag(refs[i]) && j == 1))
- continue;
-
- switch (git_reference_type(refs[i])) {
- case GIT_REF_SYMBOLIC:
- if (git_reference_resolve(&dref, refs[i]))
- goto err;
- r = dref;
- break;
- case GIT_REF_OID:
- r = refs[i];
- break;
- default:
- continue;
- }
- if (!git_reference_target(r) ||
- git_reference_peel(&obj, r, GIT_OBJ_ANY))
- goto err;
- if (!(id = git_object_id(obj)))
- goto err;
- if (!(ci = commitinfo_getbyoid(id)))
- break;
-
- /* print header if it has an entry (first). */
- if (++count == 1) {
- fprintf(fp, "<h2>%s</h2><table id=\"%s\">"
- "<thead>\n<tr><td><b>Name</b></td>"
- "<td><b>Last commit date</b></td>"
- "<td><b>Author</b></td>\n</tr>\n"
- "</thead><tbody>\n",
- titles[j], ids[j]);
- }
-
- relpath = "";
- name = git_reference_shorthand(r);
-
- fputs("<tr><td>", fp);
- xmlencode(fp, name, strlen(name));
- 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);
-
- relpath = "../";
-
- commitinfo_free(ci);
- git_object_free(obj);
- obj = NULL;
- git_reference_free(dref);
- dref = NULL;
- }
- /* table footer */
- if (count)
- fputs("</tbody></table><br/>", fp);
- }
-
-err:
- git_object_free(obj);
- git_reference_free(dref);
-
- for (i = 0; i < refcount; i++)
- git_reference_free(refs[i]);
- free(refs);
-
- return 0;
-}
-
-void
-usage(char *argv0)
-{
- fprintf(stderr, "%s [-c cachefile | -l commits] 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]);
- }
- }
- if (!repodir)
- usage(argv[0]);
-
- if (!realpath(repodir, repodirabs))
- err(1, "realpath");
-
- git_libgit2_init();
-
-#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 = "";
-
- /* copy css */
- char cwd[PATH_MAX];
- strcpy(cwd, getcwd(cwd, sizeof(cwd)));
- cp("/usr/local/share/stagit/syntax.css", strcat(cwd, "/syntax.css"));
- strcpy(cwd, getcwd(cwd, sizeof(cwd)));
- cp("/usr/local/share/stagit/style.min.css", strcat(cwd, "/style.min.css"));
-
- /* strip .git suffix */
- if (!(strippedname = strdup(name)))
- err(1, "strdup");
- if ((p = strrchr(strippedname, '.')))
- if (!strcmp(p, ".git"))
- *p = '\0';
-
- /* read description or .git/description */
- joinpath(path, sizeof(path), repodir, "description");
- if (!(fpread = fopen(path, "r"))) {
- joinpath(path, sizeof(path), repodir, ".git/description");
- fpread = fopen(path, "r");
- }
- if (fpread) {
- if (!fgets(description, sizeof(description), fpread))
- description[0] = '\0';
- fclose(fpread);
- }
-
- /* read url or .git/url */
- joinpath(path, sizeof(path), repodir, "url");
- if (!(fpread = fopen(path, "r"))) {
- joinpath(path, sizeof(path), repodir, ".git/url");
- fpread = fopen(path, "r");
- }
- if (fpread) {
- if (!fgets(cloneurl, sizeof(cloneurl), fpread))
- cloneurl[0] = '\0';
- cloneurl[strcspn(cloneurl, "\n")] = '\0';
- fclose(fpread);
- }
-
- /* check LICENSE */
- for (i = 0; i < sizeof(licensefiles) / sizeof(*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 < sizeof(readmefiles) / sizeof(*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</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))
- err(1, "fread");
- if (fwrite(buf, 1, n, fp) != n ||
- fwrite(buf, 1, n, wcachefp) != n)
- err(1, "fwrite");
- }
- fclose(rcachefp);
- }
- fclose(wcachefp);
- } else {
- if (head)
- writelog(fp, head);
- }
-
- fputs("</tbody></table>", fp);
- writefooter(fp);
- fclose(fp);
-
- /* files for HEAD */
- fp = efopen("files.html", "w");
- writeheader(fp, "Files");
- if (head)
- writefiles(fp, head);
- writefooter(fp);
- fclose(fp);
-
- /* summary page with branches and tags */
- fp = efopen("refs.html", "w");
- writeheader(fp, "Refs");
- writerefs(fp);
- writefooter(fp);
- fclose(fp);
-
- /* Atom feed */
- fp = efopen("atom.xml", "w");
- writeatom(fp);
- fclose(fp);
-
- /* rename new cache file on success */
- if (cachefile && 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;
-}
+++ /dev/null
-/* $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 */
-}
+++ /dev/null
-/* $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 */
-}
+++ /dev/null
-body {
- font-size: 0.8rem;
- font-family: monospace;
- color: #ebdbb2;
- background-color: #282828;
- max-width: max-content;
- min-width: min-content;
-}
-
-pre {
- tab-size: 4;
-}
-
-h1, h2, h3, h4, h5, h6 {
- margin: 0;
-}
-
-img, h1, h2 {
- vertical-align: middle;
-}
-
-img {
- border: 0;
-}
-
-a:target {
- background-color: #eee;
-}
-
-a.d,
-a.h,
-a.i,
-a.line {
- text-decoration: none;
-}
-
-#blob {
- display: block;
- max-width: 100%;
-}
-
-article.markup {
- font-size: 15px;
- border: 2px solid #eee;
- border-radius: 10px;
- font-family: sans-serif;
- padding: 2.5em;
- margin: 2em 0;
-}
-
-article.markup code {
- font-size: 0.9em;
- border: 1px solid #dbdbdb;
- background-color: #f7f7f7;
- padding: 0 0.3em;
- border-radius: 0.3em
-}
-
-article.markup pre code {
- border: none;
- background: none;
- padding: 0;
- border-radius: 0;
-}
-
-article.markup pre {
- background-color: #f7f7f7;
- padding: 1em;
- border: 1px solid #dbdbdb;
- border-radius: 0.3em;
-}
-
-article.markup h1 {
- font-size: 2.4em;
- padding-bottom: 6px;
- border-bottom: 5px solid #0000000a;
-}
-
-article.markup h2 {
- font-size: 1.9em;
- padding-bottom: 5px;
- border-bottom: 2px solid #00000014;
-}
-
-article.markup h3 {
- font-size: 1.5em;
-}
-
-article.markup h4 {
- font-size: 1.3em;
-}
-
-article.markup h5 {
- font-size: 1.1em;
-}
-
-article.markup h6 {
- font-size: 1em;
-}
-
-article img {
- max-width: 100%;
-}
-
-.linenos {
- margin-right: 0;
- border-right: 1px solid rgb(0 0 0 / 8%);
- user-select: none;
-}
-
-.linenos a {
- margin-right: 0.9em;
- user-select: none;
- text-decoration: none;
-}
-
-#blob a {
- color: #777;
-}
-
-#blob a:hover {
- color: #eee;
- text-decoration: none;
-}
-
-a {
- color: #83a598;
-}
-
-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: #333;
-}
-
-#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: #928374;
-}
-
-hr {
- border: 0;
- border-top: 1px solid #928374;
- height: 1px;
-}
-
-pre {
- font-family: monospace;
-}
-
-pre a.h {
- color: #fe8019;
-}
-
-.A,
-span.i,
-pre a.i {
- color: #b8bb26;
-}
-
-.D,
-span.d,
-pre a.d {
- color: #fb4934;
-}
-
-pre a.h:hover,
-pre a.i:hover,
-pre a.d:hover {
- text-decoration: none;
-}
-
-.url td:nth-child(2) {
- padding-top: 0.2em;
- padding-bottom: 0.9em;
-}
-
-.url td:nth-child(2) span {
- padding: 1px 5px;
- border: 1px solid #ebdbb2;
- border-radius: 5px;
-}
-
-.url td:nth-child(2) span a {
- color: #ebdbb2;
-}
+++ /dev/null
-body{font-size:.8rem;font-family:monospace;color:#ebdbb2;background-color:#282828;max-width:max-content;min-width:min-content}pre{tab-size:4}h1,h2,h3,h4,h5,h6{margin:0}img,h1,h2{vertical-align:middle}img{border:0}a:target{background-color:#eee}a.d,a.h,a.i,a.line{text-decoration:none}#blob{display:block;max-width:100%}article.markup{font-size:15px;border:2px solid #eee;border-radius:10px;font-family:sans-serif;padding:2.5em;margin:2em 0}article.markup code{font-size:.9em;border:1px solid #dbdbdb;background-color:#f7f7f7;padding:0 .3em;border-radius:.3em}article.markup pre code{border:none;background:0 0;padding:0;border-radius:0}article.markup pre{background-color:#f7f7f7;padding:1em;border:1px solid #dbdbdb;border-radius:.3em}article.markup h1{font-size:2.4em;padding-bottom:6px;border-bottom:5px solid #0000000a}article.markup h2{font-size:1.9em;padding-bottom:5px;border-bottom:2px solid #00000014}article.markup h3{font-size:1.5em}article.markup h4{font-size:1.3em}article.markup h5{font-size:1.1em}article.markup h6{font-size:1em}article img{max-width:100%}.linenos{margin-right:0;border-right:1px solid rgb(0 0 0/8%);user-select:none}.linenos a{margin-right:.9em;user-select:none;text-decoration:none}#blob a{color:#777}#blob a:hover{color:#eee;text-decoration:none}a{color:#83a598}table thead td{font-weight:700}table td{padding: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:#333}#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:#928374}hr{border:0;border-top:1px solid #928374;height:1px}pre{font-family:monospace}pre a.h{color:#fe8019}.A,span.i,pre a.i{color:#b8bb26}.D,span.d,pre a.d{color:#fb4934}pre a.h:hover,pre a.i:hover,pre a.d:hover{text-decoration:none}.url td:nth-child(2){padding-top:.2em;padding-bottom:.9em}.url td:nth-child(2) span{padding:1px 5px;border:1px solid #ebdbb2;border-radius:5px}.url td:nth-child(2) span a{color:#ebdbb2}
\ No newline at end of file
+++ /dev/null
-.highlight .hll{background-color:#ffc}.highlight{background:#282828;color:#ebdbb2;background-color:#282828}.highlight .c{color:#928374;font-style:italic;background-color:#282828}.highlight .err{color:#ebdbb2;background-color:#282828}.highlight .esc{color:#ebdbb2;background-color:#282828}.highlight .g{color:#ebdbb2;background-color:#282828}.highlight .k{color:#fe8019;background-color:#282828}.highlight .l{color:#ebdbb2;background-color:#282828}.highlight .n{color:#ebdbb2;background-color:#282828}.highlight .o{color:#fe8019;background-color:#282828}.highlight .x{color:#ebdbb2;background-color:#282828}.highlight .p{color:#ebdbb2;background-color:#282828}.highlight .ch{color:#928374;font-style:italic;background-color:#282828}.highlight .cm{color:#928374;font-style:italic;background-color:#282828}.highlight .cp{color:#8ec07c;background-color:#282828}.highlight .c1{color:#928374;font-style:italic;background-color:#282828}.highlight .cs{color:#928374;font-style:italic;background-color:#282828}.highlight .gd{color:#282828;background-color:#fb4934}.highlight .ge{color:#83a598;text-decoration:underline;background-color:#282828}.highlight .gr{color:#ebdbb2;font-weight:700;background-color:#fb4934}.highlight .gh{color:#b8bb26;font-weight:700;background-color:#282828}.highlight .gi{color:#282828;background-color:#b8bb26}.highlight .go{color:#504945;background-color:#282828}.highlight .gp{color:#ebdbb2;background-color:#282828}.highlight .gs{color:#ebdbb2;background-color:#282828}.highlight .gu{color:#b8bb26;font-weight:700;background-color:#282828}.highlight .gt{color:#ebdbb2;font-weight:700;background-color:#fb4934}.highlight .kc{color:#fe8019;background-color:#282828}.highlight .kd{color:#fe8019;background-color:#282828}.highlight .kn{color:#fe8019;background-color:#282828}.highlight .kp{color:#fe8019;background-color:#282828}.highlight .kr{color:#fe8019;background-color:#282828}.highlight .kt{color:#fabd2f;background-color:#282828}.highlight .ld{color:#ebdbb2;background-color:#282828}.highlight .m{color:#d3869b;background-color:#282828}.highlight .s{color:#b8bb26;background-color:#282828}.highlight .na{color:#b8bb26;font-weight:700;background-color:#282828}.highlight .nb{color:#fabd2f;background-color:#282828}.highlight .nc{color:#ebdbb2;background-color:#282828}.highlight .no{color:#d3869b;background-color:#282828}.highlight .nd{color:#ebdbb2;background-color:#282828}.highlight .ni{color:#fabd2f;background-color:#282828}.highlight .ne{color:#fb4934;background-color:#282828}.highlight .nf{color:#fabd2f;background-color:#282828}.highlight .nl{color:#fb4934;background-color:#282828}.highlight .nn{color:#ebdbb2;background-color:#282828}.highlight .nx{color:#ebdbb2;background-color:#282828}.highlight .py{color:#ebdbb2;background-color:#282828}.highlight .nt{color:#fb4934;background-color:#282828}.highlight .nv{color:#ebdbb2;background-color:#282828}.highlight .ow{color:#fe8019;background-color:#282828}.highlight .w{color:#ebdbb2;background-color:#282828}.highlight .mb{color:#d3869b;background-color:#282828}.highlight .mf{color:#d3869b;background-color:#282828}.highlight .mh{color:#d3869b;background-color:#282828}.highlight .mi{color:#d3869b;background-color:#282828}.highlight .mo{color:#d3869b;background-color:#282828}.highlight .sb{color:#b8bb26;background-color:#282828}.highlight .sc{color:#b8bb26;background-color:#282828}.highlight .sd{color:#b8bb26;background-color:#282828}.highlight .s2{color:#b8bb26;background-color:#282828}.highlight .se{color:#b8bb26;background-color:#282828}.highlight .sh{color:#b8bb26;background-color:#282828}.highlight .si{color:#b8bb26;background-color:#282828}.highlight .sx{color:#b8bb26;background-color:#282828}.highlight .sr{color:#b8bb26;background-color:#282828}.highlight .s1{color:#b8bb26;background-color:#282828}.highlight .ss{color:#83a598;background-color:#282828}.highlight .bp{color:#fabd2f;background-color:#282828}.highlight .vc{color:#ebdbb2;background-color:#282828}.highlight .vg{color:#ebdbb2;background-color:#282828}.highlight .vi{color:#ebdbb2;background-color:#282828}.highlight .il{color:#d3869b;background-color:#282828}
\ No newline at end of file