git home / emma home
logo

sgw

Static git web. Emma's fork of stagit.
git clone https://git.y1.nz/archives/sgw.tar.gz
README | Files | Log | Refs | LICENSE

commit b325811df985e925cf473e3e5aadb95dabe7a619
parent 42eae3e1bbc5085115e948f426e6e9a52e4d2a76
Author: Emma Weaver <emma@waeaves.com>
Date:   Sat, 23 May 2026 10:50:18 -0400

Redid args, added config, simplified build

Diffstat:
M.gitignore1+
MMakefile122++++++++++++-------------------------------------------------------------------
MTODO22+++++++++++-----------
Mcompat.h2--
Aconfig.h24++++++++++++++++++++++++
Aconfig.mk14++++++++++++++
Dexample_create.sh43-------------------------------------------
Dexample_post-receive.sh73-------------------------------------------------------------------------
Afiles.c0
Afiles.h0
Amain.c1866+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aopt.c62++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aopt.h34++++++++++++++++++++++++++++++++++
Dstagit-index.147-----------------------------------------------
Mstagit-index.c78+-----------------------------------------------------------------------------
Dstagit.c1853-------------------------------------------------------------------------------
Dstrlcat.c57---------------------------------------------------------
Dstrlcpy.c52----------------------------------------------------
Mstyle.css1+
19 files changed, 2032 insertions(+), 2319 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -3,4 +3,5 @@ stagit stagit-index test +old diff --git a/Makefile b/Makefile @@ -1,113 +1,27 @@ -.POSIX: -NAME = stagit -VERSION = 1.2 +include config.mk -# paths -PREFIX = /usr/local -MANPREFIX = ${PREFIX}/man -DOCPREFIX = ${PREFIX}/share/doc/${NAME} +tigit: main.c opt.o files.o config.mk + $(CC) main.c opt.o files.o -o stagit $(LDFLAGS) -LIBGIT_INC = -I/usr/local/include -LIBGIT_LIB = -L/usr/local/lib -lgit2 +opt.o: opt.c opt.h config.mk + $(CC) -c opt.c -o opt.o $(CFLAGS) -# use system flags. -STAGIT_CFLAGS = ${LIBGIT_INC} ${CFLAGS} -STAGIT_LDFLAGS = ${LIBGIT_LIB} ${LDFLAGS} -STAGIT_CPPFLAGS = -D_XOPEN_SOURCE=700 -D_DEFAULT_SOURCE -D_BSD_SOURCE - -# Uncomment to enable workaround for older libgit2 which don't support this -# option. This workaround will be removed in the future *pinky promise*. -#STAGIT_CFLAGS += -DGIT_OPT_SET_OWNER_VALIDATION=-1 - -SRC = \ - stagit.c\ - stagit-index.c -COMPATSRC = \ - reallocarray.c\ - strlcat.c\ - strlcpy.c -BIN = \ - stagit\ - stagit-index -MAN1 = \ - stagit.1\ - stagit-index.1 -DOC = \ - LICENSE\ - README -HDR = compat.h - -COMPATOBJ = \ - reallocarray.o\ - strlcat.o\ - strlcpy.o - -OBJ = ${SRC:.c=.o} ${COMPATOBJ} - -all: ${BIN} - -.o: - ${CC} -o $@ ${LDFLAGS} - -.c.o: - ${CC} -o $@ -c $< ${STAGIT_CFLAGS} ${STAGIT_CPPFLAGS} - -dist: - rm -rf ${NAME}-${VERSION} - mkdir -p ${NAME}-${VERSION} - cp -f ${MAN1} ${HDR} ${SRC} ${COMPATSRC} ${DOC} \ - Makefile favicon.png logo.png style.css \ - example_create.sh example_post-receive.sh \ - ${NAME}-${VERSION} - # make tarball - tar -cf - ${NAME}-${VERSION} | \ - gzip -c > ${NAME}-${VERSION}.tar.gz - rm -rf ${NAME}-${VERSION} - -${OBJ}: ${HDR} - -stagit: stagit.o ${COMPATOBJ} - ${CC} -o $@ stagit.o ${COMPATOBJ} ${STAGIT_LDFLAGS} - -stagit-index: stagit-index.o ${COMPATOBJ} - ${CC} -o $@ stagit-index.o ${COMPATOBJ} ${STAGIT_LDFLAGS} +files.o: files.c files.h config.mk + $(CC) -c files.c -o files.o $(CFLAGS) clean: - rm -f ${BIN} ${OBJ} ${NAME}-${VERSION}.tar.gz + rm -f opt.o stagit -install: all - # installing executable files. - mkdir -p ${DESTDIR}${PREFIX}/bin - cp -f ${BIN} ${DESTDIR}${PREFIX}/bin - for f in ${BIN}; do chmod 755 ${DESTDIR}${PREFIX}/bin/$$f; done - # installing example files. - mkdir -p ${DESTDIR}${DOCPREFIX} - cp -f style.css\ - favicon.png\ - logo.png\ - example_create.sh\ - example_post-receive.sh\ - README\ - ${DESTDIR}${DOCPREFIX} - # installing manual pages. - mkdir -p ${DESTDIR}${MANPREFIX}/man1 - cp -f ${MAN1} ${DESTDIR}${MANPREFIX}/man1 - for m in ${MAN1}; do chmod 644 ${DESTDIR}${MANPREFIX}/man1/$$m; done +install: stagit config.mk + mkdir -p $(PREFIX)/bin + cp -f stagit $(PREFIX)/bin + chmod 755 $(PREFIX)/bin/stagit + mkdir -p $(MANPREFIX)/man1 + sed "s/VERSION/$(VERSION)/g" < stagit.1 > $(MANPREFIX)/man1/stagit.1 + chmod 644 $(MANPREFIX)/man1/stagit.1 -uninstall: - # removing executable files. - for f in ${BIN}; do rm -f ${DESTDIR}${PREFIX}/bin/$$f; done - # removing example files. - rm -f \ - ${DESTDIR}${DOCPREFIX}/style.css\ - ${DESTDIR}${DOCPREFIX}/favicon.png\ - ${DESTDIR}${DOCPREFIX}/logo.png\ - ${DESTDIR}${DOCPREFIX}/example_create.sh\ - ${DESTDIR}${DOCPREFIX}/example_post-receive.sh\ - ${DESTDIR}${DOCPREFIX}/README - -rmdir ${DESTDIR}${DOCPREFIX} - # removing manual pages. - for m in ${MAN1}; do rm -f ${DESTDIR}${MANPREFIX}/man1/$$m; done +uninstall: config.mk + rm -f $(PREFIX)/bin/stagit + rm -f $(MANPREFIX)/man1/stagit.1 -.PHONY: all clean dist install uninstall diff --git a/TODO b/TODO @@ -5,13 +5,12 @@ - but how to handle includes/headers... [ ] Redesign options / I/O - - Usage: stagit [-i] [-d DESTDIR] [-c CACHEDIR] REPO [REPO_2 ... REPO_N] - - generates index page if -i - - puts repos in their own subdirectory if -i or REPO_2 - - writes output into DESTDIR (defaults to pwd) -[ ] create config.h file - - reused format strings (such as header) +[x] create config.h file + [x] reused document chunks (header, footer, logo, etc) + [ ] nested (whether to show all files on files.html) + [x] backlinks (whether to show .. in directories) + [ ] commit depth limit ---NEW FEATURES--- [x] include binary files in static site + link to them @@ -20,16 +19,17 @@ [ ] add lines of code totals on files page [x] add lines of code on each file's page [x] show line/byte totals for each directory -[ ] tweak paths that start with dots, so that they don't get hidden or 403'd +[x] tweak paths that start with dots, so that they don't get hidden or 403'd - put parentheses around the dirnames in the urls -[ ] while writing log, only print links to files whose pages exist -[ ] add backlinks on files +[x] while writing log, only print links to files whose pages exist +[x] add backlinks on files ---END NEW FEATURES--- +[ ] Incorporate index page into main.c +[ ] Separate page-generation and main's responsibilities [ ] Remove globals - - RELPATH. wtf even... -[ ] Redo caching, to never repeat work +[ ] Redo caching, to minimally repeat work - but it needs to also not increase the complexity. - it's ok to duplicate storage - perhaps: save every reusable intermediate into a file. diff --git a/compat.h b/compat.h @@ -1,6 +1,4 @@ #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/config.h b/config.h @@ -0,0 +1,24 @@ + +/* CONFIG FOR INDEX PAGE */ +#define INDEX_DEST "/index.html" +#define INDEX_LOGO "/logo.png" + +/* CONFIG FOR REPOSITORY PAGES */ +#define HEADER "<p>bonjour!</p>" +#define FOOTER "<p>ciao!</p>" + +/* These paths are all relative to the [-o OUT] directory, or pwd. */ +// TODO figure out how to use the [-b BASE] option along with that... +#define PAGE_DEST "/%s", stripped_name +#define STYLE_HREF "/style.css" +#define LOGO_SRC "/logo.png" +#define LOGO_HREF "/index.html" + +static const int dir_pages = 1; /* generate pages for each directory */ +static const int backlinks = 0; /* include ".." in directories */ +static const int max_commits = 100; +static const char *license_files[] = { + "HEAD:LICENSE", "HEAD:LICENSE.md", "HEAD:LICENSE.txt", "HEAD:COPYING" +}; +static const char *readme_files[] = { "HEAD:README", "HEAD:README.md" }; + diff --git a/config.mk b/config.mk @@ -0,0 +1,14 @@ +# update this file to match your system's configuration. + +VERSION = 0.0 + +PREFIX = /usr/local +MANPREFIX = $(PREFIX)/share/man + +CFLAGS = -I/usr/local/include +LDFLAGS = -L/usr/local/lib -lgit2 + +FLAGS = -std=c99 -pedantic -Wall -Wno-deprecated-declarations -Os \ + -D_XOPEN_SOURCE=700 -D_DEFAULT_SOURCE -D_BSD_SOURCE \ + -Wno-format-zero-length +CC = c99 $(FLAGS) diff --git a/example_create.sh b/example_create.sh @@ -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" -u "https://git.codemadness.nl/$d/" "${reposdir}/${r}" - - # symlinks - ln -sf log.html index.html - ln -sf ../style.css style.css - ln -sf ../logo.png logo.png - ln -sf ../favicon.png favicon.png - - echo "done" -done diff --git a/example_post-receive.sh b/example_post-receive.sh @@ -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}" -u "https://git.codemadness.nl/$d/" "${reposdir}/${r}" - -ln -sf log.html index.html -ln -sf ../style.css style.css -ln -sf ../logo.png logo.png - -echo "done" diff --git a/files.c b/files.c diff --git a/files.h b/files.h diff --git a/main.c b/main.c @@ -0,0 +1,1866 @@ +#include <sys/stat.h> +#include <sys/types.h> + +#include <err.h> +#include <errno.h> +#include <libgen.h> +#include <limits.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> + +#include <git2.h> + +#include "compat.h" +#include "config.h" +#include "opt.h" + +#define CPUT(f, c) putc((c), (f)) +#define SPUTF(f, s) fputs((s), (f)) +#define LENGTH(s) (sizeof(s)/sizeof(*s)) + +struct DeltaInfo { + git_patch *patch; + + size_t add_count; + size_t del_count; +}; + +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 add_count; + size_t del_count; + size_t file_count; + + struct DeltaInfo **deltas; + size_t ndeltas; +}; + +/* reference and associated data for sorting */ +struct ReferenceInfo { + struct git_reference *ref; + struct CommitInfo *ci; +}; + +struct Weight { + size_t bytes; + size_t lines; +}; + +static const char orders[] = { ' ', 'k', 'M', 'G', 'T', 'P', 'E' }; +static const char tab[] = "0123456789ABCDEF"; + +/* GLOBALS */ +// TODO untangle these, pass them around instead... + +static git_repository *repo; + +/* flags + arguments */ +static Opt opt; +static const char *repodir; + +static char *name = ""; +static char *stripped_name = ""; +static char description[255]; +static char cloneurl[1024]; +static char *submodules; +static const char *license; +static const char *readme; +static long long nlogcommits = -1; /* -1 indicates not used */ + +/* cache */ +static git_oid lastoid; +static char lastoidstr[GIT_OID_HEXSZ + 2]; /* id + newline + NUL byte */ +static FILE *rcachefp, *wcachefp; +static const char *cachefile; + +int +mkdirp(const char *path) +{ + const char *p; + char tmp[PATH_MAX]; + char *t; + + for(t = tmp, p = path; *p != '\0'; p++, t++) { + if (*p != '/') { + *t = *p; + continue; + } + + *t = '\0'; + if ( + mkdir(tmp, S_IRWXU | S_IRWXG | S_IRWXO) < 0 && + errno != EEXIST + ) + return -1; + *t = '/'; + } + return 0; +} + +FILE * +efopen(const char *filename, const char *flags) +{ + FILE *fp; + mkdirp(filename); + if (!(fp = fopen(filename, flags))) + err(1, "fopen: '%s'", filename); + return fp; +} + +/* Handle read or write errors for a FILE * stream */ +void +check_file_error(FILE *fp, const char *name, int mode) +{ + if (mode == 'r' && ferror(fp)) + errx(1, "read error: %s", name); + else if (mode == 'w' && (fflush(fp) || ferror(fp))) + errx(1, "write error: %s", name); +} + +void +join_path(char *buf, size_t bufsiz, const char *path, const char *path2) +{ + int r; + char * separator = ""; + if(path[0] && path[strlen(path) - 1] != '/') + separator = "/"; + + r = snprintf(buf, bufsiz, "%s%s%s", path, separator, path2); + if (r < 0 || (size_t)r >= bufsiz) + errx( + 1, + "path truncated: '%s%s%s'", + path, + separator, + path2 + ); +} + +void +find_license() +{ + git_object *obj = NULL; + int i; + for (i = 0; i < LENGTH(license_files) && !license; i++) { + if ( + !git_revparse_single(&obj, repo, license_files[i]) && + git_object_type(obj) == GIT_OBJ_BLOB + ) + license = license_files[i] + strlen("HEAD:"); + git_object_free(obj); + } +} + +void +find_readme() +{ + git_object *obj = NULL; + int i; + for (i = 0; i < LENGTH(readme_files) && !readme; i++) { + if ( + !git_revparse_single(&obj, repo, readme_files[i]) && + git_object_type(obj) == GIT_OBJ_BLOB + ) + readme = readme_files[i] + strlen("HEAD:"); + git_object_free(obj); + } +} + +void +delta_info_free(struct DeltaInfo *di) +{ + if (!di) return; + + git_patch_free(di->patch); + memset(di, 0, sizeof(*di)); + free(di); +} + +int +commit_info_get_stats(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->add_count++; + ci->add_count++; + } else if (line->new_lineno == -1) { + di->del_count++; + ci->del_count++; + } + } + } + } + ci->ndeltas = i; + ci->file_count = 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++) + delta_info_free(ci->deltas[i]); + free(ci->deltas); + ci->deltas = NULL; + ci->ndeltas = 0; + ci->add_count = 0; + ci->del_count = 0; + ci->file_count = 0; + + return -1; +} + +void +commit_info_free(struct CommitInfo *ci) +{ + size_t i; + + if (!ci) + return; + if (ci->deltas) + for (i = 0; i < ci->ndeltas; i++) + delta_info_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 * +commit_info_get_by_oid(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: + commit_info_free(ci); + + return NULL; +} + +int +refs_cmp(const void *v1, const void *v2) +{ + const struct ReferenceInfo *r1 = v1, *r2 = v2; + time_t t1, t2; + int r; + + if ((r = git_reference_is_tag(r1->ref) - git_reference_is_tag(r2->ref))) + return r; + + t1 = r1->ci->author ? r1->ci->author->when.time : 0; + t2 = r2->ci->author ? r2->ci->author->when.time : 0; + if ((r = t1 > t2 ? -1 : (t1 == t2 ? 0 : 1))) + return r; + + return strcmp( + git_reference_shorthand(r1->ref), + git_reference_shorthand(r2->ref) + ); +} + +int +get_refs(struct ReferenceInfo **pris, size_t *prefcount) +{ + struct ReferenceInfo *ris = NULL; + struct CommitInfo *ci = NULL; + git_reference_iterator *it = NULL; + const git_oid *id = NULL; + git_object *obj = NULL; + git_reference *dref = NULL, *r, *ref = NULL; + size_t i, refcount; + + *pris = NULL; + *prefcount = 0; + + if (git_reference_iterator_new(&it, repo)) + return -1; + + for (refcount = 0; !git_reference_next(&ref, it); ) { + if (!git_reference_is_branch(ref) && !git_reference_is_tag(ref)) { + git_reference_free(ref); + ref = NULL; + continue; + } + + switch (git_reference_type(ref)) { + case GIT_REF_SYMBOLIC: + if (git_reference_resolve(&dref, ref)) + goto err; + r = dref; + break; + case GIT_REF_OID: + r = ref; + break; + default: + continue; + } + if ( + !git_reference_target(r) || + git_reference_peel(&obj, r, GIT_OBJ_ANY) + ) + goto err; + if (!(id = git_object_id(obj))) + goto err; + if (!(ci = commit_info_get_by_oid(id))) + break; + + if (!(ris = reallocarray(ris, refcount + 1, sizeof(*ris)))) + err(1, "realloc"); + ris[refcount].ci = ci; + ris[refcount].ref = r; + refcount++; + + git_object_free(obj); + obj = NULL; + git_reference_free(dref); + dref = NULL; + } + git_reference_iterator_free(it); + + /* sort by type, date then shorthand name */ + qsort(ris, refcount, sizeof(*ris), refs_cmp); + + *pris = ris; + *prefcount = refcount; + + return 0; + +err: + git_object_free(obj); + git_reference_free(dref); + commit_info_free(ci); + for (i = 0; i < refcount; i++) { + commit_info_free(ris[i].ci); + git_reference_free(ris[i].ref); + } + free(ris); + + return -1; +} + +/* Percent-encode, see RFC3986 section 2.1. */ +void +put_percent_char(FILE *fp, unsigned char uc) +{ + /* NOTE: do not encode '/' for paths or ",-." */ + if ( + uc < ',' || + uc >= 127 || + (uc >= ':' && uc <= '@') || + uc == '[' || + uc == ']' + ) { + CPUT(fp, '%'); + CPUT(fp, tab[(uc >> 4) & 0x0f]); + CPUT(fp, tab[uc & 0x0f]); + return; + } + + CPUT(fp, uc); +} + +void +put_percent_encoded(FILE *fp, const char *s, size_t len) +{ + size_t i; + + for (i = 0; *s && i < len; s++, i++) + put_percent_char(fp, *s); +} + +/* Escape characters below as HTML 2.0 / XML 1.0. */ +void +put_xml_char(FILE *fp, const char *s) +{ + switch(*s) { + case '<': fputs("&lt;", fp); break; + case '>': fputs("&gt;", fp); break; + case '\'': fputs("&#39;", fp); break; + case '&': fputs("&amp;", fp); break; + case '"': fputs("&quot;", fp); break; + default: putc(*s, fp); + } +} + +void +put_xml(FILE *fp, const char *s, size_t len) +{ + size_t i = 0; + for (; *s && i < len; s++, i++) + put_xml_char(fp, s); +} + +/* terminate on, but don't print '\r' or '\n' */ +void +put_xml_line(FILE *fp, const char *s, size_t len) +{ + size_t i = 0; + for (; *s && i < len; s++, i++) { + if (*s == '\r' || *s == '\n') + break; + put_xml_char(fp, s); + } +} + +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 +put_absolute_path(FILE *fp, const char * path) +{ + fprintf(fp, opt.base_url); + fprintf(fp, PAGE_DEST); + fprintf(fp, "/%s", path); +} + +void +put_header(FILE *fp, const char *title) +{ + fputs( + "<!DOCTYPE html>\n" + "<html>" "\n" + "<head>" "\n" + "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n" + "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n" + "<title>", + fp + ); + put_xml(fp, title, strlen(title)); + if (title[0] && stripped_name[0]) + fputs(" - ", fp); + put_xml(fp, stripped_name, strlen(stripped_name)); + if (description[0]) + fputs(" - ", fp); + put_xml(fp, description, strlen(description)); + SPUTF(fp, "</title>" "\n"); + + SPUTF(fp, "<link rel=\"icon\" type=\"image/png\" href=\""); + put_absolute_path(fp, "favicon.png"); + SPUTF(fp, "\" />" "\n"); + + SPUTF(fp, "<link rel=\"alternate\" type=\"application/atom+xml\" title=\""); + put_xml(fp, name, strlen(name)); + SPUTF(fp, " Atom Feed\" href=\""); + put_absolute_path(fp, "atom.xml"); + SPUTF(fp, "\" />" "\n"); + + SPUTF(fp, "<link rel=\"alternate\" type=\"application/atom+xml\" title=\""); + put_xml(fp, name, strlen(name)); + SPUTF(fp, " Atom Feed (tags)\" href=\""); + put_absolute_path(fp, "tags.xml"); + SPUTF(fp, "\" />" "\n"); + + SPUTF(fp, "<link rel=\"stylesheet\" type=\"text/css\" href=\""); + fprintf(fp, STYLE_HREF); + SPUTF(fp, "\" />" "\n"); + SPUTF(fp, "</head>" "\n" "<body>" "\n"); + + fprintf(fp, HEADER); + + SPUTF(fp, "<table>" "<tr><td>"); + + SPUTF(fp, "<a href=\""); + fprintf(fp, LOGO_HREF); + SPUTF(fp, "\"><img alt=\"logo\" src=\""); + fprintf(fp, LOGO_SRC); + SPUTF(fp, "\"></a>"); + + SPUTF(fp, "</td><td>" "<h1>"); + put_xml(fp, stripped_name, strlen(stripped_name)); + SPUTF(fp, "</h1>" "<span class=\"desc\">"); + put_xml(fp, description, strlen(description)); + SPUTF(fp, "</span>" "</td></tr>"); + if (cloneurl[0]) { + SPUTF(fp, "<tr class=\"url\"><td></td><td>git clone <a href=\""); + put_xml(fp, cloneurl, strlen(cloneurl)); /* not percent-encoded */ + SPUTF(fp, "\">"); + put_xml(fp, cloneurl, strlen(cloneurl)); + SPUTF(fp, "</a></td></tr>"); + } + SPUTF(fp, "<tr><td></td><td>" "\n"); + + if (readme) { + SPUTF(fp, "<a href=\""); + put_absolute_path(fp, "files/"); + fprintf(fp, "%s.html", readme); + SPUTF(fp, "\">README</a> | "); + } + + SPUTF(fp, "<a href=\""); + put_absolute_path(fp, "files.html"); + SPUTF(fp, "\">Files</a> | "); + + SPUTF(fp, "<a href=\""); + put_absolute_path(fp, "log.html"); + SPUTF(fp, "\">Log</a> | "); + + SPUTF(fp, "<a href=\""); + put_absolute_path(fp, "refs.html"); + SPUTF(fp, "\">Refs</a>"); + + if (submodules) { + SPUTF(fp, " | <a href=\""); + put_absolute_path(fp, "files/"); + fprintf(fp, "%s.html", submodules); + SPUTF(fp, "\">Submodules</a>"); + } + + if (license) { + SPUTF(fp, " | <a href=\""); + put_absolute_path(fp, "files/"); + fprintf(fp, "%s.html", license); + SPUTF(fp, "\">LICENSE</a>"); + } + + SPUTF(fp, "</td></tr></table>" "\n"); + SPUTF(fp, "<hr/>" "\n"); + + SPUTF(fp, "<div id=\"content\">" "\n"); +} + +void +put_footer(FILE *fp) +{ + SPUTF(fp, "</div>" "\n"); + fprintf(fp, FOOTER); + SPUTF(fp, "</body>" "\n" "</html>" "\n"); +} + +size_t +put_file_html(FILE *fp, const git_blob *blob) +{ + size_t n = 0, i, len, prev; + const char *number_format = + "<a href=\"#l%zu\" class=\"line\" id=\"l%zu\">%7zu </a>"; + const char *s = git_blob_rawcontent(blob); + + len = git_blob_rawsize(blob); + SPUTF(fp, "<pre id=\"blob\">" "\n"); + + if (len > 0) { + for (i = 0, prev = 0; i < len; i++) { + if (s[i] != '\n') + continue; + n++; + fprintf(fp, number_format, n, n, n); + put_xml_line(fp, &s[prev], i - prev + 1); + SPUTF(fp, "\n"); + prev = i + 1; + } + /* trailing data */ + if ((len - prev) > 0) { + n++; + fprintf(fp, number_format, n, n, n); + put_xml_line(fp, &s[prev], len - prev); + } + } + + SPUTF(fp, "</pre>" "\n"); + + return n; +} + +void +print_commit(FILE *fp, struct CommitInfo *ci) +{ + SPUTF(fp, "<b>commit</b> <a href=\""); + put_absolute_path(fp, "commits/"); + fprintf(fp, "%s.html", ci->oid); + fprintf(fp, "\">%s</a>" "\n", ci->oid); + + if (ci->parentoid[0]) { + SPUTF(fp, "<b>parent</b> <a href=\""); + put_absolute_path(fp, "commits/"); + fprintf(fp, "%s.html", ci->parentoid); + fprintf(fp, "\">%s</a>" "\n", ci->parentoid); + } + + if (ci->author) { + SPUTF(fp, "<b>Author:</b> "); + put_xml(fp, ci->author->name, strlen(ci->author->name)); + SPUTF(fp, " &lt;<a href=\"mailto:"); + put_xml(fp, ci->author->email, strlen(ci->author->email)); /* not percent-encoded */ + SPUTF(fp, "\">"); + put_xml(fp, ci->author->email, strlen(ci->author->email)); + SPUTF(fp, "</a>&gt;\n<b>Date:</b> "); + printtime(fp, &(ci->author->when)); + CPUT(fp, '\n'); + } + if (ci->msg) { + CPUT(fp, '\n'); + put_xml(fp, ci->msg, strlen(ci->msg)); + CPUT(fp, '\n'); + } +} + +void +put_show_file(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; + + print_commit(fp, ci); + + if (!ci->deltas) + return; + + if ( + ci->file_count > 1000 || + ci->ndeltas > 1000 || + ci->add_count > 100000 || + ci->del_count > 100000 + ) { + fputs("Diff is too large, output suppressed.\n", fp); + return; + } + + /* diff stat */ + fputs("<b>Diffstat:</b>" "\n", fp); + fputs("<table id=\"diffstat\">", fp); + for (i = 0; i < ci->ndeltas; i++) { + delta = git_patch_get_delta(ci->deltas[i]->patch); + + switch (delta->status) { + case GIT_DELTA_ADDED: c = 'A'; break; + case GIT_DELTA_COPIED: c = 'C'; break; + case GIT_DELTA_DELETED: c = 'D'; break; + case GIT_DELTA_MODIFIED: c = 'M'; break; + case GIT_DELTA_RENAMED: c = 'R'; break; + case GIT_DELTA_TYPECHANGE: c = 'T'; break; + default: c = ' '; break; + } + if (c == ' ') + fprintf(fp, "<tr><td>%c", c); + else + fprintf(fp, "<tr><td class=\"%c\">%c", c, c); + + fprintf(fp, "</td><td><a href=\"#h%zu\">", i); + put_xml(fp, delta->old_file.path, strlen(delta->old_file.path)); + if (strcmp(delta->old_file.path, delta->new_file.path)) { + fputs(" -&gt; ", fp); + put_xml(fp, delta->new_file.path, strlen(delta->new_file.path)); + } + + add = ci->deltas[i]->add_count; + del = ci->deltas[i]->del_count; + changed = add + del; + total = sizeof(linestr) - 2; + if (changed > total) { + if (add) + add = ((float)total / changed * add) + 1; + if (del) + del = ((float)total / changed * del) + 1; + } + memset(&linestr, '+', add); + memset(&linestr[add], '-', del); + + fprintf( + fp, + "</a></td>" + // "<td> | </td>" + "<td class=\"num\">%zu</td><td><span class=\"i\">", + ci->deltas[i]->add_count + ci->deltas[i]->del_count + ); + fwrite(&linestr, 1, add, fp); + fputs("</span><span class=\"d\">", fp); + fwrite(&linestr[add], 1, del, fp); + fputs("</span></td></tr>\n", fp); + } + fprintf( + fp, + "</table></pre><pre>%zu file%s changed, %zu insertion%s(+), %zu deletion%s(-)\n", + ci->file_count, ci->file_count == 1 ? "" : "s", + ci->add_count, ci->add_count == 1 ? "" : "s", + ci->del_count, ci->del_count == 1 ? "" : "s" + ); + + fputs("<hr/>", fp); + + for (i = 0; i < ci->ndeltas; i++) { + patch = ci->deltas[i]->patch; + delta = git_patch_get_delta(patch); + + SPUTF(fp, "<b>diff --git a/"); + put_xml(fp, delta->old_file.path, strlen(delta->old_file.path)); + fprintf(fp, " b/<a id=\"h%zu\" href=\"#h%zu\">", i, i); + put_xml(fp, delta->new_file.path, strlen(delta->new_file.path)); + SPUTF(fp, "</a></b>" "\n"); + + /* check binary data */ + if (delta->flags & GIT_DIFF_FLAG_BINARY) { + SPUTF(fp, "Binary files differ." "\n"); + continue; + } + + nhunks = git_patch_num_hunks(patch); + for (j = 0; j < nhunks; j++) { + if (git_patch_get_hunk(&hunk, &nhunklines, patch, j)) + break; + + fprintf(fp, "<a href=\"#h%zu-%zu\" id=\"h%zu-%zu\" class=\"h\">", i, j, i, j); + put_xml(fp, hunk->header, hunk->header_len); + SPUTF(fp, "</a>"); + + for (k = 0; ; k++) { + if (git_patch_get_line_in_hunk(&line, patch, j, k)) + break; + if (line->old_lineno == -1) + fprintf( + fp, + "<a href=\"#h%zu-%zu-%zu\" id=\"h%zu-%zu-%zu\" class=\"i\">+", + i, j, k, i, j, k + ); + else if (line->new_lineno == -1) + fprintf( + fp, + "<a href=\"#h%zu-%zu-%zu\" id=\"h%zu-%zu-%zu\" class=\"d\">-", + i, j, k, i, j, k + ); + else + CPUT(fp, ' '); + put_xml_line(fp, line->content, line->content_len); + CPUT(fp, '\n'); + if (line->old_lineno == -1 || line->new_lineno == -1) + SPUTF(fp, "</a>"); + } + } + } +} + +void +put_log_line(FILE *fp, struct CommitInfo *ci) +{ + fputs("<tr><td>", fp); + if (ci->author) + printtimeshort(fp, &(ci->author->when)); + fputs("</td><td>", fp); + if (ci->summary) { + SPUTF(fp, "<a href=\""); + put_absolute_path(fp, "commits/"); + fprintf(fp, "%s.html", ci->oid); + SPUTF(fp, "\">"); + put_xml(fp, ci->summary, strlen(ci->summary)); + SPUTF(fp, "</a>"); + } + SPUTF(fp, "</td><td>"); + if (ci->author) + put_xml(fp, ci->author->name, strlen(ci->author->name)); + SPUTF(fp, "</td><td class=\"num\" align=\"right\">"); + fprintf(fp, "%zu", ci->file_count); + SPUTF(fp, "</td><td class=\"num\" align=\"right\">"); + fprintf(fp, "+%zu", ci->add_count); + SPUTF(fp, "</td><td class=\"num\" align=\"right\">"); + fprintf(fp, "-%zu", ci->del_count); + SPUTF(fp, "</td></tr>" "\n"); +} + +int +put_log(FILE *fp, const git_oid *oid) +{ + struct CommitInfo *ci; + git_revwalk *w = NULL; + git_oid id; + char path[PATH_MAX], oidstr[GIT_OID_HEXSZ + 1]; + FILE *fpfile; + size_t remcommits = 0; + int r; + + git_revwalk_new(&w, repo); + git_revwalk_push(w, oid); + + while (!git_revwalk_next(&id, w)) { + if (cachefile && !memcmp(&id, &lastoid, sizeof(id))) + break; + + git_oid_tostr(oidstr, sizeof(oidstr), &id); + r = snprintf(path, sizeof(path), "commits/%s.html", oidstr); + if (r < 0 || (size_t)r >= sizeof(path)) + errx(1, "path truncated: 'commits/%s.html'", oidstr); + r = access(path, F_OK); + + /* optimization: if there are no log lines to write and + the commit file already exists: skip the diffstat */ + if (!nlogcommits) { + remcommits++; + if (!r) + continue; + } + + if (!(ci = commit_info_get_by_oid(&id))) + break; + /* diffstat: for stagit HTML required for the log.html line */ + if (commit_info_get_stats(ci) == -1) + goto err; + + if (nlogcommits != 0) { + put_log_line(fp, ci); + if (nlogcommits > 0) + nlogcommits--; + } + + if (cachefile) + put_log_line(wcachefp, ci); + + /* check if file exists if so skip it */ + if (r) { + fpfile = efopen(path, "w"); + put_header(fpfile, ci->summary); + + fputs("<pre>", fpfile); + put_show_file(fpfile, ci); + fputs("</pre>\n", fpfile); + + put_footer(fpfile); + check_file_error(fpfile, path, 'w'); + fclose(fpfile); + } +err: + commit_info_free(ci); + } + git_revwalk_free(w); + + if (nlogcommits == 0 && remcommits != 0) + fprintf( + fp, + "<tr><td></td><td colspan=\"5\">" + "%zu more commits remaining, fetch the repository" + "</td></tr>" "\n", + remcommits + ); + + return 0; +} + +void +put_log_cached(FILE *fp, const git_oid *head) +{ + char tmppath[64] = "cache.XXXXXXXXXXXX"; + char buf[BUFSIZ]; + mode_t mask; + int fd, n; + + /* 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); + + put_log(fp, head); + + if (rcachefp) { + /* append previous log to log.html and the new cache */ + while (!feof(rcachefp)) { + n = fread(buf, 1, sizeof(buf), rcachefp); + if (ferror(rcachefp)) + break; + if ( + fwrite(buf, 1, n, fp) != n || + fwrite(buf, 1, n, wcachefp) != n + ) + break; + } + check_file_error(rcachefp, cachefile, 'r'); + fclose(rcachefp); + } + check_file_error(wcachefp, tmppath, 'w'); + fclose(wcachefp); + + // TODO previously, this would check that the log page was written + // successfully before renaming the cachefile... I'm probably going + // to blow this whole caching mess away so as long as it runs for + // now, it shouldn't matter. + + 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); +} + +void +put_log_html(FILE *fp, const git_oid *head) +{ + put_header(fp, "Log"); + fputs( + "<table id=\"log\"><thead>" "\n" + "<tr>" + "<td><b>Date</b></td>" + "<td><b>Commit message</b></td>" + "<td><b>Author</b></td>" + "<td class=\"num\" align=\"right\"><b>Files</b></td>" + "<td class=\"num\" align=\"right\"><b>+</b></td>" + "<td class=\"num\" align=\"right\"><b>-</b></td>" + "</tr>" "\n" + "</thead><tbody>" "\n", + fp + ); + + if (head && !cachefile) put_log(fp, head); + if (head && cachefile) put_log_cached(fp, head); + + fputs("</tbody></table>", fp); + put_footer(fp); +} + +void +print_commit_atom(FILE *fp, struct CommitInfo *ci, const char *tag) +{ + fputs("<entry>\n", fp); + + fprintf(fp, "<id>%s</id>\n", ci->oid); + if (ci->author) { + SPUTF(fp, "<published>"); + printtimez(fp, &(ci->author->when)); + SPUTF(fp, "</published>\n"); + } + if (ci->committer) { + SPUTF(fp, "<updated>"); + printtimez(fp, &(ci->committer->when)); + SPUTF(fp, "</updated>\n"); + } + if (ci->summary) { + SPUTF(fp, "<title>"); + if (tag && tag[0]) { + SPUTF(fp, "["); + put_xml(fp, tag, strlen(tag)); + SPUTF(fp, "] "); + } + put_xml(fp, ci->summary, strlen(ci->summary)); + SPUTF(fp, "</title>\n"); + } + SPUTF(fp, "<link rel=\"alternate\" type=\"text/html\" href=\""); + put_absolute_path(fp, "commits/"); + fprintf(fp, "%s.html\" />" "\n", ci->oid); + + if (ci->author) { + SPUTF(fp, "<author>" "\n" "<name>"); + put_xml(fp, ci->author->name, strlen(ci->author->name)); + SPUTF(fp, "</name>" "\n" "<email>"); + put_xml(fp, ci->author->email, strlen(ci->author->email)); + SPUTF(fp, "</email>" "\n" "</author>" "\n"); + } + + SPUTF(fp, "<content>"); + fprintf(fp, "commit %s" "\n", ci->oid); + if (ci->parentoid[0]) + fprintf(fp, "parent %s" "\n", ci->parentoid); + if (ci->author) { + SPUTF(fp, "Author: "); + put_xml(fp, ci->author->name, strlen(ci->author->name)); + SPUTF(fp, " &lt;"); + put_xml(fp, ci->author->email, strlen(ci->author->email)); + SPUTF(fp, "&gt;" "\n" "Date: "); + printtime(fp, &(ci->author->when)); + CPUT(fp, '\n'); + } + if (ci->msg) { + CPUT(fp, '\n'); + put_xml(fp, ci->msg, strlen(ci->msg)); + } + SPUTF(fp, "\n" "</content>" "\n" "</entry>" "\n"); +} + +int +write_atom(FILE *fp, int all) +{ + struct ReferenceInfo *ris = NULL; + size_t refcount = 0; + struct CommitInfo *ci; + git_revwalk *w = NULL; + git_oid id; + size_t i, m = 100; /* last 'm' commits */ + + SPUTF( + fp, + "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" "\n" + "<feed xmlns=\"http://www.w3.org/2005/Atom\">" "\n" "<title>" + ); + put_xml(fp, stripped_name, strlen(stripped_name)); + SPUTF(fp, ", branch HEAD</title>" "\n" "<subtitle>"); + put_xml(fp, description, strlen(description)); + SPUTF(fp, "</subtitle>" "\n"); + + /* all commits or only tags? */ + if (all) { + git_revwalk_new(&w, repo); + git_revwalk_push_head(w); + for (i = 0; i < m && !git_revwalk_next(&id, w); i++) { + if (!(ci = commit_info_get_by_oid(&id))) + break; + print_commit_atom(fp, ci, ""); + commit_info_free(ci); + } + git_revwalk_free(w); + } else if (get_refs(&ris, &refcount) != -1) { + /* references: tags */ + for (i = 0; i < refcount; i++) { + if (git_reference_is_tag(ris[i].ref)) + print_commit_atom( + fp, ris[i].ci, + git_reference_shorthand(ris[i].ref) + ); + + commit_info_free(ris[i].ci); + git_reference_free(ris[i].ref); + } + free(ris); + } + + fputs("</feed>\n", fp); + + return 0; +} + +void +write_binary_file(const char *filename, git_blob *blob) { + char dest[PATH_MAX] = ""; + const char *raw = git_blob_rawcontent(blob); + int size = git_blob_rawsize(blob); + + /* set dest */ + join_path(dest, PATH_MAX, "files", filename); + + /* write the file! */ + FILE *fp = efopen(dest, "w"); + fwrite(raw, size, 1, fp); + check_file_error(fp, dest, 'w'); + fclose(fp); +} + +void +put_size(FILE *fp, size_t size, int fixed) +{ + int order = 0; + int decimal; + + if (size == 0) { + fprintf(fp, " - "); + return; + } + + while (size > 1000) { + order++; + decimal = (size / 100) % 10; + size /= 1000; + } + + if (size < 10 && order > 0) fprintf(fp, "%1zu.%1d", size, decimal); + else fprintf(fp, fixed ? "%3zu" : "%zu", size); + + if (order != 0 || fixed) fprintf(fp, "%c", orders[order]); +} + +void +put_file_title(FILE *fp, const char *name, int is_dir) +{ + int i = 0; + int len = 0; + + /* project root */ + if (name[0] == '\0') + return; + + if (!dir_pages) { + fprintf(fp, "<p>%s</p>", name); + return; + } + + if (backlinks && !is_dir) { + for (i = 0; name[i] != '\0'; i++) + if (name[i] == '/') len = i; + + if (len != 0) { + SPUTF(fp, "<p><a href=\""); + put_absolute_path(fp, "files/"); + for (i = 0; i < len; i++) put_percent_char(fp, name[i]); + SPUTF(fp, ".html\">"); + for (i = 0; i < len; i++) put_percent_char(fp, name[i]); + fprintf(fp, "</a>%s</p>", name + len); + if (!is_dir) SPUTF(fp, "<hr>"); + return; + } + } + + + fprintf(fp, "<p>%s</p>", name); + if (!is_dir) SPUTF(fp, "<hr>"); +} + +struct Weight +write_blob( + git_object *obj, + const char *filename, + const char *entrypath, + const char *name +) { + FILE *fp; + const char *file_url; + int is_binary; + struct Weight ret; + ret.lines = 0; + ret.bytes = git_blob_rawsize((git_blob *)obj); + + is_binary = git_blob_is_binary((git_blob *)obj); + file_url = is_binary ? name : ""; + + fp = efopen(filename, "w"); + put_header(fp, name); + put_file_title(fp, entrypath, 0); + + if (is_binary) { + SPUTF(fp, "<p>Binary file: <a href=\""); + put_absolute_path(fp, ""); + fprintf(fp, "%s\">raw</a></p>" "\n", file_url); + SPUTF(fp, "<object data=\""); + put_absolute_path(fp, ""); + fprintf(fp, "%s\">", file_url); + SPUTF(fp, "no preview available.</object></p>" "\n"); + + write_binary_file(entrypath, (git_blob *) obj); + } else { + ret.lines = put_file_html(fp, (git_blob *) obj); + } + + put_footer(fp); + check_file_error(fp, filename, 'w'); + fclose(fp); + + return ret; +} + +char +get_filemode_type(git_filemode_t m) +{ + if (S_ISREG(m)) return '-'; + if (S_ISBLK(m)) return 'b'; + if (S_ISCHR(m)) return 'c'; + if (S_ISDIR(m)) return 'd'; + if (S_ISFIFO(m)) return 'p'; + if (S_ISLNK(m)) return 'l'; + if (S_ISSOCK(m)) return 's'; + return '?'; +} + +const char * +get_filemode(const git_tree_entry *entry) +{ + git_filemode_t mode; + static char ret[11]; + + /* used for backlink: .. */ + if (entry == NULL) return "d---------"; + + mode = git_tree_entry_filemode(entry); + + memset(ret, '-', sizeof(ret) - 1); + ret[0] = get_filemode_type(mode); + if (mode & S_IRUSR) ret[1] = 'r'; + if (mode & S_IWUSR) ret[2] = 'w'; + if (mode & S_IXUSR) ret[3] = 'x'; + if (mode & S_ISUID) ret[3] = (ret[3] == 'x') ? 's' : 'S'; + if (mode & S_IRGRP) ret[4] = 'r'; + if (mode & S_IWGRP) ret[5] = 'w'; + if (mode & S_IXGRP) ret[6] = 'x'; + if (mode & S_ISGID) ret[6] = (ret[6] == 'x') ? 's' : 'S'; + if (mode & S_IROTH) ret[7] = 'r'; + if (mode & S_IWOTH) ret[8] = 'w'; + if (mode & S_IXOTH) ret[9] = 'x'; + if (mode & S_ISVTX) ret[9] = (ret[9] == 'x') ? 't' : 'T'; + ret[10] = '\0'; + + return ret; +} + +void +put_filetree_file_line( + FILE *fp, + git_object *obj, + const git_tree_entry *entry, + char *filename, + char *entrypath, + const char *name, + struct Weight weight +) { + SPUTF(fp, "<tr><td>"); + SPUTF(fp, get_filemode(entry)); + SPUTF(fp, "</td><td><a href=\""); + put_absolute_path(fp, ""); + put_percent_encoded(fp, filename, strlen(filename)); + SPUTF(fp, "\">"); + if (dir_pages) put_xml(fp, name, strlen(name)); + else put_xml(fp, entrypath, strlen(entrypath)); + SPUTF(fp, "</a></td><td class=\"num\" align=\"right\">"); + put_size(fp, weight.lines, 1); + SPUTF(fp, "</td><td class=\"num\" align=\"right\">"); + put_size(fp, weight.bytes, 1); + SPUTF(fp, "</td></tr>\n"); +} + +void +put_filetree_dir_line( + FILE *fp, + const git_tree_entry *entry, + char *destination, + const char *title, + size_t lines, + size_t bytes +) { + SPUTF(fp, "<tr><td>"); + SPUTF(fp, get_filemode(entry)); + SPUTF(fp, "</td><td><a href=\""); + put_absolute_path(fp, ""); + put_percent_encoded(fp, destination, strlen(destination)); + SPUTF(fp, "\">"); + put_xml(fp, title, strlen(title)); + SPUTF(fp, "</a></td><td class=\"num\" align=\"right\">"); + put_size(fp, lines, 1); + SPUTF(fp, "</td><td class=\"num\" align=\"right\">"); + put_size(fp, bytes, 1); + SPUTF(fp, "</td></tr>\n"); +} + +void put_filetree_backlink_line(FILE *fp, const char *path) { + char previous_path[PATH_MAX]; + char previous_filename[PATH_MAX]; + int i, r, last_slash = 0; + + /* figure out previous directory */ + // TODO how to do this more cleanly... + for (i = 0; path[i] != '\0'; i++) { + previous_path[i] = path[i]; + if (path[i] == '/') last_slash = i; + } + previous_path[last_slash] = '\0'; + + if (last_slash == 0) { + put_filetree_dir_line(fp, NULL, "files.html", "..", 0, 0); + } else { + r = snprintf(previous_filename, sizeof(previous_filename), "files/%s.html", previous_path); + if (r < 0 || (size_t) r >= sizeof(previous_filename)) + errx(1, "path truncated: 'files/%s.html'", previous_path); + put_filetree_dir_line(fp, NULL, previous_filename, "..", 0, 0); + } +} + +/* need this here to allow recursion */ +struct Weight write_filetree( + const char *path, + const char *filename, + git_tree *tree +); +struct Weight put_file_list(FILE *fp, git_tree *tree, const char *path); + +struct Weight +put_entry_obj(FILE *fp, git_object *obj, const git_tree_entry *entry, char *entrypath, const char *name) +{ + struct Weight ret; + git_object_t obj_type = git_object_type(obj); + char filename[PATH_MAX]; + int r; + + /* filename = f("files/%s.html", entrypath) */ + r = snprintf(filename, sizeof(filename), "files/%s.html", entrypath); + if (r < 0 || (size_t) r >= sizeof(filename)) + errx(1, "path truncated: 'files/%s.html'", entrypath); + + switch (obj_type) { + case GIT_OBJ_TREE: + if (dir_pages) { + /* create dir page */ + ret = write_filetree(entrypath, filename, (git_tree *) obj); + + /* add link to that dir page */ + put_filetree_dir_line(fp, entry, filename, name, ret.lines, ret.bytes); + } else { + /* list files on this page */ + ret = put_file_list(fp, (git_tree *) obj, entrypath); + } + break; + case GIT_OBJ_BLOB: + ret = write_blob(obj, filename, entrypath, name); + put_filetree_file_line(fp, obj, entry, filename, entrypath, name, ret); + break; + default: break; + } + + return ret; +} + +void +put_submodule_obj(FILE *fp, const git_tree_entry *entry, const char *name) +{ + char oid[8]; + + SPUTF(fp, "<tr>" "<td>m---------</td>" "<td><a href=\""); + put_absolute_path(fp, "files/.gitmodules.html"); + SPUTF(fp, "\">"); + put_xml(fp, name, strlen(name)); + SPUTF(fp, "</a> @ "); + git_oid_tostr(oid, sizeof(oid), git_tree_entry_id(entry)); + put_xml(fp, oid, strlen(oid)); + SPUTF(fp, "</td>" "<td class=\"num\" align=\"right\"></td>" "\n"); + SPUTF(fp, "<td class=\"num\" align=\"right\"></td>" "</tr>" "\n"); +} + +void +combine_paths(char *ret, const char *path, const char *name) +{ + int r; + + if (path[0] == '\0') r = snprintf(ret, PATH_MAX, "%s", name); + else r = snprintf(ret, PATH_MAX, "%s/%s", path, name); + + if (r < 0 || (size_t) r >= PATH_MAX) + errx(1, "path truncated...?"); +} + +struct Weight +put_file_list(FILE *fp, git_tree *tree, const char *path) +{ + struct Weight ret; + size_t count; + size_t i; + + ret.lines = ret.bytes = 0; + + count = git_tree_entrycount(tree); + for (i = 0; i < count; i++) { + const git_tree_entry *entry = NULL; + const char *name; + char entrypath[PATH_MAX]; + git_object *obj = NULL; + + if ( + !(entry = git_tree_entry_byindex(tree, i)) || + !(name = git_tree_entry_name(entry)) + ) + return ret; + + if (name[0] == '.' && opt.hide_hidden) + continue; + + /* entrypath = path + name */ + combine_paths(entrypath, path, name); + + if (!git_tree_entry_to_object(&obj, repo, entry)) { + struct Weight weight = + put_entry_obj(fp, obj, entry, entrypath, name); + ret.bytes += weight.bytes; + ret.lines += weight.lines; + git_object_free(obj); + continue; + } + + if (git_tree_entry_type(entry) == GIT_OBJ_COMMIT) { + put_submodule_obj(fp, entry, name); + continue; + } + } + + return ret; +} + +struct Weight +write_filetree(const char *path, const char *filename, git_tree *tree) +{ + FILE *fp; + struct Weight ret; + + fp = efopen(filename, "w"); + put_header(fp, "Files"); + + put_file_title(fp, path, 1); + + SPUTF( + fp, + "<table id=\"files\"><thead>" "\n" "<tr>" + "<td><b>Mode</b></td><td><b>Name</b></td>" + "<td class=\"num\" align=\"right\"><b> L </b></td>" + "<td class=\"num\" align=\"right\"><b> B </b></td>" + "</tr>" "\n" "</thead><tbody>" "\n" + ); + + if (tree != NULL) { + if (backlinks && path[0] != '\0') + put_filetree_backlink_line(fp, path); + + ret = put_file_list(fp, tree, path); + } + + SPUTF(fp, "</tbody></table>"); + + // TODO how can this get printed before the contents... + SPUTF(fp, "<p>Totals: "); + put_size(fp, ret.lines, 0); + SPUTF(fp, "L "); + put_size(fp, ret.bytes, 0); + SPUTF(fp, "B</p>"); + + put_footer(fp); + check_file_error(fp, filename, 'w'); + fclose(fp); + + return ret; +} + +void +walk_git_tree(const git_oid *id) +{ + git_tree *tree = NULL; + git_commit *commit = NULL; + int has_files; + + if (!id) return; + + has_files = !git_commit_lookup(&commit, repo, id) && + !git_commit_tree(&tree, commit); + + write_filetree("", "files.html", has_files ? tree : NULL); + + git_commit_free(commit); + git_tree_free(tree); +} + +int +write_refs(FILE *fp) +{ + struct ReferenceInfo *ris = NULL; + struct CommitInfo *ci; + size_t count, i, j, refcount; + const char *titles[] = { "Branches", "Tags" }; + const char *ids[] = { "branches", "tags" }; + const char *s; + + if (get_refs(&ris, &refcount) == -1) + return -1; + + for (i = 0, j = 0, count = 0; i < refcount; i++) { + if (j == 0 && git_reference_is_tag(ris[i].ref)) { + if (count) + fputs("</tbody></table><br/>\n", fp); + count = 0; + j = 1; + } + + /* print header if it has an entry (first). */ + if (++count == 1) + fprintf( + fp, + "<h2>%s</h2><table id=\"%s\">" + "<thead>\n<tr><td><b>Name</b></td>" + "<td><b>Last commit date</b></td>" + "<td><b>Author</b></td>\n</tr>\n" + "</thead><tbody>\n", + titles[j], + ids[j] + ); + + ci = ris[i].ci; + s = git_reference_shorthand(ris[i].ref); + + fputs("<tr><td>", fp); + put_xml(fp, s, strlen(s)); + fputs("</td><td>", fp); + if (ci->author) + printtimeshort(fp, &(ci->author->when)); + fputs("</td><td>", fp); + if (ci->author) + put_xml(fp, ci->author->name, strlen(ci->author->name)); + fputs("</td></tr>\n", fp); + } + + if (count) + fputs("</tbody></table><br/>\n", fp); + + for (i = 0; i < refcount; i++) { + commit_info_free(ris[i].ci); + git_reference_free(ris[i].ref); + } + free(ris); + + return 0; +} + +void +write_log_page(const git_oid *head) +{ + const char *filename = "log.html"; + FILE *fp = efopen(filename, "w"); + put_log_html(fp, head); + check_file_error(fp, filename, 'w'); + fclose(fp); +} + +void +write_refs_page() +{ + const char *filename = "refs.html"; + FILE *fp = efopen(filename, "w"); + put_header(fp, "Refs"); + write_refs(fp); + put_footer(fp); + check_file_error(fp, filename, 'w'); + fclose(fp); +} + +void +write_feed() +{ + const char *filename = "atom.xml"; + FILE *fp = efopen(filename, "w"); + write_atom(fp, 1); + check_file_error(fp, filename, 'w'); + fclose(fp); +} + +void +write_releases_feed() +{ + const char *filename = "tags.xml"; + FILE *fp = efopen(filename, "w"); + write_atom(fp, 0); + check_file_error(fp, filename, 'w'); + fclose(fp); +} + +void +usage(char *argv0) +{ + fprintf( + stderr, + "usage: %s [-c cachefile | -l commits] " + "repodir" "\n", + argv0 + ); + exit(1); +} + +int +main(int argc, const char *argv[]) +{ + const git_oid *head = NULL; + FILE *fpread; + char path[PATH_MAX], repodirabs[PATH_MAX + 1], *p; + git_object *obj = NULL; + int i; + + int result = parse_opts(&opt, argc, argv); + if (result == OPT_FAIL) { + PRINT_USAGE(argv[0]); + return 1; + } + + printf( + "hide?: %d" "\n" + "#repos: %d" "\n" + "indexfile: %s" "\n" + "cachedir: %s" "\n" + "outdir: %s" "\n" + "\n", + opt.hide_hidden, + opt.repo_count, + opt.index_file, + opt.cache_dir, + opt.out_dir + ); + + puts("Repos:" "\n"); + for(int a = 0; a < opt.repo_count; a++) + printf("%d: '%s'" "\n", a, opt.repo_dirs[a]); + + // TODO iterate over repo_dirs + repodir = opt.repo_dirs[0]; + + /* parse args */ + if (!realpath(repodir, repodirabs)) + err(1, "realpath"); + + /* do not search outside the git repository: + GIT_CONFIG_LEVEL_APP is the highest level currently */ + git_libgit2_init(); + for (i = 1; i <= GIT_CONFIG_LEVEL_APP; i++) + git_libgit2_opts(GIT_OPT_SET_SEARCH_PATH, i, ""); + /* do not require the git repository to be owned by the current user */ + git_libgit2_opts(GIT_OPT_SET_OWNER_VALIDATION, 0); + +#ifdef __OpenBSD__ + if (unveil(repodir, "r") == -1) + err(1, "unveil: %s", repodir); + if (unveil(".", "rwc") == -1) + err(1, "unveil: ."); + if (cachefile && unveil(cachefile, "rwc") == -1) + err(1, "unveil: %s", cachefile); + + if (cachefile) { + if (pledge("stdio rpath wpath cpath fattr", NULL) == -1) + err(1, "pledge"); + } else { + if (pledge("stdio rpath wpath cpath", NULL) == -1) + err(1, "pledge"); + } +#endif + + if ( + git_repository_open_ext( + &repo, + repodir, + GIT_REPOSITORY_OPEN_NO_SEARCH, + NULL + ) < 0 + ) { + fprintf(stderr, "%s: cannot open repository\n", argv[0]); + return 1; + } + + /* find HEAD */ + if (!git_revparse_single(&obj, repo, "HEAD")) + head = git_object_id(obj); + git_object_free(obj); + + /* use directory name as name */ + if ((name = strrchr(repodirabs, '/'))) name++; + else name = ""; + + /* strip .git suffix */ + if (!(stripped_name = strdup(name))) + err(1, "strdup"); + if ((p = strrchr(stripped_name, '.'))) + if (!strcmp(p, ".git")) + *p = '\0'; + if (stripped_name[0] == '\0') + stripped_name = "untitled"; + + /* read description or .git/description */ + join_path(path, sizeof(path), repodir, "description"); + if (!(fpread = fopen(path, "r"))) { + join_path(path, sizeof(path), repodir, ".git/description"); + fpread = fopen(path, "r"); + } + if (fpread) { + if (!fgets(description, sizeof(description), fpread)) + description[0] = '\0'; + check_file_error(fpread, path, 'r'); + fclose(fpread); + } + + /* read url or .git/url */ + join_path(path, sizeof(path), repodir, "url"); + if (!(fpread = fopen(path, "r"))) { + join_path(path, sizeof(path), repodir, ".git/url"); + fpread = fopen(path, "r"); + } + if (fpread) { + if (!fgets(cloneurl, sizeof(cloneurl), fpread)) + cloneurl[0] = '\0'; + check_file_error(fpread, path, 'r'); + fclose(fpread); + cloneurl[strcspn(cloneurl, "\n")] = '\0'; + } + + find_license(); + find_readme(); + + if ( + !git_revparse_single(&obj, repo, "HEAD:.gitmodules") && + git_object_type(obj) == GIT_OBJ_BLOB + ) + submodules = ".gitmodules"; + git_object_free(obj); + + /* write pages! */ + write_log_page(head); + walk_git_tree(head); + write_refs_page(); + write_feed(); + write_releases_feed(); + + /* cleanup */ + git_repository_free(repo); + git_libgit2_shutdown(); + + return 0; +} diff --git a/opt.c b/opt.c @@ -0,0 +1,62 @@ + +#include <stdlib.h> +#include <stdio.h> +#include <limits.h> + +#include "opt.h" + + +int +parse_opt(Opt *opt, const char *cur, const char *next) +{ + int index; + char flag; + + if (cur[0] != '-') return OPT_LAST; + for (index = 1;;) { + flag = cur[index]; + if (flag == '\0') break; + + switch (flag) { + case 'h': opt->hide_hidden = 1; break; + case 'i': opt->index_file = next; return OPT_SKIP; + case 'o': opt->out_dir = next; return OPT_SKIP; + case 'c': opt->cache_dir = next; return OPT_SKIP; + case 'b': opt->base_url = next; return OPT_SKIP; + default: ERROR("Unrecognized flag: -%c." "\n\n", flag); + } + + index++; + } + + return OPT_GOOD; +} + +int +parse_opts(Opt *opt, int argc, const char * argv[]) +{ + const char * cur; + const char * next; + int index; + + if (argc < 2) ERROR(""); + + for (index = 1;;) { + cur = argv[index]; + printf("Parsing flag group: %s" "\n", cur); + next = (index+1 < argc-1) ? argv[index+1] : NULL; + switch (parse_opt(opt, cur, next)) { + case OPT_SKIP: index++; break; + case OPT_FAIL: return OPT_FAIL; + case OPT_LAST: + opt->repo_dirs = argv + index; + opt->repo_count = argc - index; + return OPT_GOOD; + } + index++; + if(index >= argc) break; + } + + ERROR("Must specify at least one repository." "\n\n"); +} + diff --git a/opt.h b/opt.h @@ -0,0 +1,34 @@ +#define FLAG_NONE 0 +#define OPT_GOOD 0 +#define OPT_FAIL 1 +#define OPT_SKIP 2 +#define OPT_LAST 3 + +#define ERROR(...) { fprintf(stderr, __VA_ARGS__); return OPT_FAIL; } + +#define USAGE \ +"Usage: %s [-h] [-o OUT] [-b BASE] [-i INDEX] [-c CACHE] REPO [REPO2 [...]]" "\n" \ +" -h: omit files and directories beginning with '.'" "\n" \ +" -o: write output into dir OUT (default is pwd)" "\n" \ +" -b: use base url BASE when creating links" "\n" \ +" -i: generate index file at OUT/INDEX" "\n" \ +" -c: utilize dir CACHE for caching" "\n" \ +"\n" + +#define PRINT_USAGE(argv0) fprintf(stderr, USAGE, argv0); + + +typedef struct _ { + int hide_hidden; + + int repo_count; + const char **repo_dirs; + + const char *cache_dir; + const char *out_dir; + const char *index_file; + + const char *base_url; +} Opt; + +int parse_opts(Opt *opt, int argc, const char *argv[]); diff --git a/stagit-index.1 b/stagit-index.1 @@ -1,47 +0,0 @@ -.Dd August 2, 2021 -.Dt STAGIT-INDEX 1 -.Os -.Sh NAME -.Nm stagit-index -.Nd static git index page generator -.Sh SYNOPSIS -.Nm -.Op Ar repodir... -.Sh DESCRIPTION -.Nm -will create an index HTML page for the repositories specified and writes -the HTML data to stdout. -The repos in the index are in the same order as the arguments -.Ar repodir -specified. -.Pp -The basename of the directory is used as the repository name. -The suffix ".git" is removed from the basename, this suffix is commonly used -for "bare" repos. -.Pp -The content of the follow files specifies the meta data for each repository: -.Bl -tag -width Ds -.It .git/description or description (bare repos). -description -.It .git/owner or owner (bare repo). -owner of repository -.El -.Pp -For changing the style of the page you can use the following files: -.Bl -tag -width Ds -.It favicon.png -favicon image. -.It logo.png -32x32 logo. -.It style.css -CSS stylesheet. -.El -.Sh EXAMPLES -.Bd -literal -cd htmlroot -stagit-index path/to/gitrepo1 path/to/gitrepo2 > index.html -.Ed -.Sh SEE ALSO -.Xr stagit 1 -.Sh AUTHORS -.An Hiltjo Posthuma Aq Mt hiltjo@codemadness.org diff --git a/stagit-index.c b/stagit-index.c @@ -16,82 +16,6 @@ static char description[255] = "Repositories"; static char *name = ""; static char owner[255]; -/* Handle read or write errors for a FILE * stream */ -void -checkfileerror(FILE *fp, const char *name, int mode) -{ - if (mode == 'r' && ferror(fp)) - errx(1, "read error: %s", name); - else if (mode == 'w' && (fflush(fp) || ferror(fp))) - errx(1, "write error: %s", name); -} - -void -joinpath(char *buf, size_t bufsiz, const char *path, const char *path2) -{ - int r; - - r = snprintf(buf, bufsiz, "%s%s%s", - path, path[0] && path[strlen(path) - 1] != '/' ? "/" : "", path2); - if (r < 0 || (size_t)r >= bufsiz) - errx(1, "path truncated: '%s%s%s'", - path, path[0] && path[strlen(path) - 1] != '/' ? "/" : "", path2); -} - -/* Percent-encode, see RFC3986 section 2.1. */ -void -percentencode(FILE *fp, const char *s, size_t len) -{ - static char tab[] = "0123456789ABCDEF"; - unsigned char uc; - size_t i; - - for (i = 0; *s && i < len; s++, i++) { - uc = *s; - /* NOTE: do not encode '/' for paths or ",-." */ - if (uc < ',' || uc >= 127 || (uc >= ':' && uc <= '@') || - uc == '[' || uc == ']') { - putc('%', fp); - putc(tab[(uc >> 4) & 0x0f], fp); - putc(tab[uc & 0x0f], fp); - } else { - putc(uc, fp); - } - } -} - -/* Escape characters below as HTML 2.0 / XML 1.0. */ -void -xmlencode(FILE *fp, const char *s, size_t len) -{ - size_t i; - - for (i = 0; *s && i < len; s++, i++) { - switch(*s) { - case '<': fputs("&lt;", fp); break; - case '>': fputs("&gt;", fp); break; - case '\'': fputs("&#39;" , fp); break; - case '&': fputs("&amp;", fp); break; - case '"': fputs("&quot;", fp); break; - default: putc(*s, fp); - } - } -} - -void -printtimeshort(FILE *fp, const git_time *intime) -{ - struct tm *intm; - time_t t; - char out[32]; - - t = (time_t)intime->time; - if (!(intm = gmtime(&t))) - return; - strftime(out, sizeof(out), "%Y-%m-%d %H:%M", intm); - fputs(out, fp); -} - void writeheader(FILE *fp) { @@ -118,7 +42,7 @@ writeheader(FILE *fp) void writefooter(FILE *fp) { - fputs("</tbody>\n</table>\n</div>\n</body>\n</html>\n", fp); + fputs("</tbody>" "\n" "</table>" "\n" "</div>" "\n" "</body>" "\n" "</html>" "\n", fp); } int diff --git a/stagit.c b/stagit.c @@ -1,1853 +0,0 @@ -#include <sys/stat.h> -#include <sys/types.h> - -#include <err.h> -#include <errno.h> -#include <libgen.h> -#include <limits.h> -#include <stdint.h> -#include <stdio.h> -#include <stdlib.h> -#include <string.h> -#include <time.h> -#include <unistd.h> - -#include <git2.h> - -#include "compat.h" - -#define CPUT(f, c) putc((c), (f)) -#define SPUTF(f, s) fputs((s), (f)) -#define LENGTH(s) (sizeof(s)/sizeof(*s)) - -struct DeltaInfo { - git_patch *patch; - - size_t add_count; - size_t del_count; -}; - -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 add_count; - size_t del_count; - size_t file_count; - - struct DeltaInfo **deltas; - size_t ndeltas; -}; - -/* reference and associated data for sorting */ -struct ReferenceInfo { - struct git_reference *ref; - struct CommitInfo *ci; -}; - -struct Weight { - size_t bytes; - size_t lines; -}; - -const char orders[] = { ' ', 'k', 'M', 'G', 'T', 'P', 'E' }; - -/* GLOBALS */ -// TODO untangle these, pass them around instead... - -static git_repository *repo; - -/* flags + arguments */ -static const char *baseurl = ""; /* base URL to make absolute RSS/Atom URI */ -static const char *repodir; - -static char *name = ""; -static char *stripped_name = ""; -static char description[255]; -static char cloneurl[1024]; -static char *submodules; -static char *license_files[] = { - "HEAD:LICENSE", "HEAD:LICENSE.md", "HEAD:LICENSE.txt", "HEAD:COPYING" -}; -static char *license; -static char *readme_files[] = { "HEAD:README", "HEAD:README.md" }; -static char *readme; -static long long nlogcommits = -1; /* -1 indicates not used */ - -/* cache */ -static git_oid lastoid; -static char lastoidstr[GIT_OID_HEXSZ + 2]; /* id + newline + NUL byte */ -static FILE *rcachefp, *wcachefp; -static const char *cachefile; - -/* Handle read or write errors for a FILE * stream */ -void -check_file_error(FILE *fp, const char *name, int mode) -{ - if (mode == 'r' && ferror(fp)) - errx(1, "read error: %s", name); - else if (mode == 'w' && (fflush(fp) || ferror(fp))) - errx(1, "write error: %s", name); -} - -void -join_path(char *buf, size_t bufsiz, const char *path, const char *path2) -{ - int r; - char * separator = ""; - if(path[0] && path[strlen(path) - 1] != '/') - separator = "/"; - - r = snprintf(buf, bufsiz, "%s%s%s", path, separator, path2); - if (r < 0 || (size_t)r >= bufsiz) - errx( - 1, - "path truncated: '%s%s%s'", - path, - separator, - path2 - ); -} - -void -find_license() -{ - git_object *obj = NULL; - int i; - for (i = 0; i < LENGTH(license_files) && !license; i++) { - if ( - !git_revparse_single(&obj, repo, license_files[i]) && - git_object_type(obj) == GIT_OBJ_BLOB - ) - license = license_files[i] + strlen("HEAD:"); - git_object_free(obj); - } -} - -void -find_readme() -{ - git_object *obj = NULL; - int i; - for (i = 0; i < LENGTH(readme_files) && !readme; i++) { - if ( - !git_revparse_single(&obj, repo, readme_files[i]) && - git_object_type(obj) == GIT_OBJ_BLOB - ) - readme = readme_files[i] + strlen("HEAD:"); - git_object_free(obj); - } -} - -void -delta_info_free(struct DeltaInfo *di) -{ - if (!di) return; - - git_patch_free(di->patch); - memset(di, 0, sizeof(*di)); - free(di); -} - -int -commit_info_get_stats(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->add_count++; - ci->add_count++; - } else if (line->new_lineno == -1) { - di->del_count++; - ci->del_count++; - } - } - } - } - ci->ndeltas = i; - ci->file_count = 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++) - delta_info_free(ci->deltas[i]); - free(ci->deltas); - ci->deltas = NULL; - ci->ndeltas = 0; - ci->add_count = 0; - ci->del_count = 0; - ci->file_count = 0; - - return -1; -} - -void -commit_info_free(struct CommitInfo *ci) -{ - size_t i; - - if (!ci) - return; - if (ci->deltas) - for (i = 0; i < ci->ndeltas; i++) - delta_info_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 * -commit_info_get_by_oid(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: - commit_info_free(ci); - - return NULL; -} - -int -refs_cmp(const void *v1, const void *v2) -{ - const struct ReferenceInfo *r1 = v1, *r2 = v2; - time_t t1, t2; - int r; - - if ((r = git_reference_is_tag(r1->ref) - git_reference_is_tag(r2->ref))) - return r; - - t1 = r1->ci->author ? r1->ci->author->when.time : 0; - t2 = r2->ci->author ? r2->ci->author->when.time : 0; - if ((r = t1 > t2 ? -1 : (t1 == t2 ? 0 : 1))) - return r; - - return strcmp( - git_reference_shorthand(r1->ref), - git_reference_shorthand(r2->ref) - ); -} - -int -get_refs(struct ReferenceInfo **pris, size_t *prefcount) -{ - struct ReferenceInfo *ris = NULL; - struct CommitInfo *ci = NULL; - git_reference_iterator *it = NULL; - const git_oid *id = NULL; - git_object *obj = NULL; - git_reference *dref = NULL, *r, *ref = NULL; - size_t i, refcount; - - *pris = NULL; - *prefcount = 0; - - if (git_reference_iterator_new(&it, repo)) - return -1; - - for (refcount = 0; !git_reference_next(&ref, it); ) { - if (!git_reference_is_branch(ref) && !git_reference_is_tag(ref)) { - git_reference_free(ref); - ref = NULL; - continue; - } - - switch (git_reference_type(ref)) { - case GIT_REF_SYMBOLIC: - if (git_reference_resolve(&dref, ref)) - goto err; - r = dref; - break; - case GIT_REF_OID: - r = ref; - break; - default: - continue; - } - if ( - !git_reference_target(r) || - git_reference_peel(&obj, r, GIT_OBJ_ANY) - ) - goto err; - if (!(id = git_object_id(obj))) - goto err; - if (!(ci = commit_info_get_by_oid(id))) - break; - - if (!(ris = reallocarray(ris, refcount + 1, sizeof(*ris)))) - err(1, "realloc"); - ris[refcount].ci = ci; - ris[refcount].ref = r; - refcount++; - - git_object_free(obj); - obj = NULL; - git_reference_free(dref); - dref = NULL; - } - git_reference_iterator_free(it); - - /* sort by type, date then shorthand name */ - qsort(ris, refcount, sizeof(*ris), refs_cmp); - - *pris = ris; - *prefcount = refcount; - - return 0; - -err: - git_object_free(obj); - git_reference_free(dref); - commit_info_free(ci); - for (i = 0; i < refcount; i++) { - commit_info_free(ris[i].ci); - git_reference_free(ris[i].ref); - } - free(ris); - - return -1; -} - -/* Percent-encode, see RFC3986 section 2.1. */ -void -put_percent_encoded(FILE *fp, const char *s, size_t len) -{ - static char tab[] = "0123456789ABCDEF"; - unsigned char uc; - size_t i; - - for (i = 0; *s && i < len; s++, i++) { - uc = *s; - /* NOTE: do not encode '/' for paths or ",-." */ - if ( - uc < ',' || - uc >= 127 || - (uc >= ':' && uc <= '@') || - uc == '[' || - uc == ']' - ) { - putc('%', fp); - putc(tab[(uc >> 4) & 0x0f], fp); - putc(tab[uc & 0x0f], fp); - } else { - putc(uc, fp); - } - } -} - -/* Escape characters below as HTML 2.0 / XML 1.0. */ -void -put_xml_char(FILE *fp, const char *s) -{ - switch(*s) { - case '<': fputs("&lt;", fp); break; - case '>': fputs("&gt;", fp); break; - case '\'': fputs("&#39;", fp); break; - case '&': fputs("&amp;", fp); break; - case '"': fputs("&quot;", fp); break; - default: putc(*s, fp); - } - -} - -void -put_xml(FILE *fp, const char *s, size_t len) -{ - size_t i = 0; - for (; *s && i < len; s++, i++) - put_xml_char(fp, s); -} - -/* terminate on, but don't print '\r' or '\n' */ -void -put_xml_line(FILE *fp, const char *s, size_t len) -{ - size_t i = 0; - for (; *s && i < len; s++, i++) { - if (*s == '\r' || *s == '\n') - break; - put_xml_char(fp, s); - } -} - -int -mkdirp(const char *path) -{ - const char *p; - char tmp[PATH_MAX]; - char *t; - - for(t = tmp, p = path; *p != '\0'; *p++, *t++) { - if (*p != '/') { - *t = *p; - continue; - } - - *t = '\0'; - if ( - mkdir(tmp, S_IRWXU | S_IRWXG | S_IRWXO) < 0 && - errno != EEXIST - ) - return -1; - *t = '/'; - } - return 0; -} - -FILE * -efopen(const char *filename, const char *flags) -{ - FILE *fp; - - mkdirp(filename); - if (!(fp = fopen(filename, flags))) - err(1, "fopen: '%s'", filename); - - return fp; -} - -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); -} - -char * -put_absolute_path(FILE *fp, const char * path) -{ - fprintf(fp, "%s/%s", baseurl, path); -} - -void -put_header(FILE *fp, const char *title) -{ - fputs( - "<!DOCTYPE html>\n" - "<html>" "\n" - "<head>" "\n" - "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n" - "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n" - "<title>", - fp - ); - put_xml(fp, title, strlen(title)); - if (title[0] && stripped_name[0]) - fputs(" - ", fp); - put_xml(fp, stripped_name, strlen(stripped_name)); - if (description[0]) - fputs(" - ", fp); - put_xml(fp, description, strlen(description)); - SPUTF(fp, "</title>" "\n"); - - SPUTF(fp, "<link rel=\"icon\" type=\"image/png\" href=\""); - put_absolute_path(fp, "favicon.png"); - SPUTF(fp, "\" />" "\n"); - - SPUTF(fp, "<link rel=\"alternate\" type=\"application/atom+xml\" title=\""); - put_xml(fp, name, strlen(name)); - SPUTF(fp, " Atom Feed\" href=\""); - put_absolute_path(fp, "atom.xml"); - SPUTF(fp, "\" />" "\n"); - - SPUTF(fp, "<link rel=\"alternate\" type=\"application/atom+xml\" title=\""); - put_xml(fp, name, strlen(name)); - SPUTF(fp, " Atom Feed (tags)\" href=\""); - put_absolute_path(fp, "tags.xml"); - SPUTF(fp, "\" />" "\n"); - - SPUTF(fp, "<link rel=\"stylesheet\" type=\"text/css\" href=\""); - put_absolute_path(fp, "style.css"); - SPUTF(fp, "\" />" "\n"); - SPUTF(fp, "</head>" "\n" "<body>" "\n"); - - SPUTF(fp, "<table>" "<tr><td>"); - /* - fprintf(fp, "<a href=\""); - put_absolute_path(fp, ""); // TODO how to handle? ./.logo.png? - SPUTF(fp, "\"><img src=\""); - put_absolute_path(fp, "logo.png"); - SPUTF(fp, "\" alt=\"\" width=\"32\" height=\"32\" /></a>"); - */ - - SPUTF(fp, "</td><td>" "<h1>"); - if (stripped_name[0] == '\0') SPUTF(fp, "untitled"); - else put_xml(fp, stripped_name, strlen(stripped_name)); - SPUTF(fp, "</h1>" "<span class=\"desc\">"); - put_xml(fp, description, strlen(description)); - SPUTF(fp, "</span>" "</td></tr>"); - if (cloneurl[0]) { - SPUTF(fp, "<tr class=\"url\"><td></td><td>git clone <a href=\""); - put_xml(fp, cloneurl, strlen(cloneurl)); /* not percent-encoded */ - SPUTF(fp, "\">"); - put_xml(fp, cloneurl, strlen(cloneurl)); - SPUTF(fp, "</a></td></tr>"); - } - SPUTF(fp, "<tr><td></td><td>" "\n"); - - if (readme) { - SPUTF(fp, "<a href=\""); - put_absolute_path(fp, "files/"); - fprintf(fp, "%s.html", readme); - SPUTF(fp, "\">README</a> | "); - } - - SPUTF(fp, "<a href=\""); - put_absolute_path(fp, "files.html"); - SPUTF(fp, "\">Files</a> | "); - - SPUTF(fp, "<a href=\""); - put_absolute_path(fp, "log.html"); - SPUTF(fp, "\">Log</a> | "); - - SPUTF(fp, "<a href=\""); - put_absolute_path(fp, "refs.html"); - SPUTF(fp, "\">Refs</a>"); - - if (submodules) { - SPUTF(fp, " | <a href=\""); - put_absolute_path(fp, "files/"); - fprintf(fp, "%s.html", submodules); - SPUTF(fp, "\">Submodules</a>"); - } - - if (license) { - SPUTF(fp, " | <a href=\""); - put_absolute_path(fp, "files/"); - fprintf(fp, "%s.html", license); - SPUTF(fp, "\">LICENSE</a>"); - } - - SPUTF(fp, "</td></tr></table>" "\n"); - SPUTF(fp, "<hr/>" "\n"); - - SPUTF(fp, "<div id=\"content\">" "\n"); -} - -void -put_footer(FILE *fp) -{ - SPUTF(fp, "</div>" "\n" "</body>" "\n" "</html>" "\n"); -} - -size_t -put_file_html(FILE *fp, const git_blob *blob) -{ - size_t n = 0, i, len, prev; - const char *number_format = - "<a href=\"#l%zu\" class=\"line\" id=\"l%zu\">%7zu </a>"; - const char *s = git_blob_rawcontent(blob); - - len = git_blob_rawsize(blob); - SPUTF(fp, "<pre id=\"blob\">" "\n"); - - if (len > 0) { - for (i = 0, prev = 0; i < len; i++) { - if (s[i] != '\n') - continue; - n++; - fprintf(fp, number_format, n, n, n); - put_xml_line(fp, &s[prev], i - prev + 1); - SPUTF(fp, "\n"); - prev = i + 1; - } - /* trailing data */ - if ((len - prev) > 0) { - n++; - fprintf(fp, number_format, n, n, n); - put_xml_line(fp, &s[prev], len - prev); - } - } - - SPUTF(fp, "</pre>" "\n"); - - return n; -} - -void -print_commit(FILE *fp, struct CommitInfo *ci) -{ - SPUTF(fp, "<b>commit</b> <a href=\""); - put_absolute_path(fp, "commits/"); - fprintf(fp, "%s.html", ci->oid); - fprintf(fp, "\">%s</a>" "\n", ci->oid); - - if (ci->parentoid[0]) - SPUTF(fp, "<b>parent</b> <a href=\""); - put_absolute_path(fp, "commits/"); - fprintf(fp, "%s.html", ci->parentoid); - fprintf(fp, "\">%s</a>" "\n", ci->parentoid); - - if (ci->author) { - SPUTF(fp, "<b>Author:</b> "); - put_xml(fp, ci->author->name, strlen(ci->author->name)); - SPUTF(fp, " &lt;<a href=\"mailto:"); - put_xml(fp, ci->author->email, strlen(ci->author->email)); /* not percent-encoded */ - SPUTF(fp, "\">"); - put_xml(fp, ci->author->email, strlen(ci->author->email)); - SPUTF(fp, "</a>&gt;\n<b>Date:</b> "); - printtime(fp, &(ci->author->when)); - CPUT(fp, '\n'); - } - if (ci->msg) { - CPUT(fp, '\n'); - put_xml(fp, ci->msg, strlen(ci->msg)); - CPUT(fp, '\n'); - } -} - -void -put_show_file(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; - - print_commit(fp, ci); - - if (!ci->deltas) - return; - - if ( - ci->file_count > 1000 || - ci->ndeltas > 1000 || - ci->add_count > 100000 || - ci->del_count > 100000 - ) { - fputs("Diff is too large, output suppressed.\n", fp); - return; - } - - /* diff stat */ - fputs("<b>Diffstat:</b>" "\n", fp); - fputs("<table id=\"diffstat\">", fp); - for (i = 0; i < ci->ndeltas; i++) { - delta = git_patch_get_delta(ci->deltas[i]->patch); - - switch (delta->status) { - case GIT_DELTA_ADDED: c = 'A'; break; - case GIT_DELTA_COPIED: c = 'C'; break; - case GIT_DELTA_DELETED: c = 'D'; break; - case GIT_DELTA_MODIFIED: c = 'M'; break; - case GIT_DELTA_RENAMED: c = 'R'; break; - case GIT_DELTA_TYPECHANGE: c = 'T'; break; - default: c = ' '; break; - } - if (c == ' ') - fprintf(fp, "<tr><td>%c", c); - else - fprintf(fp, "<tr><td class=\"%c\">%c", c, c); - - fprintf(fp, "</td><td><a href=\"#h%zu\">", i); - put_xml(fp, delta->old_file.path, strlen(delta->old_file.path)); - if (strcmp(delta->old_file.path, delta->new_file.path)) { - fputs(" -&gt; ", fp); - put_xml(fp, delta->new_file.path, strlen(delta->new_file.path)); - } - - add = ci->deltas[i]->add_count; - del = ci->deltas[i]->del_count; - changed = add + del; - total = sizeof(linestr) - 2; - if (changed > total) { - if (add) - add = ((float)total / changed * add) + 1; - if (del) - del = ((float)total / changed * del) + 1; - } - memset(&linestr, '+', add); - memset(&linestr[add], '-', del); - - fprintf( - fp, - "</a></td>" - // "<td> | </td>" - "<td class=\"num\">%zu</td><td><span class=\"i\">", - ci->deltas[i]->add_count + ci->deltas[i]->del_count - ); - fwrite(&linestr, 1, add, fp); - fputs("</span><span class=\"d\">", fp); - fwrite(&linestr[add], 1, del, fp); - fputs("</span></td></tr>\n", fp); - } - fprintf( - fp, - "</table></pre><pre>%zu file%s changed, %zu insertion%s(+), %zu deletion%s(-)\n", - ci->file_count, ci->file_count == 1 ? "" : "s", - ci->add_count, ci->add_count == 1 ? "" : "s", - ci->del_count, ci->del_count == 1 ? "" : "s" - ); - - fputs("<hr/>", fp); - - for (i = 0; i < ci->ndeltas; i++) { - patch = ci->deltas[i]->patch; - delta = git_patch_get_delta(patch); - - fprintf(fp, "<b>diff --git a/<a id=\"h%zu\" href=\"", i); - - put_absolute_path(fp, "files/"); - put_percent_encoded(fp, delta->old_file.path, strlen(delta->old_file.path)); - SPUTF(fp, ".html\">"); - - put_xml(fp, delta->old_file.path, strlen(delta->old_file.path)); - SPUTF(fp, "</a> b/<a href=\""); - - put_absolute_path(fp, "files/"); - put_percent_encoded(fp, delta->new_file.path, strlen(delta->new_file.path)); - SPUTF(fp, ".html\">"); - - put_xml(fp, delta->new_file.path, strlen(delta->new_file.path)); - SPUTF(fp, "</a></b>" "\n"); - - /* check binary data */ - if (delta->flags & GIT_DIFF_FLAG_BINARY) { - SPUTF(fp, "Binary files differ." "\n"); - continue; - } - - nhunks = git_patch_num_hunks(patch); - for (j = 0; j < nhunks; j++) { - if (git_patch_get_hunk(&hunk, &nhunklines, patch, j)) - break; - - fprintf(fp, "<a href=\"#h%zu-%zu\" id=\"h%zu-%zu\" class=\"h\">", i, j, i, j); - put_xml(fp, hunk->header, hunk->header_len); - SPUTF(fp, "</a>"); - - for (k = 0; ; k++) { - if (git_patch_get_line_in_hunk(&line, patch, j, k)) - break; - if (line->old_lineno == -1) - fprintf( - fp, - "<a href=\"#h%zu-%zu-%zu\" id=\"h%zu-%zu-%zu\" class=\"i\">+", - i, j, k, i, j, k - ); - else if (line->new_lineno == -1) - fprintf( - fp, - "<a href=\"#h%zu-%zu-%zu\" id=\"h%zu-%zu-%zu\" class=\"d\">-", - i, j, k, i, j, k - ); - else - CPUT(fp, ' '); - put_xml_line(fp, line->content, line->content_len); - CPUT(fp, '\n'); - if (line->old_lineno == -1 || line->new_lineno == -1) - SPUTF(fp, "</a>"); - } - } - } -} - -void -put_log_line(FILE *fp, struct CommitInfo *ci) -{ - fputs("<tr><td>", fp); - if (ci->author) - printtimeshort(fp, &(ci->author->when)); - fputs("</td><td>", fp); - if (ci->summary) { - SPUTF(fp, "<a href=\""); - put_absolute_path(fp, "commits/"); - fprintf(fp, "%s.html", ci->oid); - SPUTF(fp, "\">"); - put_xml(fp, ci->summary, strlen(ci->summary)); - SPUTF(fp, "</a>"); - } - SPUTF(fp, "</td><td>"); - if (ci->author) - put_xml(fp, ci->author->name, strlen(ci->author->name)); - SPUTF(fp, "</td><td class=\"num\" align=\"right\">"); - fprintf(fp, "%zu", ci->file_count); - SPUTF(fp, "</td><td class=\"num\" align=\"right\">"); - fprintf(fp, "+%zu", ci->add_count); - SPUTF(fp, "</td><td class=\"num\" align=\"right\">"); - fprintf(fp, "-%zu", ci->del_count); - SPUTF(fp, "</td></tr>" "\n"); -} - -int -write_log(FILE *fp, const git_oid *oid) -{ - struct CommitInfo *ci; - git_revwalk *w = NULL; - git_oid id; - char path[PATH_MAX], oidstr[GIT_OID_HEXSZ + 1]; - FILE *fpfile; - size_t remcommits = 0; - int r; - - git_revwalk_new(&w, repo); - git_revwalk_push(w, oid); - - while (!git_revwalk_next(&id, w)) { - if (cachefile && !memcmp(&id, &lastoid, sizeof(id))) - break; - - git_oid_tostr(oidstr, sizeof(oidstr), &id); - r = snprintf(path, sizeof(path), "commits/%s.html", oidstr); - if (r < 0 || (size_t)r >= sizeof(path)) - errx(1, "path truncated: 'commits/%s.html'", oidstr); - r = access(path, F_OK); - - /* optimization: if there are no log lines to write and - the commit file already exists: skip the diffstat */ - if (!nlogcommits) { - remcommits++; - if (!r) - continue; - } - - if (!(ci = commit_info_get_by_oid(&id))) - break; - /* diffstat: for stagit HTML required for the log.html line */ - if (commit_info_get_stats(ci) == -1) - goto err; - - if (nlogcommits != 0) { - put_log_line(fp, ci); - if (nlogcommits > 0) - nlogcommits--; - } - - if (cachefile) - put_log_line(wcachefp, ci); - - /* check if file exists if so skip it */ - if (r) { - fpfile = efopen(path, "w"); - put_header(fpfile, ci->summary); - - fputs("<pre>", fpfile); - put_show_file(fpfile, ci); - fputs("</pre>\n", fpfile); - - put_footer(fpfile); - check_file_error(fpfile, path, 'w'); - fclose(fpfile); - } -err: - commit_info_free(ci); - } - git_revwalk_free(w); - - if (nlogcommits == 0 && remcommits != 0) - fprintf( - fp, - "<tr><td></td><td colspan=\"5\">" - "%zu more commits remaining, fetch the repository" - "</td></tr>\n", - remcommits - ); - - return 0; -} - -// TODO fix this ungodly mess. Yuck! -void -write_log_cached(FILE *fp, const git_oid *head) -{ - char tmppath[64] = "cache.XXXXXXXXXXXX"; - char buf[BUFSIZ]; - mode_t mask; - int fd, n; - - /* 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); - - write_log(fp, head); - - if (rcachefp) { - /* append previous log to log.html and the new cache */ - while (!feof(rcachefp)) { - n = fread(buf, 1, sizeof(buf), rcachefp); - if (ferror(rcachefp)) - break; - if ( - fwrite(buf, 1, n, fp) != n || - fwrite(buf, 1, n, wcachefp) != n - ) - break; - } - check_file_error(rcachefp, cachefile, 'r'); - fclose(rcachefp); - } - check_file_error(wcachefp, tmppath, 'w'); - fclose(wcachefp); - - // TODO previously, this would check that the log page was written - // successfully before renaming the cachefile... I'm probably going - // to blow this whole caching mess away so as long as it runs for - // now, it shouldn't matter. - - 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); -} - -void -put_log_html(FILE *fp, const git_oid *head) -{ - put_header(fp, "Log"); - fputs( - "<table id=\"log\"><thead>" "\n" - "<tr>" - "<td><b>Date</b></td>" - "<td><b>Commit message</b></td>" - "<td><b>Author</b></td>" - "<td class=\"num\" align=\"right\"><b>Files</b></td>" - "<td class=\"num\" align=\"right\"><b>+</b></td>" - "<td class=\"num\" align=\"right\"><b>-</b></td>" - "</tr>" "\n" - "</thead><tbody>" "\n", - fp - ); - - if (head && !cachefile) write_log(fp, head); - if (head && cachefile) write_log_cached(fp, head); - - fputs("</tbody></table>", fp); - put_footer(fp); -} - -void -print_commit_atom(FILE *fp, struct CommitInfo *ci, const char *tag) -{ - fputs("<entry>\n", fp); - - fprintf(fp, "<id>%s</id>\n", ci->oid); - if (ci->author) { - SPUTF(fp, "<published>"); - printtimez(fp, &(ci->author->when)); - SPUTF(fp, "</published>\n"); - } - if (ci->committer) { - SPUTF(fp, "<updated>"); - printtimez(fp, &(ci->committer->when)); - SPUTF(fp, "</updated>\n"); - } - if (ci->summary) { - SPUTF(fp, "<title>"); - if (tag && tag[0]) { - SPUTF(fp, "["); - put_xml(fp, tag, strlen(tag)); - SPUTF(fp, "] "); - } - put_xml(fp, ci->summary, strlen(ci->summary)); - SPUTF(fp, "</title>\n"); - } - fprintf( - fp, - "<link rel=\"alternate\" type=\"text/html\" href=\"%scommits/%s.html\" />\n", - baseurl, - ci->oid - ); - - if (ci->author) { - SPUTF(fp, "<author>\n<name>"); - put_xml(fp, ci->author->name, strlen(ci->author->name)); - SPUTF(fp, "</name>\n<email>"); - put_xml(fp, ci->author->email, strlen(ci->author->email)); - SPUTF(fp, "</email>\n</author>\n"); - } - - SPUTF(fp, "<content>"); - fprintf(fp, "commit %s\n", ci->oid); - if (ci->parentoid[0]) - fprintf(fp, "parent %s\n", ci->parentoid); - if (ci->author) { - SPUTF(fp, "Author: "); - put_xml(fp, ci->author->name, strlen(ci->author->name)); - SPUTF(fp, " &lt;"); - put_xml(fp, ci->author->email, strlen(ci->author->email)); - SPUTF(fp, "&gt;\nDate: "); - printtime(fp, &(ci->author->when)); - CPUT(fp, '\n'); - } - if (ci->msg) { - CPUT(fp, '\n'); - put_xml(fp, ci->msg, strlen(ci->msg)); - } - SPUTF(fp, "\n</content>\n</entry>\n"); -} - -int -write_atom(FILE *fp, int all) -{ - struct ReferenceInfo *ris = NULL; - size_t refcount = 0; - struct CommitInfo *ci; - git_revwalk *w = NULL; - git_oid id; - size_t i, m = 100; /* last 'm' commits */ - - SPUTF( - fp, - "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" - "<feed xmlns=\"http://www.w3.org/2005/Atom\">\n<title>" - ); - put_xml(fp, stripped_name, strlen(stripped_name)); - SPUTF(fp, ", branch HEAD</title>\n<subtitle>"); - put_xml(fp, description, strlen(description)); - SPUTF(fp, "</subtitle>\n"); - - /* all commits or only tags? */ - if (all) { - git_revwalk_new(&w, repo); - git_revwalk_push_head(w); - for (i = 0; i < m && !git_revwalk_next(&id, w); i++) { - if (!(ci = commit_info_get_by_oid(&id))) - break; - print_commit_atom(fp, ci, ""); - commit_info_free(ci); - } - git_revwalk_free(w); - } else if (get_refs(&ris, &refcount) != -1) { - /* references: tags */ - for (i = 0; i < refcount; i++) { - if (git_reference_is_tag(ris[i].ref)) - print_commit_atom( - fp, ris[i].ci, - git_reference_shorthand(ris[i].ref) - ); - - commit_info_free(ris[i].ci); - git_reference_free(ris[i].ref); - } - free(ris); - } - - fputs("</feed>\n", fp); - - return 0; -} - -void -write_binary_file(const char *filename, git_blob *blob) { - char dest[PATH_MAX] = ""; - const char *raw = git_blob_rawcontent(blob); - int size = git_blob_rawsize(blob); - - /* set dest */ - join_path(dest, PATH_MAX, "files", filename); - - /* write the file! */ - FILE *fp = efopen(dest, "w"); - fwrite(raw, size, 1, fp); - check_file_error(fp, dest, 'w'); - fclose(fp); -} - -void -put_size(FILE *fp, size_t size, int fixed) -{ - int order = 0; - int decimal; - - if (size == 0) { - fprintf(fp, " - "); - return; - } - - while (size > 1000) { - order++; - decimal = (size / 100) % 10; - size /= 1000; - } - - if (size < 10 && order > 0) fprintf(fp, "%1zu.%1d", size, decimal); - else fprintf(fp, fixed ? "%3zu" : "%zu", size); - - if (order != 0 || fixed) fprintf(fp, "%c", orders[order]); -} - -size_t -put_blob( - git_object *obj, - const char *filename, - const char *entrypath, - const char *entryname, - size_t bytes -) { - char tmp[PATH_MAX] = ""; - char *d; - const char *p; - size_t lines = 0; - FILE *fp; - int is_binary; - - is_binary = git_blob_is_binary((git_blob *)obj); - const char *file_url = is_binary ? entryname : ""; - - fp = efopen(filename, "w"); - put_header(fp, entryname); - fprintf(fp, "<p>"); - put_xml(fp, entrypath, strlen(entrypath)); - SPUTF(fp, "</p>"); - - if (is_binary) { - fprintf( - fp, - "<p>Binary file: <a href=\"%s\">raw</a></p>" "\n" - "<p><object data=\"%s\">" - "no preview available." - "</object></p>" "\n", - file_url, - file_url - ); - write_binary_file(entrypath, (git_blob *) obj); - } else { - lines = put_file_html(fp, (git_blob *) obj); - } - - put_footer(fp); - check_file_error(fp, filename, 'w'); - fclose(fp); - - return lines; -} - -char -get_filemode_type(git_filemode_t m) -{ - if (S_ISREG(m)) return '-'; - if (S_ISBLK(m)) return 'b'; - if (S_ISCHR(m)) return 'c'; - if (S_ISDIR(m)) return 'd'; - if (S_ISFIFO(m)) return 'p'; - if (S_ISLNK(m)) return 'l'; - if (S_ISSOCK(m)) return 's'; - return '?'; -} - -const char * -get_filemode(const git_tree_entry *entry) -{ - git_filemode_t mode; - static char ret[11]; - - /* used for backlink: .. */ - if (entry == NULL) - return "d---------"; - - mode = git_tree_entry_filemode(entry); - - memset(ret, '-', sizeof(ret) - 1); - ret[0] = get_filemode_type(mode); - if (mode & S_IRUSR) ret[1] = 'r'; - if (mode & S_IWUSR) ret[2] = 'w'; - if (mode & S_IXUSR) ret[3] = 'x'; - if (mode & S_ISUID) ret[3] = (ret[3] == 'x') ? 's' : 'S'; - if (mode & S_IRGRP) ret[4] = 'r'; - if (mode & S_IWGRP) ret[5] = 'w'; - if (mode & S_IXGRP) ret[6] = 'x'; - if (mode & S_ISGID) ret[6] = (ret[6] == 'x') ? 's' : 'S'; - if (mode & S_IROTH) ret[7] = 'r'; - if (mode & S_IWOTH) ret[8] = 'w'; - if (mode & S_IXOTH) ret[9] = 'x'; - if (mode & S_ISVTX) ret[9] = (ret[9] == 'x') ? 't' : 'T'; - ret[10] = '\0'; - - return ret; -} - -struct Weight -put_filetree_file_line( - FILE *fp, - git_object *obj, - const git_tree_entry *entry, - char *filename, - char *entrypath, - const char *entryname -) { - size_t bytes = git_blob_rawsize((git_blob *)obj); - size_t lines = put_blob(obj, filename, entrypath, entryname, bytes); - struct Weight ret = { bytes, lines }; - - SPUTF(fp, "<tr><td>"); - SPUTF(fp, get_filemode(entry)); - SPUTF(fp, "</td><td><a href=\""); - put_absolute_path(fp, ""); - put_percent_encoded(fp, filename, strlen(filename)); - SPUTF(fp, "\">"); - put_xml(fp, entryname, strlen(entryname)); - SPUTF(fp, "</a></td><td class=\"num\" align=\"right\">"); - put_size(fp, lines, 1); - SPUTF(fp, "</td><td class=\"num\" align=\"right\">"); - put_size(fp, bytes, 1); - SPUTF(fp, "</td></tr>\n"); - return ret; -} - -void -put_filetree_dir_line( - FILE *fp, - const git_tree_entry *entry, - char *destination, - const char *title, - size_t lines, - size_t bytes -) { - SPUTF(fp, "<tr><td>"); - SPUTF(fp, get_filemode(entry)); - SPUTF(fp, "</td><td><a href=\""); - put_absolute_path(fp, ""); - put_percent_encoded(fp, destination, strlen(destination)); - SPUTF(fp, "\">"); - put_xml(fp, title, strlen(title)); - SPUTF(fp, "</a></td><td class=\"num\" align=\"right\">"); - put_size(fp, lines, 1); - SPUTF(fp, "</td><td class=\"num\" align=\"right\">"); - put_size(fp, bytes, 1); - SPUTF(fp, "</td></tr>\n"); -} - -void put_filetree_backlink_line(FILE *fp, const char *path) { - char previous_path[PATH_MAX]; - char previous_filename[PATH_MAX]; - int i, r, last_slash = 0; - - /* figure out previous directory */ - // TODO how to do this more cleanly... - for (i = 0; path[i] != '\0'; i++) { - previous_path[i] = path[i]; - if (path[i] == '/') last_slash = i; - } - previous_path[last_slash] = '\0'; - - if (last_slash == 0) { - put_filetree_dir_line(fp, NULL, "files.html", "..", 0, 0); - } else { - r = snprintf(previous_filename, sizeof(previous_filename), "files/%s.html", previous_path); - if (r < 0 || (size_t) r >= sizeof(previous_filename)) - errx(1, "path truncated: 'files/%s.html'", previous_path); - put_filetree_dir_line(fp, NULL, previous_filename, "..", 0, 0); - } -} - -/* need this here to allow recursion */ -struct Weight write_filetree( - const char *path, - const char *filename, - git_tree *tree -); - -struct Weight -put_entry_obj(FILE *fp, git_object *obj, const git_tree_entry *entry, char *entrypath, const char *entryname) -{ - struct Weight ret; - git_object_t obj_type = git_object_type(obj); - char filename[PATH_MAX]; - int r; - - /* filename = f("files/%s.html", entrypath) */ - r = snprintf(filename, sizeof(filename), "files/%s.html", entrypath); - if (r < 0 || (size_t) r >= sizeof(filename)) - errx(1, "path truncated: 'files/%s.html'", entrypath); - - switch (obj_type) { - case GIT_OBJ_TREE: - /* NOTE: recurses */ - ret = write_filetree(entrypath, filename, (git_tree *) obj); - put_filetree_dir_line( - fp, - entry, - filename, - entryname, - ret.lines, - ret.bytes - ); - break; - case GIT_OBJ_BLOB: - ret = put_filetree_file_line( - fp, - obj, - entry, - filename, - entrypath, - entryname - ); - break; - } - - return ret; -} - -void -put_submodule_obj( - FILE *fp, - const git_tree_entry *entry, - char *entrypath -) { - char oid[8]; - - SPUTF(fp, "<tr>" "<td>m---------</td>" "<td><a href=\""); - put_absolute_path(fp, "files/.gitmodules.html"); - SPUTF(fp, "\">"); - put_xml(fp, entrypath, strlen(entrypath)); - SPUTF(fp, "</a> @ "); - git_oid_tostr(oid, sizeof(oid), git_tree_entry_id(entry)); - put_xml(fp, oid, strlen(oid)); - SPUTF(fp, "</td>" "<td class=\"num\" align=\"right\"></td>" "\n"); - SPUTF(fp, "<td class=\"num\" align=\"right\"></td>" "</tr>" "\n"); -} - -struct Weight -put_file_list(FILE *fp, git_tree *tree, const char *path) -{ - struct Weight ret = {}; - size_t count; - size_t i; - - /* add line for ".." back-link */ - if (path[0] != '\0') put_filetree_backlink_line(fp, path); - - count = git_tree_entrycount(tree); - for (i = 0; i < count; i++) { - const git_tree_entry *entry = NULL; - const char *entryname; - char entrypath[PATH_MAX]; - - if ( - !(entry = git_tree_entry_byindex(tree, i)) || - !(entryname = git_tree_entry_name(entry)) - ) - return ret; - - /* entrypath = path + entryname */ - join_path(entrypath, sizeof(entrypath), path, entryname); - - { - git_object *obj = NULL; - if (!git_tree_entry_to_object(&obj, repo, entry)) { - struct Weight weight = put_entry_obj( - fp, - obj, - entry, - entrypath, - entryname - ); - ret.bytes += weight.bytes; - ret.lines += weight.lines; - git_object_free(obj); - continue; - } - } - - if (git_tree_entry_type(entry) == GIT_OBJ_COMMIT) { - put_submodule_obj(fp, entry, entrypath); - // TODO ensure that this works with the size stuff correctly! - continue; - } - } - - return ret; -} - -void -put_filetree_dir_title(FILE *fp, const char *path) -{ - put_xml(fp, path, strlen(path)); -} - -struct Weight -write_filetree(const char *path, const char *filename, git_tree *tree) -{ - FILE *fp; - struct Weight ret; - - fp = efopen(filename, "w"); - put_header(fp, "Files"); - - SPUTF(fp, "<p>"); - put_filetree_dir_title(fp, path); - SPUTF(fp, "</p>"); - - SPUTF( - fp, - "<table id=\"files\"><thead>" "\n" "<tr>" - "<td><b>Mode</b></td><td><b>Name</b></td>" - "<td class=\"num\" align=\"right\"><b> L </b></td>" - "<td class=\"num\" align=\"right\"><b> B </b></td>" - "</tr>" "\n" "</thead><tbody>" "\n" - ); - - if (tree != NULL) - ret = put_file_list(fp, tree, path); - - SPUTF(fp, "</tbody></table>"); - - SPUTF(fp, "<p>Totals: "); - put_size(fp, ret.lines, 0); - SPUTF(fp, "L "); - put_size(fp, ret.bytes, 0); - SPUTF(fp, "B</p>"); - - put_footer(fp); - check_file_error(fp, filename, 'w'); - fclose(fp); - - return ret; -} - -void -walk_git_tree(const git_oid *id) -{ - git_tree *tree = NULL; - git_commit *commit = NULL; - int has_files; - - if (!id) return; - - has_files = !git_commit_lookup(&commit, repo, id) && - !git_commit_tree(&tree, commit); - - write_filetree("", "files.html", has_files ? tree : NULL); - - git_commit_free(commit); - git_tree_free(tree); -} - -int -write_refs(FILE *fp) -{ - struct ReferenceInfo *ris = NULL; - struct CommitInfo *ci; - size_t count, i, j, refcount; - const char *titles[] = { "Branches", "Tags" }; - const char *ids[] = { "branches", "tags" }; - const char *s; - - if (get_refs(&ris, &refcount) == -1) - return -1; - - for (i = 0, j = 0, count = 0; i < refcount; i++) { - if (j == 0 && git_reference_is_tag(ris[i].ref)) { - if (count) - fputs("</tbody></table><br/>\n", fp); - count = 0; - j = 1; - } - - /* print header if it has an entry (first). */ - if (++count == 1) - fprintf( - fp, - "<h2>%s</h2><table id=\"%s\">" - "<thead>\n<tr><td><b>Name</b></td>" - "<td><b>Last commit date</b></td>" - "<td><b>Author</b></td>\n</tr>\n" - "</thead><tbody>\n", - titles[j], - ids[j] - ); - - ci = ris[i].ci; - s = git_reference_shorthand(ris[i].ref); - - fputs("<tr><td>", fp); - put_xml(fp, s, strlen(s)); - fputs("</td><td>", fp); - if (ci->author) - printtimeshort(fp, &(ci->author->when)); - fputs("</td><td>", fp); - if (ci->author) - put_xml(fp, ci->author->name, strlen(ci->author->name)); - fputs("</td></tr>\n", fp); - } - - if (count) - fputs("</tbody></table><br/>\n", fp); - - for (i = 0; i < refcount; i++) { - commit_info_free(ris[i].ci); - git_reference_free(ris[i].ref); - } - free(ris); - - return 0; -} - -void -write_log_page(const git_oid *head) -{ - const char *filename = "log.html"; - FILE *fp = efopen(filename, "w"); - put_log_html(fp, head); - check_file_error(fp, filename, 'w'); - fclose(fp); -} - -void -write_refs_page() -{ - const char *filename = "refs.html"; - FILE *fp = efopen(filename, "w"); - put_header(fp, "Refs"); - write_refs(fp); - put_footer(fp); - check_file_error(fp, filename, 'w'); - fclose(fp); -} - -void -write_feed() -{ - const char *filename = "atom.xml"; - FILE *fp = efopen(filename, "w"); - write_atom(fp, 1); - check_file_error(fp, filename, 'w'); - fclose(fp); -} - -void -write_releases_feed() -{ - const char *filename = "tags.xml"; - FILE *fp = efopen(filename, "w"); - write_atom(fp, 0); - check_file_error(fp, filename, 'w'); - fclose(fp); -} - -void -usage(char *argv0) -{ - fprintf( - stderr, - "usage: %s [-c cachefile | -l commits] " - "[-u baseurl] repodir" "\n", - argv0 - ); - exit(1); -} - -int -main(int argc, char *argv[]) -{ - const git_oid *head = NULL; - FILE *fp, *fpread; - char path[PATH_MAX], repodirabs[PATH_MAX + 1], *p; - git_object *obj = NULL; - int i; - - /* parse args */ - for (i = 1; i < argc; i++) { - if (argv[i][0] != '-') { - if (repodir) - usage(argv[0]); - repodir = argv[i]; - } else if (argv[i][1] == 'c') { - if (nlogcommits > 0 || i + 1 >= argc) - usage(argv[0]); - cachefile = argv[++i]; - } else if (argv[i][1] == 'l') { - if (cachefile || i + 1 >= argc) - usage(argv[0]); - errno = 0; - nlogcommits = strtoll(argv[++i], &p, 10); - if ( - argv[i][0] == '\0' || - *p != '\0' || - nlogcommits <= 0 || - errno - ) - usage(argv[0]); - } else if (argv[i][1] == 'u') { - if (i + 1 >= argc) - usage(argv[0]); - baseurl = argv[++i]; - } - } - if (!repodir) - usage(argv[0]); - - if (!realpath(repodir, repodirabs)) - err(1, "realpath"); - - /* do not search outside the git repository: - GIT_CONFIG_LEVEL_APP is the highest level currently */ - git_libgit2_init(); - for (i = 1; i <= GIT_CONFIG_LEVEL_APP; i++) - git_libgit2_opts(GIT_OPT_SET_SEARCH_PATH, i, ""); - /* do not require the git repository to be owned by the current user */ - git_libgit2_opts(GIT_OPT_SET_OWNER_VALIDATION, 0); - -#ifdef __OpenBSD__ - if (unveil(repodir, "r") == -1) - err(1, "unveil: %s", repodir); - if (unveil(".", "rwc") == -1) - err(1, "unveil: ."); - if (cachefile && unveil(cachefile, "rwc") == -1) - err(1, "unveil: %s", cachefile); - - if (cachefile) { - if (pledge("stdio rpath wpath cpath fattr", NULL) == -1) - err(1, "pledge"); - } else { - if (pledge("stdio rpath wpath cpath", NULL) == -1) - err(1, "pledge"); - } -#endif - - if ( - git_repository_open_ext( - &repo, - repodir, - GIT_REPOSITORY_OPEN_NO_SEARCH, - NULL - ) < 0 - ) { - fprintf(stderr, "%s: cannot open repository\n", argv[0]); - return 1; - } - - /* find HEAD */ - if (!git_revparse_single(&obj, repo, "HEAD")) - head = git_object_id(obj); - git_object_free(obj); - - /* use directory name as name */ - if ((name = strrchr(repodirabs, '/'))) name++; - else name = ""; - - /* strip .git suffix */ - if (!(stripped_name = strdup(name))) - err(1, "strdup"); - if ((p = strrchr(stripped_name, '.'))) - if (!strcmp(p, ".git")) - *p = '\0'; - - /* read description or .git/description */ - join_path(path, sizeof(path), repodir, "description"); - if (!(fpread = fopen(path, "r"))) { - join_path(path, sizeof(path), repodir, ".git/description"); - fpread = fopen(path, "r"); - } - if (fpread) { - if (!fgets(description, sizeof(description), fpread)) - description[0] = '\0'; - check_file_error(fpread, path, 'r'); - fclose(fpread); - } - - /* read url or .git/url */ - join_path(path, sizeof(path), repodir, "url"); - if (!(fpread = fopen(path, "r"))) { - join_path(path, sizeof(path), repodir, ".git/url"); - fpread = fopen(path, "r"); - } - if (fpread) { - if (!fgets(cloneurl, sizeof(cloneurl), fpread)) - cloneurl[0] = '\0'; - check_file_error(fpread, path, 'r'); - fclose(fpread); - cloneurl[strcspn(cloneurl, "\n")] = '\0'; - } - - find_license(); - find_readme(); - - if ( - !git_revparse_single(&obj, repo, "HEAD:.gitmodules") && - git_object_type(obj) == GIT_OBJ_BLOB - ) - submodules = ".gitmodules"; - git_object_free(obj); - - /* write pages! */ - write_log_page(head); - walk_git_tree(head); - write_refs_page(); - write_feed(); - write_releases_feed(); - - /* cleanup */ - git_repository_free(repo); - git_libgit2_shutdown(); - - return 0; -} diff --git a/strlcat.c b/strlcat.c @@ -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 <Todd.Miller@courtesan.com> - * - * Permission to use, copy, modify, and distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - */ - -#include <sys/types.h> -#include <string.h> - -#include "compat.h" - -/* - * Appends src to string dst of size dsize (unlike strncat, dsize is the - * full size of dst, not space left). At most dsize-1 characters - * will be copied. Always NUL terminates (unless dsize <= strlen(dst)). - * Returns strlen(src) + MIN(dsize, strlen(initial dst)). - * If retval >= dsize, truncation occurred. - */ -size_t -strlcat(char *dst, const char *src, size_t dsize) -{ - const char *odst = dst; - const char *osrc = src; - size_t n = dsize; - size_t dlen; - - /* Find the end of dst and adjust bytes left but don't go past end. */ - while (n-- != 0 && *dst != '\0') - dst++; - dlen = dst - odst; - n = dsize - dlen; - - if (n-- == 0) - return(dlen + strlen(src)); - while (*src != '\0') { - if (n != 0) { - *dst++ = *src; - n--; - } - src++; - } - *dst = '\0'; - - return(dlen + (src - osrc)); /* count does not include NUL */ -} diff --git a/strlcpy.c b/strlcpy.c @@ -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 <Todd.Miller@courtesan.com> - * - * Permission to use, copy, modify, and distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - */ - -#include <sys/types.h> -#include <string.h> - -#include "compat.h" - -/* - * Copy string src to buffer dst of size dsize. At most dsize-1 - * chars will be copied. Always NUL terminates (unless dsize == 0). - * Returns strlen(src); if retval >= dsize, truncation occurred. - */ -size_t -strlcpy(char *dst, const char *src, size_t dsize) -{ - const char *osrc = src; - size_t nleft = dsize; - - /* Copy as many bytes as will fit. */ - if (nleft != 0) { - while (--nleft != 0) { - if ((*dst++ = *src++) == '\0') - break; - } - } - - /* Not enough room in dst, add NUL and traverse rest of src. */ - if (nleft == 0) { - if (dsize != 0) - *dst = '\0'; /* NUL-terminate dst */ - while (*src++) - ; - } - - return(src - osrc - 1); /* count does not include NUL */ -} diff --git a/style.css b/style.css @@ -98,6 +98,7 @@ hr { } pre { + text-wrap: wrap; font-family: monospace; margin: 0px; }

This webpage is intended to be an accessible preview of this repository. To get a fuller picture, clone it and use the git CLI.