From: Armaan Bhojwani Date: Mon, 8 Mar 2021 14:39:49 +0000 (-0500) Subject: Restructure repo X-Git-Url: https://git.armaanb.net/?p=stagit.git;a=commitdiff_plain;h=b0d3a4f50b346f540db0fd3f20c30bd78344eb2a Restructure repo --- diff --git a/Makefile b/Makefile index c0acb10..fbe5c86 100644 --- a/Makefile +++ b/Makefile @@ -18,27 +18,27 @@ STAGIT_LDFLAGS = ${LIBGIT_LIB} ${LDFLAGS} 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} @@ -54,8 +54,7 @@ dist: 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} | \ @@ -64,12 +63,12 @@ dist: ${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 @@ -81,36 +80,27 @@ install: all 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 diff --git a/compat.h b/compat.h deleted file mode 100644 index f97a69b..0000000 --- a/compat.h +++ /dev/null @@ -1,6 +0,0 @@ -#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); diff --git a/contrib/example_create.sh b/contrib/example_create.sh new file mode 100755 index 0000000..cf16e1d --- /dev/null +++ b/contrib/example_create.sh @@ -0,0 +1,43 @@ +#!/bin/sh +# - Makes index for repositories in a single directory. +# - Makes static pages for each repository directory. +# +# NOTE, things to do manually (once) before running this script: +# - copy style.css, logo.png and favicon.png manually, a style.css example +# is included. +# +# - write clone url, for example "git://git.codemadness.org/dir" to the "url" +# file for each repo. +# - write owner of repo to the "owner" file. +# - write description in "description" file. +# +# Usage: +# - mkdir -p htmldir && cd htmldir +# - sh example_create.sh + +# path must be absolute. +reposdir="/var/www/domains/git.codemadness.nl/home/src" +curdir="$(pwd)" + +# make index. +stagit-index "${reposdir}/"*/ > "${curdir}/index.html" + +# make files per repo. +for dir in "${reposdir}/"*/; do + # strip .git suffix. + r=$(basename "${dir}") + d=$(basename "${dir}" ".git") + printf "%s... " "${d}" + + mkdir -p "${curdir}/${d}" + cd "${curdir}/${d}" || continue + stagit -c ".cache" "${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 diff --git a/contrib/example_post-receive.sh b/contrib/example_post-receive.sh new file mode 100755 index 0000000..c9bcb5e --- /dev/null +++ b/contrib/example_post-receive.sh @@ -0,0 +1,73 @@ +#!/bin/sh +# generic git post-receive hook. +# change the config options below and call this script in your post-receive +# hook or symlink it. +# +# usage: $0 [name] +# +# if name is not set the basename of the current directory is used, +# this is the directory of the repo when called from the post-receive script. + +# NOTE: needs to be set for correct locale (expects UTF-8) otherwise the +# default is LC_CTYPE="POSIX". +export LC_CTYPE="en_US.UTF-8" + +name="$1" +if test "${name}" = ""; then + name=$(basename "$(pwd)") +fi + +# config +# paths must be absolute. +reposdir="/home/src/src" +dir="${reposdir}/${name}" +htmldir="/home/www/domains/git.codemadness.org/htdocs" +stagitdir="/" +destdir="${htmldir}${stagitdir}" +cachefile=".htmlcache" +# /config + +if ! test -d "${dir}"; then + echo "${dir} does not exist" >&2 + exit 1 +fi +cd "${dir}" || exit 1 + +# detect git push -f +force=0 +while read -r old new ref; do + test "${old}" = "0000000000000000000000000000000000000000" && continue + test "${new}" = "0000000000000000000000000000000000000000" && continue + + hasrevs=$(git rev-list "${old}" "^${new}" | sed 1q) + if test -n "${hasrevs}"; then + force=1 + break + fi +done + +# strip .git suffix. +r=$(basename "${name}") +d=$(basename "${name}" ".git") +printf "[%s] stagit HTML pages... " "${d}" + +mkdir -p "${destdir}/${d}" +cd "${destdir}/${d}" || exit 1 + +# remove commits and ${cachefile} on git push -f, this recreated later on. +if test "${force}" = "1"; then + rm -f "${cachefile}" + rm -rf "commit" +fi + +# make index. +stagit-index "${reposdir}/"*/ > "${destdir}/index.html" + +# make pages. +stagit -c "${cachefile}" "${reposdir}/${r}" + +ln -sf log.html index.html +ln -sf ../style.css style.css +ln -sf ../logo.png logo.png + +echo "done" diff --git a/contrib/repo-gen.sh b/contrib/repo-gen.sh new file mode 100755 index 0000000..1a33ee9 --- /dev/null +++ b/contrib/repo-gen.sh @@ -0,0 +1,61 @@ +#!/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 diff --git a/example_create.sh b/example_create.sh deleted file mode 100755 index cf16e1d..0000000 --- a/example_create.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/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 diff --git a/example_post-receive.sh b/example_post-receive.sh deleted file mode 100755 index c9bcb5e..0000000 --- a/example_post-receive.sh +++ /dev/null @@ -1,73 +0,0 @@ -#!/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" diff --git a/favicon.png b/favicon.png deleted file mode 100644 index b2e91c9..0000000 Binary files a/favicon.png and /dev/null differ diff --git a/highlight.py b/highlight.py deleted file mode 100755 index 2b81d8c..0000000 --- a/highlight.py +++ /dev/null @@ -1,62 +0,0 @@ -#!/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 += '
' - outp += rendered - outp += "
" -outp += f'
{highlight(contents, lexer, formatter)}
"' -outp += ' - * - * 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 -#include -#include -#include - -#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); -} diff --git a/repo-gen.sh b/repo-gen.sh deleted file mode 100755 index 1a33ee9..0000000 --- a/repo-gen.sh +++ /dev/null @@ -1,61 +0,0 @@ -#!/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 diff --git a/resources/favicon.png b/resources/favicon.png new file mode 100644 index 0000000..b2e91c9 Binary files /dev/null and b/resources/favicon.png differ diff --git a/resources/logo.png b/resources/logo.png new file mode 100644 index 0000000..b2e91c9 Binary files /dev/null and b/resources/logo.png differ diff --git a/resources/style.css b/resources/style.css new file mode 100644 index 0000000..af0d598 --- /dev/null +++ b/resources/style.css @@ -0,0 +1,211 @@ +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; +} diff --git a/resources/style.min.css b/resources/style.min.css new file mode 100644 index 0000000..063054a --- /dev/null +++ b/resources/style.min.css @@ -0,0 +1 @@ +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 diff --git a/resources/syntax.css b/resources/syntax.css new file mode 100644 index 0000000..4c774f6 --- /dev/null +++ b/resources/syntax.css @@ -0,0 +1 @@ +.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 diff --git a/src/compat.h b/src/compat.h new file mode 100644 index 0000000..f97a69b --- /dev/null +++ b/src/compat.h @@ -0,0 +1,6 @@ +#undef strlcat +size_t strlcat(char *, const char *, size_t); +#undef strlcpy +size_t strlcpy(char *, const char *, size_t); +#undef reallocarray +void *reallocarray(void *, size_t, size_t); diff --git a/src/highlight.py b/src/highlight.py new file mode 100755 index 0000000..2b81d8c --- /dev/null +++ b/src/highlight.py @@ -0,0 +1,62 @@ +#!/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 += '
' + outp += rendered + outp += "
" +outp += f'
{highlight(contents, lexer, formatter)}
"' +outp += ' + * + * 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 +#include +#include +#include + +#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); +} diff --git a/src/stagit-index.c b/src/stagit-index.c new file mode 100644 index 0000000..2b243ec --- /dev/null +++ b/src/stagit-index.c @@ -0,0 +1,220 @@ +#include +#include +#include +#include +#include +#include +#include + +#include + +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("\n" + "\n\n" + "\n" + "", fp); + xmlencode(fp, description, strlen(description)); + fprintf(fp, "\n\n", relpath); + fprintf(fp, "\n", relpath); + fputs("\n\n", fp); + fprintf(fp, "\n\n" + "\n
\"\"", relpath); + xmlencode(fp, description, strlen(description)); + fputs("
\n" + "
\n
\n
\n" + "\n" + "" + "" + "\n", fp); +} + +void +writefooter(FILE *fp) +{ + fputs("\n
NameDescriptionOwnerLast commit
\n
\n\n\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("", fp); + xmlencode(fp, stripped_name, strlen(stripped_name)); + fputs("", fp); + xmlencode(fp, description, strlen(description)); + fputs("", fp); + xmlencode(fp, owner, strlen(owner)); + fputs("", fp); + if (author) + printtimeshort(fp, &(author->when)); + fputs("", 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; +} diff --git a/src/stagit.c b/src/stagit.c new file mode 100644 index 0000000..264604e --- /dev/null +++ b/src/stagit.c @@ -0,0 +1,1319 @@ +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#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("\n" + "\n\n" + "\n" + "\n" + "", 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, "\n\n", relpath); + fprintf(fp, "\n", + name, relpath); + fprintf(fp, "\n", relpath); + fprintf(fp, "\n", relpath); + fputs("\n\n", fp); + if (cloneurl[0]) { + fputs("", fp); + } + fputs("
", fp); + fprintf(fp, "\"\"", + relpath, relpath); + fputs("

", fp); + xmlencode(fp, strippedname, strlen(strippedname)); + fputs("

", fp); + xmlencode(fp, description, strlen(description)); + fputs("
git clone ", fp); + xmlencode(fp, cloneurl, strlen(cloneurl)); + fputs("
\n", fp); + fprintf(fp, "Log | ", relpath); + fprintf(fp, "Files | ", relpath); + fprintf(fp, "Refs", relpath); + if (submodules) + fprintf(fp, " | Submodules", + relpath, submodules); + if (readme) + fprintf(fp, " | README", + relpath, readme); + if (license) + fprintf(fp, " | LICENSE", + relpath, license); + fputs("
\n
\n
\n", fp); +} + +void +writefooter(FILE *fp) +{ + fputs("
\n\n\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, "commit %s\n", + relpath, ci->oid, ci->oid); + + if (ci->parentoid[0]) + fprintf(fp, "parent %s\n", + relpath, ci->parentoid, ci->parentoid); + + if (ci->author) { + fputs("Author: ", fp); + xmlencode(fp, ci->author->name, strlen(ci->author->name)); + fputs(" <author->email, strlen(ci->author->email)); + 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)); + 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("Diffstat:\n", fp); + for (i = 0; i < ci->ndeltas; i++) { + delta = git_patch_get_delta(ci->deltas[i]->patch); + + switch (delta->status) { + case GIT_DELTA_ADDED: c = 'A'; break; + case GIT_DELTA_COPIED: c = 'C'; break; + case GIT_DELTA_DELETED: c = 'D'; break; + case GIT_DELTA_MODIFIED: c = 'M'; break; + case GIT_DELTA_RENAMED: c = 'R'; break; + case GIT_DELTA_TYPECHANGE: c = 'T'; break; + default: c = ' '; break; + } + if (c == ' ') + fprintf(fp, "\n", fp); + } + fprintf(fp, "
%c", c); + else + fprintf(fp, "
%c", c, c); + + fprintf(fp, "", 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, " | %zu", + ci->deltas[i]->addcount + ci->deltas[i]->delcount); + fwrite(&linestr, 1, add, fp); + fputs("", fp); + fwrite(&linestr[add], 1, del, fp); + fputs("
%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("
", fp); + + for (i = 0; i < ci->ndeltas; i++) { + patch = ci->deltas[i]->patch; + delta = git_patch_get_delta(patch); + fprintf(fp, "diff --git a/old_file.path, strlen(delta->old_file.path)); + fputs(".html\">", fp); + xmlencode(fp, delta->old_file.path, strlen(delta->old_file.path)); + fprintf(fp, " b/new_file.path, strlen(delta->new_file.path)); + fprintf(fp, ".html\">"); + xmlencode(fp, delta->new_file.path, strlen(delta->new_file.path)); + fprintf(fp, "\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, "", i, j, i, j); + xmlencode(fp, hunk->header, hunk->header_len); + fputs("", fp); + + for (k = 0; ; k++) { + if (git_patch_get_line_in_hunk(&line, patch, j, k)) + break; + if (line->old_lineno == -1) + fprintf(fp, "+", + i, j, k, i, j, k); + else if (line->new_lineno == -1) + fprintf(fp, "-", + 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("", fp); + } + } + } +} + +void +writelogline(FILE *fp, struct commitinfo *ci) +{ + fputs("", fp); + if (ci->author) + printtimeshort(fp, &(ci->author->when)); + fputs("", fp); + if (ci->summary) { + fprintf(fp, "", relpath, ci->oid); + xmlencode(fp, ci->summary, strlen(ci->summary)); + fputs("", fp); + } + fputs("", fp); + if (ci->author) + xmlencode(fp, ci->author->name, strlen(ci->author->name)); + fputs("", fp); + fprintf(fp, "%zu", ci->filecount); + fputs("", fp); + fprintf(fp, "+%zu", ci->addcount); + fputs("", fp); + fprintf(fp, "-%zu", ci->delcount); + fputs("\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("" + "More commits remaining [...]" + "\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("
", fpfile);
+			printshowfile(fpfile, ci);
+			fputs("
\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("\n", fp); + + fprintf(fp, "%s\n", ci->oid); + if (ci->author) { + fputs("", fp); + printtimez(fp, &(ci->author->when)); + fputs("\n", fp); + } + if (ci->committer) { + fputs("", fp); + printtimez(fp, &(ci->committer->when)); + fputs("\n", fp); + } + if (ci->summary) { + fputs("", fp); + xmlencode(fp, ci->summary, strlen(ci->summary)); + fputs("\n", fp); + } + fprintf(fp, "\n", + ci->oid); + + if (ci->author) { + fputs("\n", fp); + xmlencode(fp, ci->author->name, strlen(ci->author->name)); + fputs("\n", fp); + xmlencode(fp, ci->author->email, strlen(ci->author->email)); + fputs("\n\n", fp); + } + + fputs("", 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\n\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("\n" + "\n", fp); + xmlencode(fp, strippedname, strlen(strippedname)); + fputs(", branch HEAD\n", fp); + xmlencode(fp, description, strlen(description)); + fputs("\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("\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("

", fp); + xmlencode(fp, filename, strlen(filename)); + fprintf(fp, " (%juB)", (uintmax_t)filesize); + fputs("


", fp); + + if (git_blob_is_binary((git_blob *)obj)) { + fputs("

Binary file.

\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("", fp); + fputs(filemode(git_tree_entry_filemode(entry)), fp); + fprintf(fp, "", fp); + xmlencode(fp, entrypath, strlen(entrypath)); + fputs("", fp); + if (lc > 0) + fprintf(fp, "%dL", lc); + else + fprintf(fp, "%juB", (uintmax_t)filesize); + fputs("\n", fp); + git_object_free(obj); + } else if (!git_submodule_lookup(&module, repo, entryname)) { + fprintf(fp, "m---------", + relpath); + xmlencode(fp, entrypath, strlen(entrypath)); + git_submodule_free(module); + fputs("\n", fp); + } + } + + return 0; +} + +int +writefiles(FILE *fp, const git_oid *id) +{ + git_tree *tree = NULL; + git_commit *commit = NULL; + int ret = -1; + + fputs("\n" + "" + "" + "\n\n", fp); + + if (!git_commit_lookup(&commit, repo, id) && + !git_commit_tree(&tree, commit)) + ret = writefilestree(fp, tree, ""); + + fputs("
ModeNameSize
", 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, "

%s

" + "\n" + "" + "\n\n" + "\n", + titles[j], ids[j]); + } + + relpath = ""; + name = git_reference_shorthand(r); + + fputs("\n", fp); + + relpath = "../"; + + commitinfo_free(ci); + git_object_free(obj); + obj = NULL; + git_reference_free(dref); + dref = NULL; + } + /* table footer */ + if (count) + fputs("
NameLast commit dateAuthor
", fp); + xmlencode(fp, name, strlen(name)); + fputs("", fp); + if (ci->author) + printtimeshort(fp, &(ci->author->when)); + fputs("", fp); + if (ci->author) + xmlencode(fp, ci->author->name, strlen(ci->author->name)); + fputs("

", 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("\n" + "" + "" + "" + "\n\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("
DateCommitAuthorFiles+-
", 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; +} diff --git a/src/strlcat.c b/src/strlcat.c new file mode 100644 index 0000000..bbfa64f --- /dev/null +++ b/src/strlcat.c @@ -0,0 +1,57 @@ +/* $OpenBSD: strlcat.c,v 1.15 2015/03/02 21:41:08 millert Exp $ */ + +/* + * Copyright (c) 1998, 2015 Todd C. Miller + * + * 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 +#include + +#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 */ +} diff --git a/src/strlcpy.c b/src/strlcpy.c new file mode 100644 index 0000000..ab420b6 --- /dev/null +++ b/src/strlcpy.c @@ -0,0 +1,52 @@ +/* $OpenBSD: strlcpy.c,v 1.12 2015/01/15 03:54:12 millert Exp $ */ + +/* + * Copyright (c) 1998, 2015 Todd C. Miller + * + * 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 +#include + +#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 */ +} diff --git a/stagit-index.1 b/stagit-index.1 deleted file mode 100644 index 9abd448..0000000 --- a/stagit-index.1 +++ /dev/null @@ -1,43 +0,0 @@ -.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 diff --git a/stagit-index.c b/stagit-index.c deleted file mode 100644 index 2b243ec..0000000 --- a/stagit-index.c +++ /dev/null @@ -1,220 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include - -#include - -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("\n" - "\n\n" - "\n" - "", fp); - xmlencode(fp, description, strlen(description)); - fprintf(fp, "\n\n", relpath); - fprintf(fp, "\n", relpath); - fputs("\n\n", fp); - fprintf(fp, "\n\n" - "\n
\"\"", relpath); - xmlencode(fp, description, strlen(description)); - fputs("
\n" - "
\n
\n
\n" - "\n" - "" - "" - "\n", fp); -} - -void -writefooter(FILE *fp) -{ - fputs("\n
NameDescriptionOwnerLast commit
\n
\n\n\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("", fp); - xmlencode(fp, stripped_name, strlen(stripped_name)); - fputs("", fp); - xmlencode(fp, description, strlen(description)); - fputs("", fp); - xmlencode(fp, owner, strlen(owner)); - fputs("", fp); - if (author) - printtimeshort(fp, &(author->when)); - fputs("", 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; -} diff --git a/stagit.1 b/stagit.1 deleted file mode 100644 index 23e7ca7..0000000 --- a/stagit.1 +++ /dev/null @@ -1,108 +0,0 @@ -.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 diff --git a/stagit.c b/stagit.c deleted file mode 100644 index 264604e..0000000 --- a/stagit.c +++ /dev/null @@ -1,1319 +0,0 @@ -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#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("\n" - "\n\n" - "\n" - "\n" - "", 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, "\n\n", relpath); - fprintf(fp, "\n", - name, relpath); - fprintf(fp, "\n", relpath); - fprintf(fp, "\n", relpath); - fputs("\n\n", fp); - if (cloneurl[0]) { - fputs("", fp); - } - fputs("
", fp); - fprintf(fp, "\"\"", - relpath, relpath); - fputs("

", fp); - xmlencode(fp, strippedname, strlen(strippedname)); - fputs("

", fp); - xmlencode(fp, description, strlen(description)); - fputs("
git clone ", fp); - xmlencode(fp, cloneurl, strlen(cloneurl)); - fputs("
\n", fp); - fprintf(fp, "Log | ", relpath); - fprintf(fp, "Files | ", relpath); - fprintf(fp, "Refs", relpath); - if (submodules) - fprintf(fp, " | Submodules", - relpath, submodules); - if (readme) - fprintf(fp, " | README", - relpath, readme); - if (license) - fprintf(fp, " | LICENSE", - relpath, license); - fputs("
\n
\n
\n", fp); -} - -void -writefooter(FILE *fp) -{ - fputs("
\n\n\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, "commit %s\n", - relpath, ci->oid, ci->oid); - - if (ci->parentoid[0]) - fprintf(fp, "parent %s\n", - relpath, ci->parentoid, ci->parentoid); - - if (ci->author) { - fputs("Author: ", fp); - xmlencode(fp, ci->author->name, strlen(ci->author->name)); - fputs(" <author->email, strlen(ci->author->email)); - 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)); - 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("Diffstat:\n", fp); - for (i = 0; i < ci->ndeltas; i++) { - delta = git_patch_get_delta(ci->deltas[i]->patch); - - switch (delta->status) { - case GIT_DELTA_ADDED: c = 'A'; break; - case GIT_DELTA_COPIED: c = 'C'; break; - case GIT_DELTA_DELETED: c = 'D'; break; - case GIT_DELTA_MODIFIED: c = 'M'; break; - case GIT_DELTA_RENAMED: c = 'R'; break; - case GIT_DELTA_TYPECHANGE: c = 'T'; break; - default: c = ' '; break; - } - if (c == ' ') - fprintf(fp, "\n", fp); - } - fprintf(fp, "
%c", c); - else - fprintf(fp, "
%c", c, c); - - fprintf(fp, "", 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, " | %zu", - ci->deltas[i]->addcount + ci->deltas[i]->delcount); - fwrite(&linestr, 1, add, fp); - fputs("", fp); - fwrite(&linestr[add], 1, del, fp); - fputs("
%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("
", fp); - - for (i = 0; i < ci->ndeltas; i++) { - patch = ci->deltas[i]->patch; - delta = git_patch_get_delta(patch); - fprintf(fp, "diff --git a/old_file.path, strlen(delta->old_file.path)); - fputs(".html\">", fp); - xmlencode(fp, delta->old_file.path, strlen(delta->old_file.path)); - fprintf(fp, " b/new_file.path, strlen(delta->new_file.path)); - fprintf(fp, ".html\">"); - xmlencode(fp, delta->new_file.path, strlen(delta->new_file.path)); - fprintf(fp, "\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, "", i, j, i, j); - xmlencode(fp, hunk->header, hunk->header_len); - fputs("", fp); - - for (k = 0; ; k++) { - if (git_patch_get_line_in_hunk(&line, patch, j, k)) - break; - if (line->old_lineno == -1) - fprintf(fp, "+", - i, j, k, i, j, k); - else if (line->new_lineno == -1) - fprintf(fp, "-", - 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("", fp); - } - } - } -} - -void -writelogline(FILE *fp, struct commitinfo *ci) -{ - fputs("", fp); - if (ci->author) - printtimeshort(fp, &(ci->author->when)); - fputs("", fp); - if (ci->summary) { - fprintf(fp, "", relpath, ci->oid); - xmlencode(fp, ci->summary, strlen(ci->summary)); - fputs("", fp); - } - fputs("", fp); - if (ci->author) - xmlencode(fp, ci->author->name, strlen(ci->author->name)); - fputs("", fp); - fprintf(fp, "%zu", ci->filecount); - fputs("", fp); - fprintf(fp, "+%zu", ci->addcount); - fputs("", fp); - fprintf(fp, "-%zu", ci->delcount); - fputs("\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("" - "More commits remaining [...]" - "\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("
", fpfile);
-			printshowfile(fpfile, ci);
-			fputs("
\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("\n", fp); - - fprintf(fp, "%s\n", ci->oid); - if (ci->author) { - fputs("", fp); - printtimez(fp, &(ci->author->when)); - fputs("\n", fp); - } - if (ci->committer) { - fputs("", fp); - printtimez(fp, &(ci->committer->when)); - fputs("\n", fp); - } - if (ci->summary) { - fputs("", fp); - xmlencode(fp, ci->summary, strlen(ci->summary)); - fputs("\n", fp); - } - fprintf(fp, "\n", - ci->oid); - - if (ci->author) { - fputs("\n", fp); - xmlencode(fp, ci->author->name, strlen(ci->author->name)); - fputs("\n", fp); - xmlencode(fp, ci->author->email, strlen(ci->author->email)); - fputs("\n\n", fp); - } - - fputs("", 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\n\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("\n" - "\n", fp); - xmlencode(fp, strippedname, strlen(strippedname)); - fputs(", branch HEAD\n", fp); - xmlencode(fp, description, strlen(description)); - fputs("\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("\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("

", fp); - xmlencode(fp, filename, strlen(filename)); - fprintf(fp, " (%juB)", (uintmax_t)filesize); - fputs("


", fp); - - if (git_blob_is_binary((git_blob *)obj)) { - fputs("

Binary file.

\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("", fp); - fputs(filemode(git_tree_entry_filemode(entry)), fp); - fprintf(fp, "", fp); - xmlencode(fp, entrypath, strlen(entrypath)); - fputs("", fp); - if (lc > 0) - fprintf(fp, "%dL", lc); - else - fprintf(fp, "%juB", (uintmax_t)filesize); - fputs("\n", fp); - git_object_free(obj); - } else if (!git_submodule_lookup(&module, repo, entryname)) { - fprintf(fp, "m---------", - relpath); - xmlencode(fp, entrypath, strlen(entrypath)); - git_submodule_free(module); - fputs("\n", fp); - } - } - - return 0; -} - -int -writefiles(FILE *fp, const git_oid *id) -{ - git_tree *tree = NULL; - git_commit *commit = NULL; - int ret = -1; - - fputs("\n" - "" - "" - "\n\n", fp); - - if (!git_commit_lookup(&commit, repo, id) && - !git_commit_tree(&tree, commit)) - ret = writefilestree(fp, tree, ""); - - fputs("
ModeNameSize
", 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, "

%s

" - "\n" - "" - "\n\n" - "\n", - titles[j], ids[j]); - } - - relpath = ""; - name = git_reference_shorthand(r); - - fputs("\n", fp); - - relpath = "../"; - - commitinfo_free(ci); - git_object_free(obj); - obj = NULL; - git_reference_free(dref); - dref = NULL; - } - /* table footer */ - if (count) - fputs("
NameLast commit dateAuthor
", fp); - xmlencode(fp, name, strlen(name)); - fputs("", fp); - if (ci->author) - printtimeshort(fp, &(ci->author->when)); - fputs("", fp); - if (ci->author) - xmlencode(fp, ci->author->name, strlen(ci->author->name)); - fputs("

", 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("\n" - "" - "" - "" - "\n\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("
DateCommitAuthorFiles+-
", 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; -} diff --git a/strlcat.c b/strlcat.c deleted file mode 100644 index bbfa64f..0000000 --- a/strlcat.c +++ /dev/null @@ -1,57 +0,0 @@ -/* $OpenBSD: strlcat.c,v 1.15 2015/03/02 21:41:08 millert Exp $ */ - -/* - * Copyright (c) 1998, 2015 Todd C. Miller - * - * 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 -#include - -#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 */ -} diff --git a/strlcpy.c b/strlcpy.c deleted file mode 100644 index ab420b6..0000000 --- a/strlcpy.c +++ /dev/null @@ -1,52 +0,0 @@ -/* $OpenBSD: strlcpy.c,v 1.12 2015/01/15 03:54:12 millert Exp $ */ - -/* - * Copyright (c) 1998, 2015 Todd C. Miller - * - * 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 -#include - -#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 */ -} diff --git a/style.css b/style.css deleted file mode 100644 index af0d598..0000000 --- a/style.css +++ /dev/null @@ -1,211 +0,0 @@ -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; -} diff --git a/style.min.css b/style.min.css deleted file mode 100644 index 063054a..0000000 --- a/style.min.css +++ /dev/null @@ -1 +0,0 @@ -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 diff --git a/syntax.css b/syntax.css deleted file mode 100644 index 4c774f6..0000000 --- a/syntax.css +++ /dev/null @@ -1 +0,0 @@ -.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