sgw | Static git web (fork of stagit) |
| download: https://git.y1.nz/archives/sgw.tar.gz | |
| README | Files | Log | Refs | LICENSE |
commit ed7acdb2111aaf1dfa1fd54eb564fb6e4f48c252 parent 022c85fdcf44b9b8723ce6983c62d585e2563c9d Author: Emma Weaver <emma@waeaves.com> Date: Thu, 11 Jun 2026 16:57:56 -0400 Emma rewrite :3 (a lot of changes) Diffstat:
| M | .gitignore | 5 | ++--- |
| M | Makefile | 123 | +++++++++++++------------------------------------------------------------------ |
| M | README | 224 | ++++++++++++++----------------------------------------------------------------- |
| M | TODO | 42 | +++++++++++------------------------------- |
| M | compat.h | 2 | -- |
| A | config.h | 41 | +++++++++++++++++++++++++++++++++++++++++ |
| A | config.mk | 14 | ++++++++++++++ |
| D | example_create.sh | 43 | ------------------------------------------- |
| D | example_post-receive.sh | 73 | ------------------------------------------------------------------------- |
| A | files.c | 86 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | files.h | 10 | ++++++++++ |
| A | index.c | 127 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | main.c | 1868 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | opt.c | 68 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | opt.h | 32 | ++++++++++++++++++++++++++++++++ |
| A | sgw.1 | 66 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| D | stagit-index.1 | 47 | ----------------------------------------------- |
| D | stagit-index.c | 258 | ------------------------------------------------------------------------------- |
| D | stagit.1 | 125 | ------------------------------------------------------------------------------- |
| D | stagit.c | 1566 | ------------------------------------------------------------------------------- |
| D | strlcat.c | 57 | --------------------------------------------------------- |
| D | strlcpy.c | 52 | ---------------------------------------------------- |
| M | style.css | 35 | +++++++++-------------------------- |
23 files changed, 2392 insertions(+), 2572 deletions(-)
diff --git a/.gitignore b/.gitignore @@ -1,6 +1,5 @@ *.o **.swp -stagit -stagit-index +sgw test - +old diff --git a/Makefile b/Makefile @@ -1,113 +1,30 @@ -.POSIX: -NAME = stagit -VERSION = 1.2 +include config.mk -# paths -PREFIX = /usr/local -MANPREFIX = ${PREFIX}/man -DOCPREFIX = ${PREFIX}/share/doc/${NAME} +sgw: main.c opt.o files.o index.o config.h config.mk + $(CC) main.c opt.o files.o index.o -o sgw $(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 +files.o: files.c files.h config.mk + $(CC) -c files.c -o files.o $(CFLAGS) -# 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} +index.o: index.c config.h config.mk + $(CC) -c index.c -o index.o $(CFLAGS) clean: - rm -f ${BIN} ${OBJ} ${NAME}-${VERSION}.tar.gz + rm -f opt.o files.o index.o sgw -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: sgw config.mk + mkdir -p $(PREFIX)/bin + cp -f sgw $(PREFIX)/bin + chmod 755 $(PREFIX)/bin/sgw + mkdir -p $(MANPREFIX)/man1 + sed "s/VERSION/$(VERSION)/g" < sgw.1 > $(MANPREFIX)/man1/sgw.1 + chmod 644 $(MANPREFIX)/man1/sgw.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/sgw + rm -f $(MANPREFIX)/man1/sgw.1 -.PHONY: all clean dist install uninstall diff --git a/README b/README @@ -1,186 +1,38 @@ -stagit ------- - -static git page generator. - -It generates static HTML pages for a git repository. - - -Usage ------ - -Make files per repository: - - $ mkdir -p htmlroot/htmlrepo1 && cd htmlroot/htmlrepo1 - $ stagit path/to/gitrepo1 - repeat for other repositories - $ ... - -Make index file for repositories: - - $ cd htmlroot - $ stagit-index path/to/gitrepo1 \ - path/to/gitrepo2 \ - path/to/gitrepo3 > index.html - - -Build and install ------------------ - -$ make -# make install - - -Dependencies ------------- - -- C compiler (C99). -- libc (tested with OpenBSD, FreeBSD, NetBSD, Linux: glibc and musl). -- libgit2 (v0.22+). -- POSIX make (optional). - - -Documentation -------------- - -See man pages: stagit(1) and stagit-index(1). - - -Building a static binary ------------------------- - -It may be useful to build static binaries, for example to run in a chroot. - -It can be done like this at the time of writing (v0.24): - -cd libgit2-src - -# change the options in the CMake file: CMakeLists.txt -BUILD_SHARED_LIBS to OFF (static) -CURL to OFF (not needed) -USE_SSH OFF (not needed) -THREADSAFE OFF (not needed) -USE_OPENSSL OFF (not needed, use builtin) - -mkdir -p build && cd build -cmake ../ -make -make install - - -Extract owner field from git config ------------------------------------ - -A way to extract the gitweb owner for example in the format: - - [gitweb] - owner = Name here - -Script: - - #!/bin/sh - awk '/^[ ]*owner[ ]=/ { - sub(/^[^=]*=[ ]*/, ""); - print $0; - }' - - -Set clone URL for a directory of repos --------------------------------------- - #!/bin/sh - cd "$dir" - for i in *; do - test -d "$i" && echo "git://git.codemadness.org/$i" > "$i/url" - done - - -Update files on git push ------------------------- - -Using a post-receive hook the static files can be automatically updated. -Keep in mind git push -f can change the history and the commits may need -to be recreated. This is because stagit checks if a commit file already -exists. It also has a cache (-c) option which can conflict with the new -history. See stagit(1). - -git post-receive hook (repo/.git/hooks/post-receive): - - #!/bin/sh - # detect git push -f - force=0 - while read -r old new ref; do - hasrevs=$(git rev-list "$old" "^$new" | sed 1q) - if test -n "$hasrevs"; then - force=1 - break - fi - done - - # remove commits and .cache on git push -f - #if test "$force" = "1"; then - # ... - #fi - - # see example_create.sh for normal creation of the files. - - -Create .tar.gz archives by tag ------------------------------- - #!/bin/sh - name="stagit" - mkdir -p archives - git tag -l | while read -r t; do - f="archives/${name}-$(echo "${t}" | tr '/' '_').tar.gz" - test -f "${f}" && continue - git archive \ - --format tar.gz \ - --prefix "${t}/" \ - -o "${f}" \ - -- \ - "${t}" - done - - -Features --------- - -- Log of all commits from HEAD. -- Log and diffstat per commit. -- Show file tree with linkable line numbers. -- Show references: local branches and tags. -- Detect README and LICENSE file from HEAD and link it as a webpage. -- Detect submodules (.gitmodules file) from HEAD and link it as a webpage. -- Atom feed of the commit log (atom.xml). -- Atom feed of the tags/refs (tags.xml). -- Make index page for multiple repositories with stagit-index. -- After generating the pages (relatively slow) serving the files is very fast, - simple and requires little resources (because the content is static), only - a HTTP file server is required. -- Usable with text-browsers such as dillo, links, lynx and w3m. - - -Cons ----- - -- Not suitable for large repositories (2000+ commits), because diffstats are - an expensive operation, the cache (-c flag) is a workaround for this in - some cases. -- Not suitable for large repositories with many files, because all files are - written for each execution of stagit. This is because stagit shows the lines - of textfiles and there is no "cache" for file metadata (this would add more - complexity to the code). -- Not suitable for repositories with many branches, a quite linear history is - assumed (from HEAD). - - In these cases it is better to just use cgit or possibly change stagit to - run as a CGI program. - -- Relatively slow to run the first time (about 3 seconds for sbase, - 1500+ commits), incremental updates are faster. -- Does not support some of the dynamic features cgit has, like: - - Snapshot tarballs per commit. - - File tree per commit. - - History log of branches diverged from HEAD. - - Stats (git shortlog -s). - - This is by design, just use git locally. +┌─────┐ +│~sgw~│ +└─────┘ + +Static Git Webpage (Generator). +Fork of Hiltjo Posthuma's Stagit: www.codemadness.org/git/stagit/ + +Build: + Run "make" to create the sgw binary. Then, run "make install" as root + to install, if that's what you want. + +Documentation/Usage: + Calling sgw with no options will print a brief usage guide. For + further info, read the man page via "mandoc sgw.1" or just + "man sgw" (if you've installed sgw). + +Dependency: + - libgit2 (v0.22+) + +Tool Features: + - Specify output directory + - Generate repository pages, index page, or both. + +Webpage Features: + - customizable via config.h: + - header/footer + - webpage structure: link destinations, page locations, etc. + - nested directory pages (or not) + - Presents log of commits from HEAD + - Presents file tree at HEAD + - Text files have linkable line numbers + - Binary files have preview (via object tag) and download link. + - Detects README, LICENSE, and submodules and links them in repo page headers. + - Atom feeds for the commits (atom.xml) and tags/refs (tags.xml). + - Compatible with text-centric browsers (dillo, links, lynx and w3m). + +TODO: + See TODO. diff --git a/TODO b/TODO @@ -1,33 +1,13 @@ -STUFF I WANT TO CHANGE ABOUT THIS -[ ] 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) - -[ ] add warnings on missing decorations - - flag that skips repos with incomplete metadata - - (possible mechanism to hide private repos) - -[ ] create config.h file - - reused format strings (such as header) - -[ ] handle projects with large numbers of files better - - show "{total LoC}L + {total binary B}B" for each directory - - (VERY IMPORTANT to still punish large projects) - - - NEW FEATURES -[ ] add lines of code totals on files page -[ ] add lines of code to top of each file's page -[ ] allow download of arbitrary binary files -[ ] display images on their file page (png, jpg, webp, which others?) -[ ] / add support for projects with folders in them -[ ] remove leading dot from links, so they can be served more easily - - END NEW FEATURES - -[ ] Redo caching, to never repeat work - - but it needs to also not increase the complexity. +[ ] Separate repo page generation out of main.c +[ ] clean up directory configurations (whether to show all files on files.html) + - allow creation of both dir pages, and "all files" page? +[ ] enforce lack of trailing/leading slashes in -o and -b options +[ ] Remove globals / replace them with passed structs +[ ] Rewrite man page +[ ] Caching + - but it needs to also not increase complexity. - it's ok to duplicate storage - - perhaps: save every reusable intermediate into a file. - then, check for changes and reuse if valid. + - method: save every reusable intermediate into a file. + then, check for changes and reuse if there aren't any. + 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,41 @@ +/* Only macros! :) */ + +/********* global configurations ************/ +/* url of the stylesheet */ +#define STYLE_HREF "/style.css" +/* whether to generate a page for each dir in a repo */ +#define DIR_PAGES 1 +/* where to truncate the commit history */ +#define MAX_COMMITS 10 +/* where to check for license files */ +#define LICENSE_FILES "HEAD:LICENSE", "HEAD:LICENSE.md", \ + "HEAD:LICENSE.txt", "HEAD:COPYING" +/* where to check for readme files */ +#define README_FILES "HEAD:README", "HEAD:README.md" + +/********* index page configurations ************/ +/* the relative path to save to index page to */ +#define INDEX_DEST "index.html" +/* what projects' titles link to */ +#define INDEX_REPO_LINK readme == NULL ? "files.html" : "files/%s.html", readme +/* title of the index page */ +#define INDEX_TITLE "Emma's Repositories" +/* shown at the top of index page */ +#define INDEX_HEADER "<span>" \ + "<a href=\"https://y1.nz\">y1.nz</a>" \ +"</span>" + +/********* repository page configurations ************/ +/* the relative path to save each repository's page to */ +#define PAGE_DEST "%s", repo_name +/* shown at the top of each repo page */ +#define PAGE_HEADER "<span> <a href=\"/\">git.y1.nz</a> </span>" +/* label shown before download/clone link */ +#define PAGE_ACCESS_LABEL "download" +/* shown at the bottom of each repo page */ +#define PAGE_FOOTER "<p><i>" \ + "This webpage is intended to be an accessible preview of this " \ + "repository. To get a fuller picture, clone it and use the " \ + "git CLI." \ +"</i></p>" + 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 @@ -0,0 +1,86 @@ +#include <sys/stat.h> +#include <sys/types.h> + +#include <stddef.h> +#include <stdio.h> +#include <err.h> +#include <errno.h> +#include <string.h> +#include <limits.h> + +typedef struct _path { + char *buf; + size_t buf_size; + int index; +} Path; + +void +join_path(char *buf, size_t buf_size, const char *path, const char *path2) +{ + int r; + char * separator = ""; + if(path[0] && path[strlen(path) - 1] != '/') + separator = "/"; + + r = snprintf(buf, buf_size, "%s%s%s", path, separator, path2); + if (r < 0 || (size_t) r >= buf_size) + errx(1, "path truncated: '%s%s%s'", path, separator, path2); +} + +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...?"); +} + +int +mkdirp(const char *path) +{ + const char *p; + char tmp[PATH_MAX]; + char *t; + + // TODO use strchr for this? + 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; +} + +/* populates buf with the filepath that was opened. */ +FILE * +fopen_w(const char *name) +{ + FILE *ret; + + mkdirp(name); // TODO minimize calls to this + if (!(ret = fopen(name, "w"))) err(1, "fopen: '%s'", name); + return ret; +} + +/* Handle read or write errors for a FILE * stream */ +void +check_file_error(FILE *fp, const char *path, int mode) +{ + if (mode == 'r' && ferror(fp)) + errx(1, "read error: %s", path); + else if (mode == 'w' && (fflush(fp) || ferror(fp))) + errx(1, "write error: %s", path); +} diff --git a/files.h b/files.h @@ -0,0 +1,10 @@ + +int mkdirp(const char *path); + +FILE * fopen_w(const char *name); + +void check_file_error(FILE *fp, const char *name, int mode); + +void join_path(char *buf, size_t bufsiz, const char *path, const char *path2); + +void combine_paths(char *ret, const char *path, const char *name); diff --git a/index.c b/index.c @@ -0,0 +1,127 @@ +#include <err.h> +#include <limits.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> + +#include <git2.h> + +#include "config.h" + +// TODO extract these to a shared location (main.h?) +#define SPUTF(f, s) fputs((s), (f)) +#define put_xml(fp, s) put_xml_len((fp), (s), strlen(s)) + +static char owner[255]; + +FILE * git_fopen(char *path, size_t path_size, const char *name); +void put_xml_len(FILE *fp, const char *data, size_t len); +void put_percent_encoded(FILE *fp, const char *data); +void printtimeshort(FILE *fp, const git_time *intime); +void check_file_error(FILE *fp, const char *path, int mode); +void put_absolute_path(FILE *fp, char *name); + +void +put_index_header(FILE *fp) +{ + SPUTF( + fp, + "<!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>" + ); + put_xml(fp, INDEX_TITLE); + // TODO make links relative to base_url or ? + SPUTF( + fp, + "</title>" "\n" "<link rel=\"icon\" type=\"image/png\" href=\"favicon.png\" />" "\n" + "<link rel=\"stylesheet\" type=\"text/css\" href=\"style.css\" />" "\n" + "</head>" "\n" "<body>" "\n" + ); + fprintf(fp, INDEX_HEADER); + SPUTF(fp, "<table><tbody>" "<tr><td>" "<h1>"); + put_xml(fp, INDEX_TITLE); + SPUTF( + fp, + "</h1>" "</td></tr>" "</tbody>" "</table>" "\n" "<hr/>" "\n" + "<div id=\"content\">" "\n" "<table id=\"index\"><thead>" "\n" + "<tr>" "<td><b>Name</b></td>" "<td><b>Description</b></td>" + "<td><b>Last commit</b></td></tr>" + "</thead><tbody>" "\n" + ); +} + +void +put_index_footer(FILE *fp) +{ + SPUTF( + fp, + "</tbody>" "\n" "</table>" "\n" "</div>" "\n" + "</body>" "\n" "</html>" "\n" + ); +} + +int +put_index_log( + FILE *fp, + git_repository *repo, + const char *description, + const char *repo_name, + const char *readme +) { + git_commit *commit = NULL; + const git_signature *author; + git_revwalk *w = NULL; + git_oid id; + int ret = 0; + + git_revwalk_new(&w, repo); + git_revwalk_push_head(w); + + if (git_revwalk_next(&id, w) || + git_commit_lookup(&commit, repo, &id)) { + ret = -1; + goto err; + } + + author = git_commit_author(commit); + + // TODO make this link reflect the repo page configuration + SPUTF(fp, "<tr><td><a href=\""); + put_absolute_path(fp, ""); + fprintf(fp, INDEX_REPO_LINK); + SPUTF(fp, "\">"); + put_xml(fp, repo_name); + SPUTF(fp, "</a></td><td>"); + put_xml(fp, description); + SPUTF(fp, "</td><td>"); + if (author) printtimeshort(fp, &(author->when)); + SPUTF(fp, "</td></tr>"); + + git_commit_free(commit); + +err: + git_revwalk_free(w); + + return ret; +} + +void +find_owner() +{ + FILE *fp; + char path[PATH_MAX]; + fp = git_fopen(path, sizeof(path), ".git/owner"); + + owner[0] = '\0'; + if (fp) { + if (!fgets(owner, sizeof(owner), fp)) + owner[0] = '\0'; + check_file_error(fp, path, 'r'); + fclose(fp); + owner[strcspn(owner, "\n")] = '\0'; + } +} diff --git a/main.c b/main.c @@ -0,0 +1,1868 @@ +#include <sys/stat.h> +#include <sys/types.h> + +#include <err.h> +#include <errno.h> +#include <libgen.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> +#include <limits.h> + +#include <git2.h> + +#include "compat.h" +#include "opt.h" +#include "files.h" + +#define CPUT(f, c) putc((c), (f)) +#define put_xml(fp, s) put_xml_len((fp), (s), strlen(s)) +#define SPUTF(f, s) fputs((s), (f)) +#define LENGTH(s) (sizeof(s)/sizeof(*s)) + +#include "config.h" + +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 *repo_dir; + +static const char *license_files[] = { LICENSE_FILES }; +static const char *readme_files[] = { README_FILES }; + +// TODO extract these into a struct. LMFAO +static char *name = ""; +static char *repo_name = ""; +static char description[255]; +static char clone_url[1024]; +static char *submodules; +static const char *license; +static const char *readme; + +/* bad globals for index.... */ +// TODO move this stuff to index.h? +static FILE *index_fp; +static char index_fname[255] = "index.html"; + +void find_owner(); +int put_index_log(FILE *fp, git_repository *repo, const char *description, const char *repo_name, const char *readme); +void put_index_header(FILE *fp); +void put_index_footer(FILE *fp); + +void +get_page_path(char *buf, size_t buf_size, const char *dir, char *name) +{ + int index = 0, chars = 0; + + // TODO tweak this so that it correctly places slashes between names + chars = snprintf(buf + index, buf_size - index, "%s", dir); + if (chars < 0 || (size_t) (index += chars) >= buf_size) + goto page_path_err; + + if (chars != 0) buf[index++] = '/'; + + chars = snprintf(buf + index, buf_size - index, PAGE_DEST); + if (chars < 0 || (size_t) (index += chars) >= buf_size) + goto page_path_err; + + if (chars != 0) buf[index++] = '/'; + + chars = snprintf(buf + index, buf_size - index, "%s", name); + if (chars < 0 || (size_t) (index += chars) >= buf_size) + goto page_path_err; + + return; + +page_path_err: + errx(1, "page path truncated! '%s' / '%s'" "\n", dir, name); +} + +void +get_index_page_path(char *buf, size_t buf_size, const char *dir, char *name) +{ + int index = 0, chars = 0; + + // TODO tweak this so that it correctly places slashes between names + chars = snprintf(buf + index, buf_size - index, "%s", dir); + if (chars < 0 || (size_t) (index += chars) >= buf_size) + goto page_path_err; + + const char *format = dir[0] == '\0' ? "%s" : "/%s"; + chars = snprintf(buf + index, buf_size - index, format, name); + if (chars < 0 || (size_t) (index += chars) >= buf_size) + goto page_path_err; + + printf("Index page path: %s" "\n", buf); + + return; + +page_path_err: + errx(1, "page path truncated! '%s' / '%s'" "\n", dir, name); +} + +FILE * +git_fopen(char *path, int path_size, const char *file) +{ + FILE *ret; + + /* try dir/(whatever), */ + /* (+5 removes the leading '.git/'.) */ + join_path(path, path_size, repo_dir, file + 5); + if ((ret = fopen(path, "r"))) return ret; + + /* then try dir/.git/(whatever). */ + join_path(path, path_size, repo_dir, file); + return fopen(path, "r"); +} + +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; + readme = NULL; + 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("<", fp); break; + case '>': fputs(">", fp); break; + case '\'': fputs("'", fp); break; + case '&': fputs("&", fp); break; + case '"': fputs(""", fp); break; + default: putc(*s, fp); + } +} + +void +put_xml_len(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) +{ + if (opt.base_url[0] != '\0') + fprintf(fp, "/%s", opt.base_url); + CPUT(fp, '/'); + fprintf(fp, PAGE_DEST); /* should have no leading/trailing slashes */ + 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); + if (title[0] && repo_name[0]) + fputs(" - ", fp); + put_xml(fp, repo_name); + if (description[0]) + fputs(" - ", fp); + put_xml(fp, 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); + 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); + 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, PAGE_HEADER); + + SPUTF(fp, "<table>" "<tr>" "<td><h1>"); + put_xml(fp, repo_name); + SPUTF(fp, "</h1></td>" "<td><span class=\"desc\">"); + put_xml(fp, description); + SPUTF(fp, "</span></td>" "</tr>"); + if (clone_url[0]) { + SPUTF(fp, "<tr class=\"url\">" "<td></td>" "<td>"); + fprintf(fp, PAGE_ACCESS_LABEL); + SPUTF(fp, ": <a href=\""); + put_xml(fp, clone_url); /* not percent encoded */ + SPUTF(fp, "\">"); + put_xml(fp, clone_url); + 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, PAGE_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); + SPUTF(fp, " <<a href=\"mailto:"); + put_xml(fp, ci->author->email); /* not percent encoded */ + SPUTF(fp, "\">"); + put_xml(fp, ci->author->email); + SPUTF(fp, "</a>>\n<b>Date:</b> "); + printtime(fp, &(ci->author->when)); + CPUT(fp, '\n'); + } + if (ci->msg) { + CPUT(fp, '\n'); + put_xml(fp, 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); + if (strcmp(delta->old_file.path, delta->new_file.path)) { + fputs(" -> ", fp); + put_xml(fp, 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); + fprintf(fp, " b/<a id=\"h%zu\" href=\"#h%zu\">", i, i); + put_xml(fp, 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_len(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); + SPUTF(fp, "</a>"); + } + SPUTF(fp, "</td><td>"); + if (ci->author) put_xml(fp, 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"); +} + +void +write_commit_page(char *name, struct CommitInfo *ci) +{ + char fname[PATH_MAX]; + get_page_path(fname, sizeof(fname), opt.out_dir, name); + FILE *fp = fopen_w(fname); + // [old] fpfile = efopen(path, "w"); + + put_header(fp, ci->summary); + + SPUTF(fp, "<pre>"); + put_show_file(fp, ci); + SPUTF(fp, "</pre>" "\n"); + + put_footer(fp); + check_file_error(fp, fname, 'w'); + fclose(fp); +} + +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]; + size_t remcommits = 0; + int r; + int commits_left = MAX_COMMITS; + + git_revwalk_new(&w, repo); + git_revwalk_push(w, oid); + + // TODO avoid processing very large commits + while (!git_revwalk_next(&id, w)) { + 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 (!commits_left) { + 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 (commits_left != 0) { + put_log_line(fp, ci); + if (commits_left > 0) + commits_left--; + } + + /* check if file exists if so skip it */ + if (r) write_commit_page(path, ci); + +err: + commit_info_free(ci); + } + git_revwalk_free(w); + + if (commits_left == 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_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) put_log(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); + SPUTF(fp, "] "); + } + put_xml(fp, 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); + SPUTF(fp, "</name>" "\n" "<email>"); + put_xml(fp, 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); + SPUTF(fp, " <"); + put_xml(fp, ci->author->email); + SPUTF(fp, ">" "\n" "Date: "); + printtime(fp, &(ci->author->when)); + CPUT(fp, '\n'); + } + if (ci->msg) { + CPUT(fp, '\n'); + put_xml(fp, 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, repo_name); + SPUTF(fp, ", branch HEAD</title>" "\n" "<subtitle>"); + put_xml(fp, 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! */ + char fname[PATH_MAX]; + get_page_path(fname, sizeof(fname), opt.out_dir, dest); + FILE *fp = fopen_w(fname); + + fwrite(raw, size, 1, fp); + check_file_error(fp, fname, '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 (!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); + return; + } + } + + + fprintf(fp, "<p>%s</p>", name); +} + +void +put_binary_blob_stub(FILE *fp, const char *file_url) +{ + SPUTF(fp, "<p>Binary file: <a href=\""); + put_absolute_path(fp, "files/"); + fprintf(fp, "%s\">raw</a></p>" "\n", file_url); + SPUTF(fp, "<object data=\""); + put_absolute_path(fp, "files/"); + fprintf(fp, "%s\">", file_url); + SPUTF(fp, "no preview available.</object></p>" "\n"); +} + +struct Weight +write_blob( + git_blob *blob, + char *filename, + const char *entrypath, + const char *name +) { + char fname[PATH_MAX]; + FILE *fp; + struct Weight ret; + + ret.lines = 0; + ret.bytes = 0; + + get_page_path(fname, sizeof(fname), opt.out_dir, filename); + fp = fopen_w(fname); + + put_header(fp, name); + put_file_title(fp, entrypath, 0); + + if (git_blob_is_binary(blob)) { + put_binary_blob_stub(fp, entrypath); // name); + write_binary_file(entrypath, blob); + ret.bytes = git_blob_rawsize(blob); + } else { + ret.lines = put_file_html(fp, blob); + } + + put_footer(fp); + check_file_error(fp, fname, '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); + else put_xml(fp, 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); + 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((git_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); + SPUTF(fp, "</a> @ "); + git_oid_tostr(oid, sizeof(oid), git_tree_entry_id(entry)); + put_xml(fp, 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; + + 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; + + 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) +{ + struct Weight ret; + char fname[PATH_MAX] = ""; + FILE *fp; + + get_page_path(fname, sizeof(fname), opt.out_dir, (char *) filename); + fp = fopen_w(fname); + + 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 (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, fname, '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 +put_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); + 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); + 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) +{ + FILE *fp; + char fname[PATH_MAX]; + + get_page_path(fname, sizeof(fname), opt.out_dir, "log.html"); + fp = fopen_w(fname); + + put_log_html(fp, head); + + check_file_error(fp, fname, 'w'); + fclose(fp); +} + +void +write_refs_page() +{ + FILE *fp; + char fname[PATH_MAX]; + + get_page_path(fname, sizeof(fname), opt.out_dir, "refs.html"); + fp = fopen_w(fname); + + put_header(fp, "Refs"); + put_refs(fp); + put_footer(fp); + + check_file_error(fp, fname, 'w'); + fclose(fp); +} + +void +write_feed() +{ + FILE *fp; + char fname[PATH_MAX]; + + get_page_path(fname, sizeof(fname), opt.out_dir, "atom.xml"); + fp = fopen_w(fname); + + write_atom(fp, 1); + + check_file_error(fp, fname, 'w'); + fclose(fp); +} + +void +write_releases_feed() +{ + FILE *fp; + char fname[PATH_MAX]; + + get_page_path(fname, sizeof(fname), opt.out_dir, "tags.xml"); + fp = fopen_w(fname); + + write_atom(fp, 0); + + check_file_error(fp, fname, 'w'); + fclose(fp); +} + +void +find_submodules() +{ + git_object *obj = NULL; + if ( + !git_revparse_single(&obj, repo, "HEAD:.gitmodules") && + git_object_type(obj) == GIT_OBJ_BLOB + ) + submodules = ".gitmodules"; + git_object_free(obj); +} + +void +find_description() +{ + char path[PATH_MAX]; + FILE *fp; + if ((fp = git_fopen(path, sizeof(path), ".git/description"))) { + if (!fgets(description, sizeof(description), fp)) + description[0] = '\0'; + check_file_error(fp, path, 'r'); + fclose(fp); + } +} + +void +find_clone_url() +{ + char path[PATH_MAX]; + FILE *fp; + if ((fp = git_fopen(path, sizeof(path), ".git/url"))) { + if (!fgets(clone_url, sizeof(clone_url), fp)) + clone_url[0] = '\0'; + check_file_error(fp, path, 'r'); + fclose(fp); + + // TODO ??? + clone_url[strcspn(clone_url, "\n")] = '\0'; + } +} + +void +find_repo_name() +{ + char abs_dir[PATH_MAX + 1]; + char *p; + + /* set abs_dir */ + if (!realpath(repo_dir, abs_dir)) + err(1, "realpath"); + + if ((name = strrchr(abs_dir, '/'))) name++; + else name = ""; + + if (!(repo_name = strdup(name))) err(1, "strdup"); + if ((p = strrchr(repo_name, '.'))) + if (!strcmp(p, ".git")) + *p = '\0'; + + if (repo_name[0] == '\0') + repo_name = "untitled"; +} + +int +write_repo_pages(const char* dir) +{ + + // TODO fully stop using globals like this.... + repo_dir = dir; + +#ifdef __OpenBSD__ + if (unveil(repo_dir, "r") == -1) err(1, "unveil: %s", repo_dir); + if (unveil(".", "rwc") == -1) err(1, "unveil: ."); + + /* + if (cachefile) { + if (unveil(cachefile, "rwc") == -1) err(1, "unveil: %s", 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 + + /* open the git repo */ + int result = git_repository_open_ext( + &repo, repo_dir, GIT_REPOSITORY_OPEN_NO_SEARCH, NULL + ); + if (result < 0) { + fprintf(stderr, "fatal: cannot open repository '%s'" "\n", dir); + return 1; + } + + /* find HEAD */ + const git_oid *head = NULL; + git_object *obj = NULL; + if (!git_revparse_single(&obj, repo, "HEAD")) + head = git_object_id(obj); + git_object_free(obj); + + /* set global vars... */ + // TODO repo = open_repo(); + find_repo_name(); + find_description(); + find_clone_url(); + find_license(); + find_readme(); + find_submodules(); + if (opt.index) find_owner(); + + if (opt.index) { + put_index_log(index_fp, repo, description, repo_name, readme); + } + + if (opt.pages) { + write_log_page(head); + walk_git_tree(head); + write_refs_page(); + write_feed(); + write_releases_feed(); + } + + /* clean up */ + // TODO close_repo(); + git_repository_free(repo); + + return 0; +} + +void +begin_index() +{ + get_index_page_path( + index_fname, + sizeof(index_fname), + opt.out_dir, + INDEX_DEST + ); + index_fp = fopen_w(index_fname); + put_index_header(index_fp); +} + +void +end_index() +{ + put_index_footer(index_fp); + check_file_error(index_fp, index_fname, 'w'); + fclose(index_fp); +} + +void +init_lib_git() +{ + int i; + + /* 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); +} + +int +main(int argc, const char *argv[]) +{ + int i, result; + + if (parse_opts(&opt, argc, argv) == OPT_FAIL) { + PRINT_USAGE(argv[0]); + return 1; + } + + if (!opt.pages && !opt.index) { + printf("Nothing to do, use -i and/or -p." "\n\n"); + PRINT_USAGE(argv[0]); + return 1; + } + + init_lib_git(); + + if (opt.index) begin_index(); + for (i = 0; i < opt.repo_count; i++) { + result = write_repo_pages(opt.repo_dirs[i]); + if(result > 0) return 1; + } + if (opt.index) end_index(); + + git_libgit2_shutdown(); + + return 0; +} diff --git a/opt.c b/opt.c @@ -0,0 +1,68 @@ + +#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 'i': opt->index = 1; break; + case 'p': opt->pages = 1; break; + case 'o': opt->out_dir = next; return OPT_SKIP; + case 'b': opt->base_url = next; return OPT_SKIP; + case 'c': opt->cache = 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(""); + + /* set default values */ + opt->index = 0; + opt->pages = 0; + opt->base_url = ""; + opt->out_dir = ""; + + 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,32 @@ +#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 [-i] [-p] [-o DIR] [-b URL] [-c DIR] REPO [REPO2 [...]]" "\n" \ +" -i: generate index file" "\n" \ +" -p: generate repository pages" "\n" \ +" -o: specify output dir" "\n" \ +" -b: specify base url" "\n" \ +" -c: specify cache dir" "\n" + +#define PRINT_USAGE(argv0) fprintf(stderr, USAGE, argv0); + + +typedef struct _ { + int index; /* 0 or 1 */ + int pages; /* 0 or 1 */ + + const char *cache; + const char *out_dir; + const char *base_url; + + int repo_count; + const char **repo_dirs; +} Opt; + +int parse_opts(Opt *opt, int argc, const char *argv[]); diff --git a/sgw.1 b/sgw.1 @@ -0,0 +1,66 @@ +.Dd August 2, 2021 +.Dt SGW 1 +.Os +.Sh NAME +.Nm sgw +.Nd static git webpage generator +.Sh SYNOPSIS +.Nm +.Op Fl i +.Op Fl p +.Op Fl o Ar outdir +.Op Fl b Ar baseurl +.Ar repo +.Op Ar repo2 Op ... +.Sh DESCRIPTION +.Nm +writes a webpage consisting of index and repository +pages for bare repositories located at +.Ar repo , +.Ar repo2 , +etc. Non-bare repositories can also be used, but will be labelled +as "untitled" wherever their name would have appeared. Its options +are as follows: +.Bl -tag -width Ds +.It Fl i +If present, generate an index page. The exact file this page is +saved to is defined by config.h at compile time. +.It Fl p +If present, generate the specified repositories' pages. +.It Fl o Ar outdir +Write all output files under the +.Ar outdir +directory. The exact destinations of the files are determined by +the paths specified in config.h at compile time. +.It Fl b Ar baseurl +Links within the generated webpages include this path, to allow +the webpage to be served from the +.Ar baseurl +subdirectory. +.El +.Pp +The program will return with a failing status if neither of +.Fl i +and +.Fl p +are specified, or if no repositories are selected. +.Pp +.Nm +will do nothing to attempt to clean up the output it has produced +if it fails after writing files. +.Pp +It does not remove any files from its output directories and +does not make changes to any of the input repositories. +.Pp +It will not, however, indicate any kind of warning before +overwriting the files it has been configured to write its +output into. +.El +.Sh EXIT STATUS +.Ex -std +.Sh SEE ALSO +.Xr git 1 +.Xr +.Sh AUTHORS +.An Emma Weaver Aq Mt emma@y1.nz +.An Based heavily on work by Hiltjo Posthuma Aq Mt hiltjo@codemadness.org 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 @@ -1,258 +0,0 @@ -#include <err.h> -#include <limits.h> -#include <stdio.h> -#include <stdlib.h> -#include <string.h> -#include <time.h> -#include <unistd.h> - -#include <git2.h> - -static git_repository *repo; - -static const char *relpath = ""; - -static char description[255] = "Repositories"; -static char *name = ""; -static char owner[255]; - -/* Handle read or write errors for a FILE * stream */ -void -checkfileerror(FILE *fp, const char *name, int mode) -{ - if (mode == 'r' && ferror(fp)) - errx(1, "read error: %s", name); - else if (mode == 'w' && (fflush(fp) || ferror(fp))) - errx(1, "write error: %s", name); -} - -void -joinpath(char *buf, size_t bufsiz, const char *path, const char *path2) -{ - int r; - - r = snprintf(buf, bufsiz, "%s%s%s", - path, path[0] && path[strlen(path) - 1] != '/' ? "/" : "", path2); - if (r < 0 || (size_t)r >= bufsiz) - errx(1, "path truncated: '%s%s%s'", - path, path[0] && path[strlen(path) - 1] != '/' ? "/" : "", path2); -} - -/* Percent-encode, see RFC3986 section 2.1. */ -void -percentencode(FILE *fp, const char *s, size_t len) -{ - static char tab[] = "0123456789ABCDEF"; - unsigned char uc; - size_t i; - - for (i = 0; *s && i < len; s++, i++) { - uc = *s; - /* NOTE: do not encode '/' for paths or ",-." */ - if (uc < ',' || uc >= 127 || (uc >= ':' && uc <= '@') || - uc == '[' || uc == ']') { - putc('%', fp); - putc(tab[(uc >> 4) & 0x0f], fp); - putc(tab[uc & 0x0f], fp); - } else { - putc(uc, fp); - } - } -} - -/* Escape characters below as HTML 2.0 / XML 1.0. */ -void -xmlencode(FILE *fp, const char *s, size_t len) -{ - size_t i; - - for (i = 0; *s && i < len; s++, i++) { - switch(*s) { - case '<': fputs("<", fp); break; - case '>': fputs(">", fp); break; - case '\'': fputs("'" , fp); break; - case '&': fputs("&", fp); break; - case '"': fputs(""", fp); break; - default: putc(*s, fp); - } - } -} - -void -printtimeshort(FILE *fp, const git_time *intime) -{ - struct tm *intm; - time_t t; - char out[32]; - - t = (time_t)intime->time; - if (!(intm = gmtime(&t))) - return; - strftime(out, sizeof(out), "%Y-%m-%d %H:%M", intm); - fputs(out, fp); -} - -void -writeheader(FILE *fp) -{ - fputs("<!DOCTYPE html>\n" - "<html>\n<head>\n" - "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n" - "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n" - "<title>", fp); - xmlencode(fp, description, strlen(description)); - fprintf(fp, "</title>\n<link rel=\"icon\" type=\"image/png\" href=\"%sfavicon.png\" />\n", relpath); - fprintf(fp, "<link rel=\"stylesheet\" type=\"text/css\" href=\"%sstyle.css\" />\n", relpath); - fputs("</head>\n<body>\n", fp); - fprintf(fp, "<table>\n<tr><td><img src=\"%slogo.png\" alt=\"\" width=\"32\" height=\"32\" /></td>\n" - "<td><span class=\"desc\">", relpath); - xmlencode(fp, description, strlen(description)); - fputs("</span></td></tr><tr><td></td><td>\n" - "</td></tr>\n</table>\n<hr/>\n<div id=\"content\">\n" - "<table id=\"index\"><thead>\n" - "<tr><td><b>Name</b></td><td><b>Description</b></td><td><b>Owner</b></td>" - "<td><b>Last commit</b></td></tr>" - "</thead><tbody>\n", fp); -} - -void -writefooter(FILE *fp) -{ - fputs("</tbody>\n</table>\n</div>\n</body>\n</html>\n", fp); -} - -int -writelog(FILE *fp) -{ - git_commit *commit = NULL; - const git_signature *author; - git_revwalk *w = NULL; - git_oid id; - char *stripped_name = NULL, *p; - int ret = 0; - - git_revwalk_new(&w, repo); - git_revwalk_push_head(w); - - if (git_revwalk_next(&id, w) || - git_commit_lookup(&commit, repo, &id)) { - ret = -1; - goto err; - } - - author = git_commit_author(commit); - - /* strip .git suffix */ - if (!(stripped_name = strdup(name))) - err(1, "strdup"); - if ((p = strrchr(stripped_name, '.'))) - if (!strcmp(p, ".git")) - *p = '\0'; - - fputs("<tr><td><a href=\"", fp); - percentencode(fp, stripped_name, strlen(stripped_name)); - fputs("/log.html\">", fp); - xmlencode(fp, stripped_name, strlen(stripped_name)); - fputs("</a></td><td>", fp); - xmlencode(fp, description, strlen(description)); - fputs("</td><td>", fp); - xmlencode(fp, owner, strlen(owner)); - fputs("</td><td>", fp); - if (author) - printtimeshort(fp, &(author->when)); - fputs("</td></tr>", fp); - - git_commit_free(commit); -err: - git_revwalk_free(w); - free(stripped_name); - - return ret; -} - -int -main(int argc, char *argv[]) -{ - FILE *fp; - char path[PATH_MAX], repodirabs[PATH_MAX + 1]; - const char *repodir; - int i, ret = 0; - - if (argc < 2) { - fprintf(stderr, "usage: %s [repodir...]\n", argv[0]); - return 1; - } - - /* do not search outside the git repository: - GIT_CONFIG_LEVEL_APP is the highest level currently */ - git_libgit2_init(); - for (i = 1; i <= GIT_CONFIG_LEVEL_APP; i++) - git_libgit2_opts(GIT_OPT_SET_SEARCH_PATH, i, ""); - /* do not require the git repository to be owned by the current user */ - git_libgit2_opts(GIT_OPT_SET_OWNER_VALIDATION, 0); - -#ifdef __OpenBSD__ - if (pledge("stdio rpath", NULL) == -1) - err(1, "pledge"); -#endif - - writeheader(stdout); - - for (i = 1; i < argc; i++) { - repodir = argv[i]; - if (!realpath(repodir, repodirabs)) - err(1, "realpath"); - - if (git_repository_open_ext(&repo, repodir, - GIT_REPOSITORY_OPEN_NO_SEARCH, NULL)) { - fprintf(stderr, "%s: cannot open repository\n", argv[0]); - ret = 1; - continue; - } - - /* use directory name as name */ - if ((name = strrchr(repodirabs, '/'))) - name++; - else - name = ""; - - /* read description or .git/description */ - joinpath(path, sizeof(path), repodir, "description"); - if (!(fp = fopen(path, "r"))) { - joinpath(path, sizeof(path), repodir, ".git/description"); - fp = fopen(path, "r"); - } - description[0] = '\0'; - if (fp) { - if (!fgets(description, sizeof(description), fp)) - description[0] = '\0'; - checkfileerror(fp, "description", 'r'); - fclose(fp); - } - - /* read owner or .git/owner */ - joinpath(path, sizeof(path), repodir, "owner"); - if (!(fp = fopen(path, "r"))) { - joinpath(path, sizeof(path), repodir, ".git/owner"); - fp = fopen(path, "r"); - } - owner[0] = '\0'; - if (fp) { - if (!fgets(owner, sizeof(owner), fp)) - owner[0] = '\0'; - checkfileerror(fp, "owner", 'r'); - fclose(fp); - owner[strcspn(owner, "\n")] = '\0'; - } - writelog(stdout); - } - writefooter(stdout); - - /* cleanup */ - git_repository_free(repo); - git_libgit2_shutdown(); - - checkfileerror(stdout, "<stdout>", 'w'); - - return ret; -} diff --git a/stagit.1 b/stagit.1 @@ -1,125 +0,0 @@ -.Dd August 2, 2021 -.Dt STAGIT 1 -.Os -.Sh NAME -.Nm stagit -.Nd static git page generator -.Sh SYNOPSIS -.Nm -.Op Fl c Ar cachefile -.Op Fl l Ar commits -.Op Fl u Ar baseurl -.Ar repodir -.Sh DESCRIPTION -.Nm -writes HTML pages for the repository -.Ar repodir -to the current directory. -.Pp -The options are as follows: -.Bl -tag -width Ds -.It Fl c Ar cachefile -Cache the entries of the log page up to the point of -the last commit. -The -.Ar cachefile -will store the last commit id and the entries in the HTML table. -It is up to the user to make sure the state of the -.Ar cachefile -is in sync with the history of the repository. -.It Fl l Ar commits -Write a maximum number of -.Ar commits -to the log.html file only. -However the commit files are written as usual. -.It Fl u Ar baseurl -Base URL to make links in the Atom feeds absolute. -For example: "https://git.codemadness.org/stagit/". -.El -.Pp -The options -.Fl c -and -.Fl l -cannot be used at the same time. -.Pp -The following files will be written: -.Bl -tag -width Ds -.It atom.xml -Atom XML feed of the last 100 commits. -.It tags.xml -Atom XML feed of the tags. -.It files.html -List of files in the latest tree, linking to the file. -.It log.html -List of commits in reverse chronological applied commit order, each commit -links to a page with a diffstat and diff of the commit. -.It refs.html -Lists references of the repository such as branches and tags. -.El -.Pp -For each entry in HEAD a file will be written in the format: -file/filepath.html. -This file will contain the textual data of the file prefixed by line numbers. -The file will have the string "Binary file" if the data is considered to be -non-textual. -.Pp -For each commit a file will be written in the format: -commit/commitid.html. -This file will contain the diffstat and diff of the commit. -It will write the string "Binary files differ" if the data is considered to -be non-textual. -Too large diffs will be suppressed and a string -"Diff is too large, output suppressed" will be written. -.Pp -When a commit HTML file exists it won't be overwritten again, note that if -you've changed -.Nm -or changed one of the metadata files of the repository it is recommended to -recreate all the output files because it will contain old data. -To do this remove the output directory and -.Ar cachefile , -then recreate the files. -.Pp -The basename of the directory is used as the repository name. -The suffix ".git" is removed from the basename, this suffix is commonly used -for "bare" repos. -.Pp -The content of the follow files specifies the metadata for each repository: -.Bl -tag -width Ds -.It .git/description or description (bare repo). -description -.It .git/owner or owner (bare repo). -owner of repository -.It .git/url or url (bare repo). -primary clone URL of the repository, for example: -git://git.codemadness.org/stagit -.El -.Pp -When a README or LICENSE file exists in HEAD or a .gitmodules submodules file -exists in HEAD a direct link in the menu is made. -.Pp -For changing the style of the page you can use the following files: -.Bl -tag -width Ds -.It favicon.png -favicon image. -.It logo.png -32x32 logo. -.It style.css -CSS stylesheet. -.El -.Sh EXIT STATUS -.Ex -std -.Sh EXAMPLES -.Bd -literal -mkdir -p htmlroot/htmlrepo1 && cd htmlroot/htmlrepo1 -stagit path/to/gitrepo1 -# repeat for other repositories. -.Ed -.Pp -To update the HTML files when the repository is changed a git post-receive hook -can be used, see the file example_post-receive.sh for an example. -.Sh SEE ALSO -.Xr stagit-index 1 -.Sh AUTHORS -.An Hiltjo Posthuma Aq Mt hiltjo@codemadness.org diff --git a/stagit.c b/stagit.c @@ -1,1566 +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 LEN(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; -}; - -static git_repository *repo; - -/* flags + arguments */ -static const char *baseurl = ""; /* base URL to make absolute RSS/Atom URI */ -static const char *relpath = ""; -static const char *repodir; - -static char *name = ""; -static char *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 */ -// TODO yuckyyy -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 -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; -} - -FILE * -efopen(const char *filename, const char *flags) -{ - FILE *fp; - - if (!(fp = fopen(filename, flags))) - err(1, "fopen: '%s'", filename); - - return fp; -} - -/* Percent-encode, see RFC3986 section 2.1. */ -void -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("<", fp); break; - case '>': fputs(">", fp); break; - case '\'': fputs("'", fp); break; - case '&': fputs("&", fp); break; - case '"': fputs(""", 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) -{ - char tmp[PATH_MAX], *p; - - if (strlcpy(tmp, path, sizeof(tmp)) >= sizeof(tmp)) - errx(1, "path truncated: '%s'", path); - for (p = tmp + (tmp[0] == '/'); *p; p++) { - if (*p != '/') - continue; - *p = '\0'; - if (mkdir(tmp, S_IRWXU | S_IRWXG | S_IRWXO) < 0 && errno != EEXIST) - return -1; - *p = '/'; - } - if (mkdir(tmp, S_IRWXU | S_IRWXG | S_IRWXO) < 0 && errno != EEXIST) - return -1; - return 0; -} - -void -printtimez(FILE *fp, const git_time *intime) -{ - struct tm *intm; - time_t t; - char out[32]; - - t = (time_t)intime->time; - if (!(intm = gmtime(&t))) - return; - strftime(out, sizeof(out), "%Y-%m-%dT%H:%M:%SZ", intm); - fputs(out, fp); -} - -void -printtime(FILE *fp, const git_time *intime) -{ - struct tm *intm; - time_t t; - char out[32]; - - t = (time_t)intime->time + (intime->offset * 60); - if (!(intm = gmtime(&t))) - return; - strftime(out, sizeof(out), "%a, %e %b %Y %H:%M:%S", intm); - if (intime->offset < 0) { - fprintf( - fp, - "%s -%02d%02d", - out, - -(intime->offset) / 60, - -(intime->offset) % 60 - ); - } else { - fprintf( - fp, - "%s +%02d%02d", out, - intime->offset / 60, - intime->offset % 60 - ); - } -} - -void -printtimeshort(FILE *fp, const git_time *intime) -{ - struct tm *intm; - time_t t; - char out[32]; - - t = (time_t)intime->time; - if (!(intm = gmtime(&t))) - return; - strftime(out, sizeof(out), "%Y-%m-%d %H:%M", intm); - fputs(out, fp); -} - -void -write_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)); - fprintf(fp, "</title>\n<link rel=\"icon\" type=\"image/png\" href=\"%sfavicon.png\" />\n", relpath); - fputs("<link rel=\"alternate\" type=\"application/atom+xml\" title=\"", fp); - put_xml(fp, name, strlen(name)); - fprintf(fp, " Atom Feed\" href=\"%satom.xml\" />\n", relpath); - fputs("<link rel=\"alternate\" type=\"application/atom+xml\" title=\"", fp); - put_xml(fp, name, strlen(name)); - fprintf(fp, " Atom Feed (tags)\" href=\"%stags.xml\" />\n", relpath); - fprintf(fp, "<link rel=\"stylesheet\" type=\"text/css\" href=\"%sstyle.css\" />\n", relpath); - fputs("</head>" "\n" "<body>" "\n", fp); - - // TODO handle backlink using config.h - fprintf(fp, "<p><a href=\"%s\">home</a></p>" "\n", "/files.html"); - - fputs("<table><tr><td>", fp); - fprintf( - fp, - "<a href=\"../%s\"><img src=\"%slogo.png\" alt=\"\" width=\"32\" height=\"32\" /></a>", - relpath, - relpath - ); - fputs("</td><td><h1>", fp); - put_xml(fp, stripped_name, strlen(stripped_name)); - fputs("</h1><span class=\"desc\">", fp); - put_xml(fp, description, strlen(description)); - fputs("</span></td></tr>", fp); - if (cloneurl[0]) { - fputs("<tr class=\"url\"><td></td><td>git clone <a href=\"", fp); - put_xml(fp, cloneurl, strlen(cloneurl)); /* not percent-encoded */ - fputs("\">", fp); - put_xml(fp, cloneurl, strlen(cloneurl)); - fputs("</a></td></tr>", fp); - } - fputs("<tr><td></td><td>\n", fp); - - if (readme) - fprintf( - fp, - "<a href=\"%sfile/%s.html\">README</a> | ", - relpath, - readme - ); - - fprintf(fp, "<a href=\"%sfiles.html\">Files</a> | ", relpath); - fprintf(fp, "<a href=\"%slog.html\">Log</a> | ", relpath); - fprintf(fp, "<a href=\"%srefs.html\">Refs</a>", relpath); - - if (submodules) - fprintf( - fp, - " | <a href=\"%sfile/%s.html\">Submodules</a>", - relpath, - submodules - ); - - if (license) - fprintf( - fp, - " | <a href=\"%sfile/%s.html\">LICENSE</a>", - relpath, - license - ); - - fputs("</td></tr></table>" "\n", fp); - fputs("<hr/>" "\n", fp); - - fputs("<div id=\"content\">" "\n", fp); -} - -void -write_footer(FILE *fp) -{ - fputs("</div>" "\n" "</body>" "\n" "</html>" "\n", fp); -} - -size_t -write_blob_html(FILE *fp, const git_blob *blob) -{ - size_t n = 0, i, len, prev; - const char *nfmt = "<a href=\"#l%zu\" class=\"line\" id=\"l%zu\">%7zu</a>"; - const char *s = git_blob_rawcontent(blob); - - len = git_blob_rawsize(blob); - fputs("<pre id=\"blob\">" "\n", fp); - - if (len > 0) { - for (i = 0, prev = 0; i < len; i++) { - if (s[i] != '\n') - continue; - n++; - fprintf(fp, nfmt, n, n, n); - put_xml_line(fp, &s[prev], i - prev + 1); - fputs("\n", fp); - prev = i + 1; - } - /* trailing data */ - if ((len - prev) > 0) { - n++; - fprintf(fp, nfmt, n, n, n); - put_xml_line(fp, &s[prev], len - prev); - } - } - - fputs("</pre>" "\n", fp); - - return n; -} - -void -print_commit(FILE *fp, struct CommitInfo *ci) -{ - fprintf(fp, "<b>commit</b> <a href=\"%scommit/%s.html\">%s</a>\n", - relpath, ci->oid, ci->oid); - - if (ci->parentoid[0]) - fprintf(fp, "<b>parent</b> <a href=\"%scommit/%s.html\">%s</a>\n", - relpath, ci->parentoid, ci->parentoid); - - if (ci->author) { - fputs("<b>Author:</b> ", fp); - put_xml(fp, ci->author->name, strlen(ci->author->name)); - fputs(" <<a href=\"mailto:", fp); - put_xml(fp, ci->author->email, strlen(ci->author->email)); /* not percent-encoded */ - fputs("\">", fp); - put_xml(fp, ci->author->email, strlen(ci->author->email)); - fputs("</a>>\n<b>Date:</b> ", fp); - printtime(fp, &(ci->author->when)); - putc('\n', fp); - } - if (ci->msg) { - putc('\n', fp); - put_xml(fp, ci->msg, strlen(ci->msg)); - putc('\n', fp); - } -} - -void -print_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(" -> ", 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=\"%sfile/", i, relpath); - put_percent_encoded(fp, delta->old_file.path, strlen(delta->old_file.path)); - fputs(".html\">", fp); - put_xml(fp, delta->old_file.path, strlen(delta->old_file.path)); - fprintf(fp, "</a> b/<a href=\"%sfile/", relpath); - put_percent_encoded(fp, delta->new_file.path, strlen(delta->new_file.path)); - fprintf(fp, ".html\">"); - put_xml(fp, delta->new_file.path, strlen(delta->new_file.path)); - fprintf(fp, "</a></b>\n"); - - /* check binary data */ - if (delta->flags & GIT_DIFF_FLAG_BINARY) { - fputs("Binary files differ.\n", fp); - continue; - } - - nhunks = git_patch_num_hunks(patch); - for (j = 0; j < nhunks; j++) { - if (git_patch_get_hunk(&hunk, &nhunklines, patch, j)) - break; - - fprintf(fp, "<a href=\"#h%zu-%zu\" id=\"h%zu-%zu\" class=\"h\">", i, j, i, j); - put_xml(fp, hunk->header, hunk->header_len); - fputs("</a>", fp); - - for (k = 0; ; k++) { - if (git_patch_get_line_in_hunk(&line, patch, j, k)) - break; - if (line->old_lineno == -1) - fprintf(fp, "<a href=\"#h%zu-%zu-%zu\" id=\"h%zu-%zu-%zu\" class=\"i\">+", - i, j, k, i, j, k); - else if (line->new_lineno == -1) - fprintf(fp, "<a href=\"#h%zu-%zu-%zu\" id=\"h%zu-%zu-%zu\" class=\"d\">-", - i, j, k, i, j, k); - else - putc(' ', fp); - put_xml_line(fp, line->content, line->content_len); - putc('\n', fp); - if (line->old_lineno == -1 || line->new_lineno == -1) - fputs("</a>", fp); - } - } - } -} - -void -write_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) { - fprintf(fp, "<a href=\"%scommit/%s.html\">", relpath, ci->oid); - put_xml(fp, ci->summary, strlen(ci->summary)); - fputs("</a>", fp); - } - fputs("</td><td>", fp); - if (ci->author) - put_xml(fp, ci->author->name, strlen(ci->author->name)); - fputs("</td><td class=\"num\" align=\"right\">", fp); - fprintf(fp, "%zu", ci->file_count); - fputs("</td><td class=\"num\" align=\"right\">", fp); - fprintf(fp, "+%zu", ci->add_count); - fputs("</td><td class=\"num\" align=\"right\">", fp); - fprintf(fp, "-%zu", ci->del_count); - fputs("</td></tr>\n", fp); -} - -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)) { - relpath = ""; - - if (cachefile && !memcmp(&id, &lastoid, sizeof(id))) - break; - - git_oid_tostr(oidstr, sizeof(oidstr), &id); - r = snprintf(path, sizeof(path), "commit/%s.html", oidstr); - if (r < 0 || (size_t)r >= sizeof(path)) - errx(1, "path truncated: 'commit/%s.html'", oidstr); - r = access(path, F_OK); - - /* optimization: if there are no log lines to write and - the commit file already exists: skip the diffstat */ - if (!nlogcommits) { - remcommits++; - if (!r) - continue; - } - - if (!(ci = 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) { - write_log_line(fp, ci); - if (nlogcommits > 0) - nlogcommits--; - } - - if (cachefile) - write_log_line(wcachefp, ci); - - /* check if file exists if so skip it */ - if (r) { - relpath = "../"; - fpfile = efopen(path, "w"); - write_header(fpfile, ci->summary); - - fputs("<pre>", fpfile); - print_show_file(fpfile, ci); - fputs("</pre>\n", fpfile); - - write_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 - ); - - relpath = ""; - - 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 -write_log_page(FILE *fp, const git_oid *head) -{ - relpath = ""; - mkdir("commit", S_IRWXU | S_IRWXG | S_IRWXO); - write_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); - write_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) { - fputs("<published>", fp); - printtimez(fp, &(ci->author->when)); - fputs("</published>\n", fp); - } - if (ci->committer) { - fputs("<updated>", fp); - printtimez(fp, &(ci->committer->when)); - fputs("</updated>\n", fp); - } - if (ci->summary) { - fputs("<title>", fp); - if (tag && tag[0]) { - fputs("[", fp); - put_xml(fp, tag, strlen(tag)); - fputs("] ", fp); - } - put_xml(fp, ci->summary, strlen(ci->summary)); - fputs("</title>\n", fp); - } - fprintf( - fp, - "<link rel=\"alternate\" type=\"text/html\" href=\"%scommit/%s.html\" />\n", - baseurl, - ci->oid - ); - - if (ci->author) { - fputs("<author>\n<name>", fp); - put_xml(fp, ci->author->name, strlen(ci->author->name)); - fputs("</name>\n<email>", fp); - put_xml(fp, ci->author->email, strlen(ci->author->email)); - fputs("</email>\n</author>\n", fp); - } - - fputs("<content>", fp); - fprintf(fp, "commit %s\n", ci->oid); - if (ci->parentoid[0]) - fprintf(fp, "parent %s\n", ci->parentoid); - if (ci->author) { - fputs("Author: ", fp); - put_xml(fp, ci->author->name, strlen(ci->author->name)); - fputs(" <", fp); - put_xml(fp, ci->author->email, strlen(ci->author->email)); - fputs(">\nDate: ", fp); - printtime(fp, &(ci->author->when)); - putc('\n', fp); - } - if (ci->msg) { - putc('\n', fp); - put_xml(fp, ci->msg, strlen(ci->msg)); - } - fputs("\n</content>\n</entry>\n", fp); -} - -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 */ - - fputs( - "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" - "<feed xmlns=\"http://www.w3.org/2005/Atom\">\n<title>", - fp - ); - put_xml(fp, stripped_name, strlen(stripped_name)); - fputs(", branch HEAD</title>\n<subtitle>", fp); - put_xml(fp, description, strlen(description)); - fputs("</subtitle>\n", fp); - - /* all commits or only tags? */ - if (all) { - git_revwalk_new(&w, repo); - git_revwalk_push_head(w); - for (i = 0; i < m && !git_revwalk_next(&id, w); i++) { - if (!(ci = 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; -} - -size_t -write_blob(git_object *obj, const char *fpath, const char *filename, size_t filesize) -{ - char tmp[PATH_MAX] = "", *d; - const char *p; - size_t lc = 0; - FILE *fp; - - if (strlcpy(tmp, fpath, sizeof(tmp)) >= sizeof(tmp)) - errx(1, "path truncated: '%s'", fpath); - if (!(d = dirname(tmp))) - err(1, "dirname"); - if (mkdirp(d)) - return -1; - - for (p = fpath, tmp[0] = '\0'; *p; p++) { - if (*p == '/' && strlcat(tmp, "../", sizeof(tmp)) >= sizeof(tmp)) - errx(1, "path truncated: '../%s'", tmp); - } - relpath = tmp; - - fp = efopen(fpath, "w"); - write_header(fp, filename); - fputs("<p> ", fp); - put_xml(fp, filename, strlen(filename)); - fprintf(fp, " (%zuB)", filesize); - fputs("</p>", fp); - - if (git_blob_is_binary((git_blob *)obj)) - // TODO check and display image - fputs("<p>Binary file.</p>\n", fp); - else - lc = write_blob_html(fp, (git_blob *)obj); - - write_footer(fp); - check_file_error(fp, fpath, 'w'); - fclose(fp); - - relpath = ""; - - return lc; -} - -const char * -filemode(git_filemode_t m) -{ - static char mode[11]; - - memset(mode, '-', sizeof(mode) - 1); - mode[10] = '\0'; - - if (S_ISREG(m)) mode[0] = '-'; - else if (S_ISBLK(m)) mode[0] = 'b'; - else if (S_ISCHR(m)) mode[0] = 'c'; - else if (S_ISDIR(m)) mode[0] = 'd'; - else if (S_ISFIFO(m)) mode[0] = 'p'; - else if (S_ISLNK(m)) mode[0] = 'l'; - else if (S_ISSOCK(m)) mode[0] = 's'; - else mode[0] = '?'; - - if (m & S_IRUSR) mode[1] = 'r'; - if (m & S_IWUSR) mode[2] = 'w'; - if (m & S_IXUSR) mode[3] = 'x'; - if (m & S_IRGRP) mode[4] = 'r'; - if (m & S_IWGRP) mode[5] = 'w'; - if (m & S_IXGRP) mode[6] = 'x'; - if (m & S_IROTH) mode[7] = 'r'; - if (m & S_IWOTH) mode[8] = 'w'; - if (m & S_IXOTH) mode[9] = 'x'; - - if (m & S_ISUID) mode[3] = (mode[3] == 'x') ? 's' : 'S'; - if (m & S_ISGID) mode[6] = (mode[6] == 'x') ? 's' : 'S'; - if (m & S_ISVTX) mode[9] = (mode[9] == 'x') ? 't' : 'T'; - - return mode; -} - -int -write_file_list(FILE *fp, git_tree *tree, const char *path) -{ - const git_tree_entry *entry = NULL; - git_object *obj = NULL; - const char *entryname; - char filepath[PATH_MAX], entrypath[PATH_MAX], oid[8]; - size_t count, i, lc, filesize; - int r, ret; - - count = git_tree_entrycount(tree); - for (i = 0; i < count; i++) { - if ( - !(entry = git_tree_entry_byindex(tree, i)) || - !(entryname = git_tree_entry_name(entry)) - ) - return -1; - join_path(entrypath, sizeof(entrypath), path, entryname); - - r = snprintf(filepath, sizeof(filepath), "file/%s.html", entrypath); - if (r < 0 || (size_t)r >= sizeof(filepath)) - errx(1, "path truncated: 'file/%s.html'", entrypath); - - if (!git_tree_entry_to_object(&obj, repo, entry)) { - switch (git_object_type(obj)) { - case GIT_OBJ_BLOB: - break; - case GIT_OBJ_TREE: - /* NOTE: recurses */ - // TODO create page for each dir - // instead of dumping all files into "files.html" - // or, is that a good idea? hm... - ret = write_file_list( fp, (git_tree *)obj, entrypath); - git_object_free(obj); - if (ret) - return ret; - continue; - default: - git_object_free(obj); - continue; - } - - filesize = git_blob_rawsize((git_blob *)obj); - lc = write_blob(obj, filepath, entryname, filesize); - - fputs("<tr><td>", fp); - fputs(filemode(git_tree_entry_filemode(entry)), fp); - fprintf(fp, "</td><td><a href=\"%s", relpath); - put_percent_encoded(fp, filepath, strlen(filepath)); - fputs("\">", fp); - put_xml(fp, entrypath, strlen(entrypath)); - fputs("</a></td><td class=\"num\" align=\"right\">", fp); - if (lc > 0) fprintf(fp, "%zuL", lc); - else fprintf(fp, "%zuB", filesize); - fputs("</td></tr>\n", fp); - git_object_free(obj); - } else if (git_tree_entry_type(entry) == GIT_OBJ_COMMIT) { - /* commit object in tree is a submodule */ - fprintf( - fp, - "<tr><td>m---------</td><td><a href=\"%sfile/.gitmodules.html\">", - relpath - ); - put_xml(fp, entrypath, strlen(entrypath)); - fputs("</a> @ ", fp); - git_oid_tostr(oid, sizeof(oid), git_tree_entry_id(entry)); - put_xml(fp, oid, strlen(oid)); - fputs("</td><td class=\"num\" align=\"right\"></td></tr>\n", fp); - } - } - - return 0; -} - -int -write_files(FILE *fp, const git_oid *id) -{ - git_tree *tree = NULL; - git_commit *commit = NULL; - int ret = -1; - - fputs( - "<table id=\"files\"><thead>\n<tr>" - "<td><b>Mode</b></td><td><b>Name</b></td>" - "<td class=\"num\" align=\"right\"><b>Size</b></td>" - "</tr>\n</thead><tbody>\n", - fp - ); - - if ( - !git_commit_lookup(&commit, repo, id) && - !git_commit_tree(&tree, commit) - ) - ret = write_file_list(fp, tree, ""); - - fputs("</tbody></table>", fp); - - git_commit_free(commit); - git_tree_free(tree); - - return ret; -} - -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); - } - /* table footer */ - 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 -usage(char *argv0) -{ - fprintf( - stderr, - "usage: %s [-c cachefile | -l commits] " - "[-u baseurl] repodir\n", - argv0 - ); - exit(1); -} - -int -main(int argc, char *argv[]) -{ - git_object *obj = NULL; - const git_oid *head = NULL; - FILE *fp, *fpread; - char path[PATH_MAX], repodirabs[PATH_MAX + 1], *p; - 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'; - } - - /* check LICENSE */ - for (i = 0; i < LEN(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); - } - - /* check README */ - for (i = 0; i < LEN(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); - } - - if ( - !git_revparse_single(&obj, repo, "HEAD:.gitmodules") && - git_object_type(obj) == GIT_OBJ_BLOB - ) - submodules = ".gitmodules"; - git_object_free(obj); - - /* log for HEAD */ - fp = efopen("log.html", "w"); - write_log_page(fp, head); - check_file_error(fp, "log.html", 'w'); - fclose(fp); - - /* files for HEAD */ - fp = efopen("files.html", "w"); - write_header(fp, "Files"); - if (head) write_files(fp, head); - write_footer(fp); - check_file_error(fp, "files.html", 'w'); - fclose(fp); - - /* summary page with branches and tags */ - fp = efopen("refs.html", "w"); - write_header(fp, "Refs"); - write_refs(fp); - write_footer(fp); - check_file_error(fp, "refs.html", 'w'); - fclose(fp); - - /* Atom feed */ - fp = efopen("atom.xml", "w"); - write_atom(fp, 1); - check_file_error(fp, "atom.xml", 'w'); - fclose(fp); - - /* Atom feed for tags / releases */ - fp = efopen("tags.xml", "w"); - write_atom(fp, 0); - check_file_error(fp, "tags.xml", 'w'); - fclose(fp); - - /* 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 @@ -12,8 +12,8 @@ img, h1, h2 { vertical-align: middle; } -img { - border: 0; +img, object { + border: 1px solid #ccc; } a { @@ -30,10 +30,7 @@ a:target { background-color: #ddf; } -a.d, -a.h, -a.i, -a.line { +a.d, a.h, a.i, a.line { text-decoration: none; } @@ -53,7 +50,7 @@ table td { #content table td { vertical-align: top; - white-space: pre-wrap; + white-space: pre; } #branches tr:hover td, @@ -82,19 +79,10 @@ table#diffstat { } #blob a { - text-align: right; - border-right: 1px solid black; - margin-right: 5px; - padding-right: 5px; + margin-right: 10px; user-select: none; } -/* -table#blob { - border-collapse: collapse; -} -*/ - td.num { text-align: right; } @@ -110,6 +98,7 @@ hr { } pre { + text-wrap: wrap; font-family: monospace; margin: 0px; } @@ -118,20 +107,14 @@ pre a.h { color: #0072aa; /* blue */ } -.A, -span.i, -pre a.i { +.A, span.i, pre a.i { color: #00ba53; /* green */ } -.D, -span.d, -pre a.d { +.D, span.d, pre a.d { color: #ee3200; /* red */ } -pre a.h:hover, -pre a.i:hover, -pre a.d:hover { +pre a.h:hover, pre a.i:hover, pre a.d:hover { text-decoration: none; }
This webpage is intended to be an accessible preview of this repository. To get a fuller picture, clone it and use the git CLI.