git-query

git data extraction tool using c and libgit2

git clone https://9o.is/git/git-query.git

commit 0c89b7e63f72e0a5ebbe5f83cbd5eae65d3e26fb
parent 5d964e5c133c4ddb2ff27ab65506dff7886a875d
Author: Jul <jul@9o.is>
Date:   Tue,  3 Mar 2026 16:31:52 +0800

remove stagit-related code

Diffstat:
A.gitignore | 2++
MMakefile | 56++++++--------------------------------------------------
MREADME | 186++++++++++++-------------------------------------------------------------------
Dexample_create.sh | 43-------------------------------------------
Dexample_post-receive.sh | 73-------------------------------------------------------------------------
Dfavicon.png | 0
Dlogo.png | 0
Dstagit-index.1 | 47-----------------------------------------------
Dstagit-index.c | 258-------------------------------------------------------------------------------
Dstagit.1 | 125-------------------------------------------------------------------------------
Dstagit.c | 1430-------------------------------------------------------------------------------
Dstyle.css | 154-------------------------------------------------------------------------------
12 files changed, 35 insertions(+), 2339 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -0,0 +1,2 @@ +/git-query +*.o diff --git a/Makefile b/Makefile @@ -1,40 +1,26 @@ .POSIX: -NAME = stagit -VERSION = 1.2 +NAME = git-query +VERSION = 1.0 # paths PREFIX = /usr/local MANPREFIX = ${PREFIX}/man -DOCPREFIX = ${PREFIX}/share/doc/${NAME} LIBGIT_INC = -I/usr/local/include LIBGIT_LIB = -L/usr/local/lib -lgit2 -# use system flags. STAGIT_CFLAGS = ${LIBGIT_INC} ${CFLAGS} STAGIT_LDFLAGS = ${LIBGIT_LIB} ${LDFLAGS} STAGIT_CPPFLAGS = -D_XOPEN_SOURCE=700 -D_DEFAULT_SOURCE -D_BSD_SOURCE -# Uncomment to enable workaround for older libgit2 which don't support this -# option. This workaround will be removed in the future *pinky promise*. -#STAGIT_CFLAGS += -DGIT_OPT_SET_OWNER_VALIDATION=-1 - -SRC = \ - stagit.c\ - stagit-index.c +SRC = git-query.c COMPATSRC = \ reallocarray.c\ strlcat.c\ strlcpy.c -BIN = \ - stagit\ - stagit-index\ - git-query -MAN1 = \ - stagit.1\ - stagit-index.1\ - git-query.1 +BIN = git-query +MAN1 = git-query.1 DOC = \ LICENSE\ README @@ -59,22 +45,14 @@ 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 \ + Makefile \ ${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} - git-query: git-query.o ${COMPATOBJ} ${CC} -o $@ git-query.o ${COMPATOBJ} ${STAGIT_LDFLAGS} @@ -82,37 +60,15 @@ clean: rm -f ${BIN} ${OBJ} ${NAME}-${VERSION}.tar.gz install: all - # installing executable files. mkdir -p ${DESTDIR}${PREFIX}/bin cp -f ${BIN} ${DESTDIR}${PREFIX}/bin for f in ${BIN}; do chmod 755 ${DESTDIR}${PREFIX}/bin/$$f; done - # installing example files. - mkdir -p ${DESTDIR}${DOCPREFIX} - cp -f style.css\ - favicon.png\ - logo.png\ - example_create.sh\ - example_post-receive.sh\ - README\ - ${DESTDIR}${DOCPREFIX} - # installing manual pages. mkdir -p ${DESTDIR}${MANPREFIX}/man1 cp -f ${MAN1} ${DESTDIR}${MANPREFIX}/man1 for m in ${MAN1}; do chmod 644 ${DESTDIR}${MANPREFIX}/man1/$$m; done uninstall: - # removing executable files. for f in ${BIN}; do rm -f ${DESTDIR}${PREFIX}/bin/$$f; done - # removing example files. - rm -f \ - ${DESTDIR}${DOCPREFIX}/style.css\ - ${DESTDIR}${DOCPREFIX}/favicon.png\ - ${DESTDIR}${DOCPREFIX}/logo.png\ - ${DESTDIR}${DOCPREFIX}/example_create.sh\ - ${DESTDIR}${DOCPREFIX}/example_post-receive.sh\ - ${DESTDIR}${DOCPREFIX}/README - -rmdir ${DESTDIR}${DOCPREFIX} - # removing manual pages. for m in ${MAN1}; do rm -f ${DESTDIR}${MANPREFIX}/man1/$$m; done .PHONY: all clean dist install uninstall diff --git a/README b/README @@ -1,27 +1,23 @@ -stagit ------- +git-query +-------- -static git page generator. +Git data extraction tool. -It generates static HTML pages for a git repository. +It extracts git repository data in XML format, suitable for piping to +other tools or templating. Usage ----- -Make files per repository: - - $ mkdir -p htmlroot/htmlrepo1 && cd htmlroot/htmlrepo1 - $ stagit path/to/gitrepo1 - repeat for other repositories - $ ... +git-query <command> <repo> [args...] -Make index file for repositories: - - $ cd htmlroot - $ stagit-index path/to/gitrepo1 \ - path/to/gitrepo2 \ - path/to/gitrepo3 > index.html +Commands: + log <repo> [n] Output commit log (default 100 commits) + commit <repo> [oid] Output commit detail with diff + tree <repo> [oid] Output file tree listing + blob <repo> <path> [oid] Output raw file content + refs <repo> Output branches and tags Build and install @@ -34,153 +30,25 @@ $ make 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 - +- C compiler (C99) +- libgit2 (v0.22+) +- POSIX make -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. +Examples +-------- +# Get last 10 commits +git-query log /path/to/repo 10 -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 +# Get specific commit +git-query commit /path/to/repo abc123def +# Get file tree at HEAD +git-query tree /path/to/repo -Features --------- +# Get file content +git-query blob /path/to/repo README.md -- 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. +# Get list of branches and tags +git-query refs /path/to/repo 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/favicon.png b/favicon.png Binary files differ. diff --git a/logo.png b/logo.png Binary files differ. 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("&lt;", fp); break; - case '>': fputs("&gt;", fp); break; - case '\'': fputs("&#39;" , fp); break; - case '&': fputs("&amp;", fp); break; - case '"': fputs("&quot;", fp); break; - default: putc(*s, fp); - } - } -} - -void -printtimeshort(FILE *fp, const git_time *intime) -{ - struct tm *intm; - time_t t; - char out[32]; - - t = (time_t)intime->time; - if (!(intm = gmtime(&t))) - return; - strftime(out, sizeof(out), "%Y-%m-%d %H:%M", intm); - fputs(out, fp); -} - -void -writeheader(FILE *fp) -{ - fputs("<!DOCTYPE html>\n" - "<html>\n<head>\n" - "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n" - "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n" - "<title>", fp); - xmlencode(fp, description, strlen(description)); - fprintf(fp, "</title>\n<link rel=\"icon\" type=\"image/png\" href=\"%sfavicon.png\" />\n", relpath); - fprintf(fp, "<link rel=\"stylesheet\" type=\"text/css\" href=\"%sstyle.css\" />\n", relpath); - fputs("</head>\n<body>\n", fp); - fprintf(fp, "<table>\n<tr><td><img src=\"%slogo.png\" alt=\"\" width=\"32\" height=\"32\" /></td>\n" - "<td><span class=\"desc\">", relpath); - xmlencode(fp, description, strlen(description)); - fputs("</span></td></tr><tr><td></td><td>\n" - "</td></tr>\n</table>\n<hr/>\n<div id=\"content\">\n" - "<table id=\"index\"><thead>\n" - "<tr><td><b>Name</b></td><td><b>Description</b></td><td><b>Owner</b></td>" - "<td><b>Last commit</b></td></tr>" - "</thead><tbody>\n", fp); -} - -void -writefooter(FILE *fp) -{ - fputs("</tbody>\n</table>\n</div>\n</body>\n</html>\n", fp); -} - -int -writelog(FILE *fp) -{ - git_commit *commit = NULL; - const git_signature *author; - git_revwalk *w = NULL; - git_oid id; - char *stripped_name = NULL, *p; - int ret = 0; - - git_revwalk_new(&w, repo); - git_revwalk_push_head(w); - - if (git_revwalk_next(&id, w) || - git_commit_lookup(&commit, repo, &id)) { - ret = -1; - goto err; - } - - author = git_commit_author(commit); - - /* strip .git suffix */ - if (!(stripped_name = strdup(name))) - err(1, "strdup"); - if ((p = strrchr(stripped_name, '.'))) - if (!strcmp(p, ".git")) - *p = '\0'; - - fputs("<tr><td><a href=\"", fp); - percentencode(fp, stripped_name, strlen(stripped_name)); - fputs("/log.html\">", fp); - xmlencode(fp, stripped_name, strlen(stripped_name)); - fputs("</a></td><td>", fp); - xmlencode(fp, description, strlen(description)); - fputs("</td><td>", fp); - xmlencode(fp, owner, strlen(owner)); - fputs("</td><td>", fp); - if (author) - printtimeshort(fp, &(author->when)); - fputs("</td></tr>", fp); - - git_commit_free(commit); -err: - git_revwalk_free(w); - free(stripped_name); - - return ret; -} - -int -main(int argc, char *argv[]) -{ - FILE *fp; - char path[PATH_MAX], repodirabs[PATH_MAX + 1]; - const char *repodir; - int i, ret = 0; - - if (argc < 2) { - fprintf(stderr, "usage: %s [repodir...]\n", argv[0]); - return 1; - } - - /* do not search outside the git repository: - GIT_CONFIG_LEVEL_APP is the highest level currently */ - git_libgit2_init(); - for (i = 1; i <= GIT_CONFIG_LEVEL_APP; i++) - git_libgit2_opts(GIT_OPT_SET_SEARCH_PATH, i, ""); - /* do not require the git repository to be owned by the current user */ - git_libgit2_opts(GIT_OPT_SET_OWNER_VALIDATION, 0); - -#ifdef __OpenBSD__ - if (pledge("stdio rpath", NULL) == -1) - err(1, "pledge"); -#endif - - writeheader(stdout); - - for (i = 1; i < argc; i++) { - repodir = argv[i]; - if (!realpath(repodir, repodirabs)) - err(1, "realpath"); - - if (git_repository_open_ext(&repo, repodir, - GIT_REPOSITORY_OPEN_NO_SEARCH, NULL)) { - fprintf(stderr, "%s: cannot open repository\n", argv[0]); - ret = 1; - continue; - } - - /* use directory name as name */ - if ((name = strrchr(repodirabs, '/'))) - name++; - else - name = ""; - - /* read description or .git/description */ - joinpath(path, sizeof(path), repodir, "description"); - if (!(fp = fopen(path, "r"))) { - joinpath(path, sizeof(path), repodir, ".git/description"); - fp = fopen(path, "r"); - } - description[0] = '\0'; - if (fp) { - if (!fgets(description, sizeof(description), fp)) - description[0] = '\0'; - checkfileerror(fp, "description", 'r'); - fclose(fp); - } - - /* read owner or .git/owner */ - joinpath(path, sizeof(path), repodir, "owner"); - if (!(fp = fopen(path, "r"))) { - joinpath(path, sizeof(path), repodir, ".git/owner"); - fp = fopen(path, "r"); - } - owner[0] = '\0'; - if (fp) { - if (!fgets(owner, sizeof(owner), fp)) - owner[0] = '\0'; - checkfileerror(fp, "owner", 'r'); - fclose(fp); - owner[strcspn(owner, "\n")] = '\0'; - } - writelog(stdout); - } - writefooter(stdout); - - /* cleanup */ - git_repository_free(repo); - git_libgit2_shutdown(); - - checkfileerror(stdout, "<stdout>", 'w'); - - return ret; -} 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,1430 +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 addcount; - size_t delcount; -}; - -struct commitinfo { - const git_oid *id; - - char oid[GIT_OID_HEXSZ + 1]; - char parentoid[GIT_OID_HEXSZ + 1]; - - const git_signature *author; - const git_signature *committer; - const char *summary; - const char *msg; - - git_diff *diff; - git_commit *commit; - git_commit *parent; - git_tree *commit_tree; - git_tree *parent_tree; - - size_t addcount; - size_t delcount; - size_t filecount; - - struct deltainfo **deltas; - size_t ndeltas; -}; - -/* reference and associated data for sorting */ -struct referenceinfo { - struct git_reference *ref; - struct commitinfo *ci; -}; - -static git_repository *repo; - -static const char *baseurl = ""; /* base URL to make absolute RSS/Atom URI */ -static const char *relpath = ""; -static const char *repodir; - -static char *name = ""; -static char *strippedname = ""; -static char description[255]; -static char cloneurl[1024]; -static char *submodules; -static char *licensefiles[] = { "HEAD:LICENSE", "HEAD:LICENSE.md", "HEAD:COPYING" }; -static char *license; -static char *readmefiles[] = { "HEAD:README", "HEAD:README.md" }; -static char *readme; -static long long nlogcommits = -1; /* -1 indicates not used */ - -/* cache */ -static git_oid lastoid; -static char lastoidstr[GIT_OID_HEXSZ + 2]; /* id + newline + NUL byte */ -static FILE *rcachefp, *wcachefp; -static const char *cachefile; - -/* Handle read or write errors for a FILE * stream */ -void -checkfileerror(FILE *fp, const char *name, int mode) -{ - if (mode == 'r' && ferror(fp)) - errx(1, "read error: %s", name); - else if (mode == 'w' && (fflush(fp) || ferror(fp))) - errx(1, "write error: %s", name); -} - -void -joinpath(char *buf, size_t bufsiz, const char *path, const char *path2) -{ - int r; - - r = snprintf(buf, bufsiz, "%s%s%s", - path, path[0] && path[strlen(path) - 1] != '/' ? "/" : "", path2); - if (r < 0 || (size_t)r >= bufsiz) - errx(1, "path truncated: '%s%s%s'", - path, path[0] && path[strlen(path) - 1] != '/' ? "/" : "", path2); -} - -void -deltainfo_free(struct deltainfo *di) -{ - if (!di) - return; - git_patch_free(di->patch); - memset(di, 0, sizeof(*di)); - free(di); -} - -int -commitinfo_getstats(struct commitinfo *ci) -{ - struct deltainfo *di; - git_diff_options opts; - git_diff_find_options fopts; - const git_diff_delta *delta; - const git_diff_hunk *hunk; - const git_diff_line *line; - git_patch *patch = NULL; - size_t ndeltas, nhunks, nhunklines; - size_t i, j, k; - - if (git_tree_lookup(&(ci->commit_tree), repo, git_commit_tree_id(ci->commit))) - goto err; - if (!git_commit_parent(&(ci->parent), ci->commit, 0)) { - if (git_tree_lookup(&(ci->parent_tree), repo, git_commit_tree_id(ci->parent))) { - ci->parent = NULL; - ci->parent_tree = NULL; - } - } - - git_diff_init_options(&opts, GIT_DIFF_OPTIONS_VERSION); - opts.flags |= GIT_DIFF_DISABLE_PATHSPEC_MATCH | - GIT_DIFF_IGNORE_SUBMODULES | - GIT_DIFF_INCLUDE_TYPECHANGE; - if (git_diff_tree_to_tree(&(ci->diff), repo, ci->parent_tree, ci->commit_tree, &opts)) - goto err; - - if (git_diff_find_init_options(&fopts, GIT_DIFF_FIND_OPTIONS_VERSION)) - goto err; - /* find renames and copies, exact matches (no heuristic) for renames. */ - fopts.flags |= GIT_DIFF_FIND_RENAMES | GIT_DIFF_FIND_COPIES | - GIT_DIFF_FIND_EXACT_MATCH_ONLY; - if (git_diff_find_similar(ci->diff, &fopts)) - goto err; - - ndeltas = git_diff_num_deltas(ci->diff); - if (ndeltas && !(ci->deltas = calloc(ndeltas, sizeof(struct deltainfo *)))) - err(1, "calloc"); - - for (i = 0; i < ndeltas; i++) { - if (git_patch_from_diff(&patch, ci->diff, i)) - goto err; - - if (!(di = calloc(1, sizeof(struct deltainfo)))) - err(1, "calloc"); - di->patch = patch; - ci->deltas[i] = di; - - delta = git_patch_get_delta(patch); - - /* skip stats for binary data */ - if (delta->flags & GIT_DIFF_FLAG_BINARY) - continue; - - nhunks = git_patch_num_hunks(patch); - for (j = 0; j < nhunks; j++) { - if (git_patch_get_hunk(&hunk, &nhunklines, patch, j)) - break; - for (k = 0; ; k++) { - if (git_patch_get_line_in_hunk(&line, patch, j, k)) - break; - if (line->old_lineno == -1) { - di->addcount++; - ci->addcount++; - } else if (line->new_lineno == -1) { - di->delcount++; - ci->delcount++; - } - } - } - } - ci->ndeltas = i; - ci->filecount = i; - - return 0; - -err: - git_diff_free(ci->diff); - ci->diff = NULL; - git_tree_free(ci->commit_tree); - ci->commit_tree = NULL; - git_tree_free(ci->parent_tree); - ci->parent_tree = NULL; - git_commit_free(ci->parent); - ci->parent = NULL; - - if (ci->deltas) - for (i = 0; i < ci->ndeltas; i++) - deltainfo_free(ci->deltas[i]); - free(ci->deltas); - ci->deltas = NULL; - ci->ndeltas = 0; - ci->addcount = 0; - ci->delcount = 0; - ci->filecount = 0; - - return -1; -} - -void -commitinfo_free(struct commitinfo *ci) -{ - size_t i; - - if (!ci) - return; - if (ci->deltas) - for (i = 0; i < ci->ndeltas; i++) - deltainfo_free(ci->deltas[i]); - - free(ci->deltas); - git_diff_free(ci->diff); - git_tree_free(ci->commit_tree); - git_tree_free(ci->parent_tree); - git_commit_free(ci->commit); - git_commit_free(ci->parent); - memset(ci, 0, sizeof(*ci)); - free(ci); -} - -struct commitinfo * -commitinfo_getbyoid(const git_oid *id) -{ - struct commitinfo *ci; - - if (!(ci = calloc(1, sizeof(struct commitinfo)))) - err(1, "calloc"); - - if (git_commit_lookup(&(ci->commit), repo, id)) - goto err; - ci->id = id; - - git_oid_tostr(ci->oid, sizeof(ci->oid), git_commit_id(ci->commit)); - git_oid_tostr(ci->parentoid, sizeof(ci->parentoid), git_commit_parent_id(ci->commit, 0)); - - ci->author = git_commit_author(ci->commit); - ci->committer = git_commit_committer(ci->commit); - ci->summary = git_commit_summary(ci->commit); - ci->msg = git_commit_message(ci->commit); - - return ci; - -err: - commitinfo_free(ci); - - return NULL; -} - -int -refs_cmp(const void *v1, const void *v2) -{ - const struct referenceinfo *r1 = v1, *r2 = v2; - time_t t1, t2; - int r; - - if ((r = git_reference_is_tag(r1->ref) - git_reference_is_tag(r2->ref))) - return r; - - t1 = r1->ci->author ? r1->ci->author->when.time : 0; - t2 = r2->ci->author ? r2->ci->author->when.time : 0; - if ((r = t1 > t2 ? -1 : (t1 == t2 ? 0 : 1))) - return r; - - return strcmp(git_reference_shorthand(r1->ref), - git_reference_shorthand(r2->ref)); -} - -int -getrefs(struct referenceinfo **pris, size_t *prefcount) -{ - struct referenceinfo *ris = NULL; - struct commitinfo *ci = NULL; - git_reference_iterator *it = NULL; - const git_oid *id = NULL; - git_object *obj = NULL; - git_reference *dref = NULL, *r, *ref = NULL; - size_t i, refcount; - - *pris = NULL; - *prefcount = 0; - - if (git_reference_iterator_new(&it, repo)) - return -1; - - for (refcount = 0; !git_reference_next(&ref, it); ) { - if (!git_reference_is_branch(ref) && !git_reference_is_tag(ref)) { - git_reference_free(ref); - ref = NULL; - continue; - } - - switch (git_reference_type(ref)) { - case GIT_REF_SYMBOLIC: - if (git_reference_resolve(&dref, ref)) - goto err; - r = dref; - break; - case GIT_REF_OID: - r = ref; - break; - default: - continue; - } - if (!git_reference_target(r) || - git_reference_peel(&obj, r, GIT_OBJ_ANY)) - goto err; - if (!(id = git_object_id(obj))) - goto err; - if (!(ci = commitinfo_getbyoid(id))) - break; - - if (!(ris = reallocarray(ris, refcount + 1, sizeof(*ris)))) - err(1, "realloc"); - ris[refcount].ci = ci; - ris[refcount].ref = r; - refcount++; - - git_object_free(obj); - obj = NULL; - git_reference_free(dref); - dref = NULL; - } - git_reference_iterator_free(it); - - /* sort by type, date then shorthand name */ - qsort(ris, refcount, sizeof(*ris), refs_cmp); - - *pris = ris; - *prefcount = refcount; - - return 0; - -err: - git_object_free(obj); - git_reference_free(dref); - commitinfo_free(ci); - for (i = 0; i < refcount; i++) { - commitinfo_free(ris[i].ci); - git_reference_free(ris[i].ref); - } - free(ris); - - return -1; -} - -FILE * -efopen(const char *filename, const char *flags) -{ - FILE *fp; - - if (!(fp = fopen(filename, flags))) - err(1, "fopen: '%s'", filename); - - return fp; -} - -/* Percent-encode, see RFC3986 section 2.1. */ -void -percentencode(FILE *fp, const char *s, size_t len) -{ - static char tab[] = "0123456789ABCDEF"; - unsigned char uc; - size_t i; - - for (i = 0; *s && i < len; s++, i++) { - uc = *s; - /* NOTE: do not encode '/' for paths or ",-." */ - if (uc < ',' || uc >= 127 || (uc >= ':' && uc <= '@') || - uc == '[' || uc == ']') { - putc('%', fp); - putc(tab[(uc >> 4) & 0x0f], fp); - putc(tab[uc & 0x0f], fp); - } else { - putc(uc, fp); - } - } -} - -/* Escape characters below as HTML 2.0 / XML 1.0. */ -void -xmlencode(FILE *fp, const char *s, size_t len) -{ - size_t i; - - for (i = 0; *s && i < len; s++, i++) { - switch(*s) { - case '<': fputs("&lt;", fp); break; - case '>': fputs("&gt;", fp); break; - case '\'': fputs("&#39;", fp); break; - case '&': fputs("&amp;", fp); break; - case '"': fputs("&quot;", fp); break; - default: putc(*s, fp); - } - } -} - -/* Escape characters below as HTML 2.0 / XML 1.0, ignore printing '\r', '\n' */ -void -xmlencodeline(FILE *fp, const char *s, size_t len) -{ - size_t i; - - for (i = 0; *s && i < len; s++, i++) { - switch(*s) { - case '<': fputs("&lt;", fp); break; - case '>': fputs("&gt;", fp); break; - case '\'': fputs("&#39;", fp); break; - case '&': fputs("&amp;", fp); break; - case '"': fputs("&quot;", fp); break; - case '\r': break; /* ignore CR */ - case '\n': break; /* ignore LF */ - default: putc(*s, fp); - } - } -} - -int -mkdirp(const char *path) -{ - char tmp[PATH_MAX], *p; - - if (strlcpy(tmp, path, sizeof(tmp)) >= sizeof(tmp)) - errx(1, "path truncated: '%s'", path); - for (p = tmp + (tmp[0] == '/'); *p; p++) { - if (*p != '/') - continue; - *p = '\0'; - if (mkdir(tmp, S_IRWXU | S_IRWXG | S_IRWXO) < 0 && errno != EEXIST) - return -1; - *p = '/'; - } - if (mkdir(tmp, S_IRWXU | S_IRWXG | S_IRWXO) < 0 && errno != EEXIST) - return -1; - return 0; -} - -void -printtimez(FILE *fp, const git_time *intime) -{ - struct tm *intm; - time_t t; - char out[32]; - - t = (time_t)intime->time; - if (!(intm = gmtime(&t))) - return; - strftime(out, sizeof(out), "%Y-%m-%dT%H:%M:%SZ", intm); - fputs(out, fp); -} - -void -printtime(FILE *fp, const git_time *intime) -{ - struct tm *intm; - time_t t; - char out[32]; - - t = (time_t)intime->time + (intime->offset * 60); - if (!(intm = gmtime(&t))) - return; - strftime(out, sizeof(out), "%a, %e %b %Y %H:%M:%S", intm); - if (intime->offset < 0) - fprintf(fp, "%s -%02d%02d", out, - -(intime->offset) / 60, -(intime->offset) % 60); - else - fprintf(fp, "%s +%02d%02d", out, - intime->offset / 60, intime->offset % 60); -} - -void -printtimeshort(FILE *fp, const git_time *intime) -{ - struct tm *intm; - time_t t; - char out[32]; - - t = (time_t)intime->time; - if (!(intm = gmtime(&t))) - return; - strftime(out, sizeof(out), "%Y-%m-%d %H:%M", intm); - fputs(out, fp); -} - -void -writeheader(FILE *fp, const char *title) -{ - fputs("<!DOCTYPE html>\n" - "<html>\n<head>\n" - "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n" - "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n" - "<title>", fp); - xmlencode(fp, title, strlen(title)); - if (title[0] && strippedname[0]) - fputs(" - ", fp); - xmlencode(fp, strippedname, strlen(strippedname)); - if (description[0]) - fputs(" - ", fp); - xmlencode(fp, description, strlen(description)); - fprintf(fp, "</title>\n<link rel=\"icon\" type=\"image/png\" href=\"%sfavicon.png\" />\n", relpath); - fputs("<link rel=\"alternate\" type=\"application/atom+xml\" title=\"", fp); - xmlencode(fp, name, strlen(name)); - fprintf(fp, " Atom Feed\" href=\"%satom.xml\" />\n", relpath); - fputs("<link rel=\"alternate\" type=\"application/atom+xml\" title=\"", fp); - xmlencode(fp, name, strlen(name)); - fprintf(fp, " Atom Feed (tags)\" href=\"%stags.xml\" />\n", relpath); - fprintf(fp, "<link rel=\"stylesheet\" type=\"text/css\" href=\"%sstyle.css\" />\n", relpath); - fputs("</head>\n<body>\n<table><tr><td>", fp); - fprintf(fp, "<a href=\"../%s\"><img src=\"%slogo.png\" alt=\"\" width=\"32\" height=\"32\" /></a>", - relpath, relpath); - fputs("</td><td><h1>", fp); - xmlencode(fp, strippedname, strlen(strippedname)); - fputs("</h1><span class=\"desc\">", fp); - xmlencode(fp, description, strlen(description)); - fputs("</span></td></tr>", fp); - if (cloneurl[0]) { - fputs("<tr class=\"url\"><td></td><td>git clone <a href=\"", fp); - xmlencode(fp, cloneurl, strlen(cloneurl)); /* not percent-encoded */ - fputs("\">", fp); - xmlencode(fp, cloneurl, strlen(cloneurl)); - fputs("</a></td></tr>", fp); - } - fputs("<tr><td></td><td>\n", fp); - fprintf(fp, "<a href=\"%slog.html\">Log</a> | ", relpath); - fprintf(fp, "<a href=\"%sfiles.html\">Files</a> | ", relpath); - fprintf(fp, "<a href=\"%srefs.html\">Refs</a>", relpath); - if (submodules) - fprintf(fp, " | <a href=\"%sfile/%s.html\">Submodules</a>", - relpath, submodules); - if (readme) - fprintf(fp, " | <a href=\"%sfile/%s.html\">README</a>", - relpath, readme); - if (license) - fprintf(fp, " | <a href=\"%sfile/%s.html\">LICENSE</a>", - relpath, license); - fputs("</td></tr></table>\n<hr/>\n<div id=\"content\">\n", fp); -} - -void -writefooter(FILE *fp) -{ - fputs("</div>\n</body>\n</html>\n", fp); -} - -size_t -writeblobhtml(FILE *fp, const git_blob *blob) -{ - size_t n = 0, i, len, prev; - const char *nfmt = "<a href=\"#l%zu\" class=\"line\" id=\"l%zu\">%7zu</a> "; - const char *s = git_blob_rawcontent(blob); - - len = git_blob_rawsize(blob); - fputs("<pre id=\"blob\">\n", fp); - - if (len > 0) { - for (i = 0, prev = 0; i < len; i++) { - if (s[i] != '\n') - continue; - n++; - fprintf(fp, nfmt, n, n, n); - xmlencodeline(fp, &s[prev], i - prev + 1); - putc('\n', fp); - prev = i + 1; - } - /* trailing data */ - if ((len - prev) > 0) { - n++; - fprintf(fp, nfmt, n, n, n); - xmlencodeline(fp, &s[prev], len - prev); - } - } - - fputs("</pre>\n", fp); - - return n; -} - -void -printcommit(FILE *fp, struct commitinfo *ci) -{ - fprintf(fp, "<b>commit</b> <a href=\"%scommit/%s.html\">%s</a>\n", - relpath, ci->oid, ci->oid); - - if (ci->parentoid[0]) - fprintf(fp, "<b>parent</b> <a href=\"%scommit/%s.html\">%s</a>\n", - relpath, ci->parentoid, ci->parentoid); - - if (ci->author) { - fputs("<b>Author:</b> ", fp); - xmlencode(fp, ci->author->name, strlen(ci->author->name)); - fputs(" &lt;<a href=\"mailto:", fp); - xmlencode(fp, ci->author->email, strlen(ci->author->email)); /* not percent-encoded */ - fputs("\">", fp); - xmlencode(fp, ci->author->email, strlen(ci->author->email)); - fputs("</a>&gt;\n<b>Date:</b> ", fp); - printtime(fp, &(ci->author->when)); - putc('\n', fp); - } - if (ci->msg) { - putc('\n', fp); - xmlencode(fp, ci->msg, strlen(ci->msg)); - putc('\n', fp); - } -} - -void -printshowfile(FILE *fp, struct commitinfo *ci) -{ - const git_diff_delta *delta; - const git_diff_hunk *hunk; - const git_diff_line *line; - git_patch *patch; - size_t nhunks, nhunklines, changed, add, del, total, i, j, k; - char linestr[80]; - int c; - - printcommit(fp, ci); - - if (!ci->deltas) - return; - - if (ci->filecount > 1000 || - ci->ndeltas > 1000 || - ci->addcount > 100000 || - ci->delcount > 100000) { - fputs("Diff is too large, output suppressed.\n", fp); - return; - } - - /* diff stat */ - fputs("<b>Diffstat:</b>\n<table>", fp); - for (i = 0; i < ci->ndeltas; i++) { - delta = git_patch_get_delta(ci->deltas[i]->patch); - - switch (delta->status) { - case GIT_DELTA_ADDED: c = 'A'; break; - case GIT_DELTA_COPIED: c = 'C'; break; - case GIT_DELTA_DELETED: c = 'D'; break; - case GIT_DELTA_MODIFIED: c = 'M'; break; - case GIT_DELTA_RENAMED: c = 'R'; break; - case GIT_DELTA_TYPECHANGE: c = 'T'; break; - default: c = ' '; break; - } - if (c == ' ') - fprintf(fp, "<tr><td>%c", c); - else - fprintf(fp, "<tr><td class=\"%c\">%c", c, c); - - fprintf(fp, "</td><td><a href=\"#h%zu\">", i); - xmlencode(fp, delta->old_file.path, strlen(delta->old_file.path)); - if (strcmp(delta->old_file.path, delta->new_file.path)) { - fputs(" -&gt; ", fp); - xmlencode(fp, delta->new_file.path, strlen(delta->new_file.path)); - } - - add = ci->deltas[i]->addcount; - del = ci->deltas[i]->delcount; - changed = add + del; - total = sizeof(linestr) - 2; - if (changed > total) { - if (add) - add = ((float)total / changed * add) + 1; - if (del) - del = ((float)total / changed * del) + 1; - } - memset(&linestr, '+', add); - memset(&linestr[add], '-', del); - - fprintf(fp, "</a></td><td> | </td><td class=\"num\">%zu</td><td><span class=\"i\">", - ci->deltas[i]->addcount + ci->deltas[i]->delcount); - fwrite(&linestr, 1, add, fp); - fputs("</span><span class=\"d\">", fp); - fwrite(&linestr[add], 1, del, fp); - fputs("</span></td></tr>\n", fp); - } - fprintf(fp, "</table></pre><pre>%zu file%s changed, %zu insertion%s(+), %zu deletion%s(-)\n", - ci->filecount, ci->filecount == 1 ? "" : "s", - ci->addcount, ci->addcount == 1 ? "" : "s", - ci->delcount, ci->delcount == 1 ? "" : "s"); - - fputs("<hr/>", fp); - - for (i = 0; i < ci->ndeltas; i++) { - patch = ci->deltas[i]->patch; - delta = git_patch_get_delta(patch); - fprintf(fp, "<b>diff --git a/<a id=\"h%zu\" href=\"%sfile/", i, relpath); - percentencode(fp, delta->old_file.path, strlen(delta->old_file.path)); - fputs(".html\">", fp); - xmlencode(fp, delta->old_file.path, strlen(delta->old_file.path)); - fprintf(fp, "</a> b/<a href=\"%sfile/", relpath); - percentencode(fp, delta->new_file.path, strlen(delta->new_file.path)); - fprintf(fp, ".html\">"); - xmlencode(fp, delta->new_file.path, strlen(delta->new_file.path)); - fprintf(fp, "</a></b>\n"); - - /* check binary data */ - if (delta->flags & GIT_DIFF_FLAG_BINARY) { - fputs("Binary files differ.\n", fp); - continue; - } - - nhunks = git_patch_num_hunks(patch); - for (j = 0; j < nhunks; j++) { - if (git_patch_get_hunk(&hunk, &nhunklines, patch, j)) - break; - - fprintf(fp, "<a href=\"#h%zu-%zu\" id=\"h%zu-%zu\" class=\"h\">", i, j, i, j); - xmlencode(fp, hunk->header, hunk->header_len); - fputs("</a>", fp); - - for (k = 0; ; k++) { - if (git_patch_get_line_in_hunk(&line, patch, j, k)) - break; - if (line->old_lineno == -1) - fprintf(fp, "<a href=\"#h%zu-%zu-%zu\" id=\"h%zu-%zu-%zu\" class=\"i\">+", - i, j, k, i, j, k); - else if (line->new_lineno == -1) - fprintf(fp, "<a href=\"#h%zu-%zu-%zu\" id=\"h%zu-%zu-%zu\" class=\"d\">-", - i, j, k, i, j, k); - else - putc(' ', fp); - xmlencodeline(fp, line->content, line->content_len); - putc('\n', fp); - if (line->old_lineno == -1 || line->new_lineno == -1) - fputs("</a>", fp); - } - } - } -} - -void -writelogline(FILE *fp, struct commitinfo *ci) -{ - fputs("<tr><td>", fp); - if (ci->author) - printtimeshort(fp, &(ci->author->when)); - fputs("</td><td>", fp); - if (ci->summary) { - fprintf(fp, "<a href=\"%scommit/%s.html\">", relpath, ci->oid); - xmlencode(fp, ci->summary, strlen(ci->summary)); - fputs("</a>", fp); - } - fputs("</td><td>", fp); - if (ci->author) - xmlencode(fp, ci->author->name, strlen(ci->author->name)); - fputs("</td><td class=\"num\" align=\"right\">", fp); - fprintf(fp, "%zu", ci->filecount); - fputs("</td><td class=\"num\" align=\"right\">", fp); - fprintf(fp, "+%zu", ci->addcount); - fputs("</td><td class=\"num\" align=\"right\">", fp); - fprintf(fp, "-%zu", ci->delcount); - fputs("</td></tr>\n", fp); -} - -int -writelog(FILE *fp, const git_oid *oid) -{ - struct commitinfo *ci; - git_revwalk *w = NULL; - git_oid id; - char path[PATH_MAX], oidstr[GIT_OID_HEXSZ + 1]; - FILE *fpfile; - size_t remcommits = 0; - int r; - - git_revwalk_new(&w, repo); - git_revwalk_push(w, oid); - - while (!git_revwalk_next(&id, w)) { - relpath = ""; - - if (cachefile && !memcmp(&id, &lastoid, sizeof(id))) - break; - - git_oid_tostr(oidstr, sizeof(oidstr), &id); - r = snprintf(path, sizeof(path), "commit/%s.html", oidstr); - if (r < 0 || (size_t)r >= sizeof(path)) - errx(1, "path truncated: 'commit/%s.html'", oidstr); - r = access(path, F_OK); - - /* optimization: if there are no log lines to write and - the commit file already exists: skip the diffstat */ - if (!nlogcommits) { - remcommits++; - if (!r) - continue; - } - - if (!(ci = commitinfo_getbyoid(&id))) - break; - /* diffstat: for stagit HTML required for the log.html line */ - if (commitinfo_getstats(ci) == -1) - goto err; - - if (nlogcommits != 0) { - writelogline(fp, ci); - if (nlogcommits > 0) - nlogcommits--; - } - - if (cachefile) - writelogline(wcachefp, ci); - - /* check if file exists if so skip it */ - if (r) { - relpath = "../"; - fpfile = efopen(path, "w"); - writeheader(fpfile, ci->summary); - fputs("<pre>", fpfile); - printshowfile(fpfile, ci); - fputs("</pre>\n", fpfile); - writefooter(fpfile); - checkfileerror(fpfile, path, 'w'); - fclose(fpfile); - } -err: - commitinfo_free(ci); - } - git_revwalk_free(w); - - if (nlogcommits == 0 && remcommits != 0) { - fprintf(fp, "<tr><td></td><td colspan=\"5\">" - "%zu more commits remaining, fetch the repository" - "</td></tr>\n", remcommits); - } - - relpath = ""; - - return 0; -} - -void -printcommitatom(FILE *fp, struct commitinfo *ci, const char *tag) -{ - fputs("<entry>\n", fp); - - fprintf(fp, "<id>%s</id>\n", ci->oid); - if (ci->author) { - fputs("<published>", fp); - printtimez(fp, &(ci->author->when)); - fputs("</published>\n", fp); - } - if (ci->committer) { - fputs("<updated>", fp); - printtimez(fp, &(ci->committer->when)); - fputs("</updated>\n", fp); - } - if (ci->summary) { - fputs("<title>", fp); - if (tag && tag[0]) { - fputs("[", fp); - xmlencode(fp, tag, strlen(tag)); - fputs("] ", fp); - } - xmlencode(fp, ci->summary, strlen(ci->summary)); - fputs("</title>\n", fp); - } - fprintf(fp, "<link rel=\"alternate\" type=\"text/html\" href=\"%scommit/%s.html\" />\n", - baseurl, ci->oid); - - if (ci->author) { - fputs("<author>\n<name>", fp); - xmlencode(fp, ci->author->name, strlen(ci->author->name)); - fputs("</name>\n<email>", fp); - xmlencode(fp, ci->author->email, strlen(ci->author->email)); - fputs("</email>\n</author>\n", fp); - } - - fputs("<content>", fp); - fprintf(fp, "commit %s\n", ci->oid); - if (ci->parentoid[0]) - fprintf(fp, "parent %s\n", ci->parentoid); - if (ci->author) { - fputs("Author: ", fp); - xmlencode(fp, ci->author->name, strlen(ci->author->name)); - fputs(" &lt;", fp); - xmlencode(fp, ci->author->email, strlen(ci->author->email)); - fputs("&gt;\nDate: ", fp); - printtime(fp, &(ci->author->when)); - putc('\n', fp); - } - if (ci->msg) { - putc('\n', fp); - xmlencode(fp, ci->msg, strlen(ci->msg)); - } - fputs("\n</content>\n</entry>\n", fp); -} - -int -writeatom(FILE *fp, int all) -{ - struct referenceinfo *ris = NULL; - size_t refcount = 0; - struct commitinfo *ci; - git_revwalk *w = NULL; - git_oid id; - size_t i, m = 100; /* last 'm' commits */ - - fputs("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" - "<feed xmlns=\"http://www.w3.org/2005/Atom\">\n<title>", fp); - xmlencode(fp, strippedname, strlen(strippedname)); - fputs(", branch HEAD</title>\n<subtitle>", fp); - xmlencode(fp, description, strlen(description)); - fputs("</subtitle>\n", fp); - - /* all commits or only tags? */ - if (all) { - git_revwalk_new(&w, repo); - git_revwalk_push_head(w); - for (i = 0; i < m && !git_revwalk_next(&id, w); i++) { - if (!(ci = commitinfo_getbyoid(&id))) - break; - printcommitatom(fp, ci, ""); - commitinfo_free(ci); - } - git_revwalk_free(w); - } else if (getrefs(&ris, &refcount) != -1) { - /* references: tags */ - for (i = 0; i < refcount; i++) { - if (git_reference_is_tag(ris[i].ref)) - printcommitatom(fp, ris[i].ci, - git_reference_shorthand(ris[i].ref)); - - commitinfo_free(ris[i].ci); - git_reference_free(ris[i].ref); - } - free(ris); - } - - fputs("</feed>\n", fp); - - return 0; -} - -size_t -writeblob(git_object *obj, const char *fpath, const char *filename, size_t filesize) -{ - char tmp[PATH_MAX] = "", *d; - const char *p; - size_t lc = 0; - FILE *fp; - - if (strlcpy(tmp, fpath, sizeof(tmp)) >= sizeof(tmp)) - errx(1, "path truncated: '%s'", fpath); - if (!(d = dirname(tmp))) - err(1, "dirname"); - if (mkdirp(d)) - return -1; - - for (p = fpath, tmp[0] = '\0'; *p; p++) { - if (*p == '/' && strlcat(tmp, "../", sizeof(tmp)) >= sizeof(tmp)) - errx(1, "path truncated: '../%s'", tmp); - } - relpath = tmp; - - fp = efopen(fpath, "w"); - writeheader(fp, filename); - fputs("<p> ", fp); - xmlencode(fp, filename, strlen(filename)); - fprintf(fp, " (%zuB)", filesize); - fputs("</p><hr/>", fp); - - if (git_blob_is_binary((git_blob *)obj)) - fputs("<p>Binary file.</p>\n", fp); - else - lc = writeblobhtml(fp, (git_blob *)obj); - - writefooter(fp); - checkfileerror(fp, fpath, 'w'); - fclose(fp); - - relpath = ""; - - return lc; -} - -const char * -filemode(git_filemode_t m) -{ - static char mode[11]; - - memset(mode, '-', sizeof(mode) - 1); - mode[10] = '\0'; - - if (S_ISREG(m)) - mode[0] = '-'; - else if (S_ISBLK(m)) - mode[0] = 'b'; - else if (S_ISCHR(m)) - mode[0] = 'c'; - else if (S_ISDIR(m)) - mode[0] = 'd'; - else if (S_ISFIFO(m)) - mode[0] = 'p'; - else if (S_ISLNK(m)) - mode[0] = 'l'; - else if (S_ISSOCK(m)) - mode[0] = 's'; - else - mode[0] = '?'; - - if (m & S_IRUSR) mode[1] = 'r'; - if (m & S_IWUSR) mode[2] = 'w'; - if (m & S_IXUSR) mode[3] = 'x'; - if (m & S_IRGRP) mode[4] = 'r'; - if (m & S_IWGRP) mode[5] = 'w'; - if (m & S_IXGRP) mode[6] = 'x'; - if (m & S_IROTH) mode[7] = 'r'; - if (m & S_IWOTH) mode[8] = 'w'; - if (m & S_IXOTH) mode[9] = 'x'; - - if (m & S_ISUID) mode[3] = (mode[3] == 'x') ? 's' : 'S'; - if (m & S_ISGID) mode[6] = (mode[6] == 'x') ? 's' : 'S'; - if (m & S_ISVTX) mode[9] = (mode[9] == 'x') ? 't' : 'T'; - - return mode; -} - -int -writefilestree(FILE *fp, git_tree *tree, const char *path) -{ - const git_tree_entry *entry = NULL; - git_object *obj = NULL; - const char *entryname; - char filepath[PATH_MAX], entrypath[PATH_MAX], oid[8]; - size_t count, i, lc, filesize; - int r, ret; - - count = git_tree_entrycount(tree); - for (i = 0; i < count; i++) { - if (!(entry = git_tree_entry_byindex(tree, i)) || - !(entryname = git_tree_entry_name(entry))) - return -1; - joinpath(entrypath, sizeof(entrypath), path, entryname); - - r = snprintf(filepath, sizeof(filepath), "file/%s.html", - entrypath); - if (r < 0 || (size_t)r >= sizeof(filepath)) - errx(1, "path truncated: 'file/%s.html'", entrypath); - - if (!git_tree_entry_to_object(&obj, repo, entry)) { - switch (git_object_type(obj)) { - case GIT_OBJ_BLOB: - break; - case GIT_OBJ_TREE: - /* NOTE: recurses */ - ret = writefilestree(fp, (git_tree *)obj, - entrypath); - git_object_free(obj); - if (ret) - return ret; - continue; - default: - git_object_free(obj); - continue; - } - - filesize = git_blob_rawsize((git_blob *)obj); - lc = writeblob(obj, filepath, entryname, filesize); - - fputs("<tr><td>", fp); - fputs(filemode(git_tree_entry_filemode(entry)), fp); - fprintf(fp, "</td><td><a href=\"%s", relpath); - percentencode(fp, filepath, strlen(filepath)); - fputs("\">", fp); - xmlencode(fp, entrypath, strlen(entrypath)); - fputs("</a></td><td class=\"num\" align=\"right\">", fp); - if (lc > 0) - fprintf(fp, "%zuL", lc); - else - fprintf(fp, "%zuB", filesize); - fputs("</td></tr>\n", fp); - git_object_free(obj); - } else if (git_tree_entry_type(entry) == GIT_OBJ_COMMIT) { - /* commit object in tree is a submodule */ - fprintf(fp, "<tr><td>m---------</td><td><a href=\"%sfile/.gitmodules.html\">", - relpath); - xmlencode(fp, entrypath, strlen(entrypath)); - fputs("</a> @ ", fp); - git_oid_tostr(oid, sizeof(oid), git_tree_entry_id(entry)); - xmlencode(fp, oid, strlen(oid)); - fputs("</td><td class=\"num\" align=\"right\"></td></tr>\n", fp); - } - } - - return 0; -} - -int -writefiles(FILE *fp, const git_oid *id) -{ - git_tree *tree = NULL; - git_commit *commit = NULL; - int ret = -1; - - fputs("<table id=\"files\"><thead>\n<tr>" - "<td><b>Mode</b></td><td><b>Name</b></td>" - "<td class=\"num\" align=\"right\"><b>Size</b></td>" - "</tr>\n</thead><tbody>\n", fp); - - if (!git_commit_lookup(&commit, repo, id) && - !git_commit_tree(&tree, commit)) - ret = writefilestree(fp, tree, ""); - - fputs("</tbody></table>", fp); - - git_commit_free(commit); - git_tree_free(tree); - - return ret; -} - -int -writerefs(FILE *fp) -{ - struct referenceinfo *ris = NULL; - struct commitinfo *ci; - size_t count, i, j, refcount; - const char *titles[] = { "Branches", "Tags" }; - const char *ids[] = { "branches", "tags" }; - const char *s; - - if (getrefs(&ris, &refcount) == -1) - return -1; - - for (i = 0, j = 0, count = 0; i < refcount; i++) { - if (j == 0 && git_reference_is_tag(ris[i].ref)) { - if (count) - fputs("</tbody></table><br/>\n", fp); - count = 0; - j = 1; - } - - /* print header if it has an entry (first). */ - if (++count == 1) { - 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); - xmlencode(fp, s, strlen(s)); - fputs("</td><td>", fp); - if (ci->author) - printtimeshort(fp, &(ci->author->when)); - fputs("</td><td>", fp); - if (ci->author) - xmlencode(fp, ci->author->name, strlen(ci->author->name)); - fputs("</td></tr>\n", fp); - } - /* table footer */ - if (count) - fputs("</tbody></table><br/>\n", fp); - - for (i = 0; i < refcount; i++) { - commitinfo_free(ris[i].ci); - git_reference_free(ris[i].ref); - } - free(ris); - - return 0; -} - -void -usage(char *argv0) -{ - fprintf(stderr, "usage: %s [-c cachefile | -l commits] " - "[-u baseurl] repodir\n", argv0); - exit(1); -} - -int -main(int argc, char *argv[]) -{ - git_object *obj = NULL; - const git_oid *head = NULL; - mode_t mask; - FILE *fp, *fpread; - char path[PATH_MAX], repodirabs[PATH_MAX + 1], *p; - char tmppath[64] = "cache.XXXXXXXXXXXX", buf[BUFSIZ]; - size_t n; - int i, fd; - - for (i = 1; i < argc; i++) { - if (argv[i][0] != '-') { - if (repodir) - usage(argv[0]); - repodir = argv[i]; - } else if (argv[i][1] == 'c') { - if (nlogcommits > 0 || i + 1 >= argc) - usage(argv[0]); - cachefile = argv[++i]; - } else if (argv[i][1] == 'l') { - if (cachefile || i + 1 >= argc) - usage(argv[0]); - errno = 0; - nlogcommits = strtoll(argv[++i], &p, 10); - if (argv[i][0] == '\0' || *p != '\0' || - nlogcommits <= 0 || errno) - usage(argv[0]); - } else if (argv[i][1] == 'u') { - if (i + 1 >= argc) - usage(argv[0]); - baseurl = argv[++i]; - } - } - if (!repodir) - usage(argv[0]); - - if (!realpath(repodir, repodirabs)) - err(1, "realpath"); - - /* do not search outside the git repository: - GIT_CONFIG_LEVEL_APP is the highest level currently */ - git_libgit2_init(); - for (i = 1; i <= GIT_CONFIG_LEVEL_APP; i++) - git_libgit2_opts(GIT_OPT_SET_SEARCH_PATH, i, ""); - /* do not require the git repository to be owned by the current user */ - git_libgit2_opts(GIT_OPT_SET_OWNER_VALIDATION, 0); - -#ifdef __OpenBSD__ - if (unveil(repodir, "r") == -1) - err(1, "unveil: %s", repodir); - if (unveil(".", "rwc") == -1) - err(1, "unveil: ."); - if (cachefile && unveil(cachefile, "rwc") == -1) - err(1, "unveil: %s", cachefile); - - if (cachefile) { - if (pledge("stdio rpath wpath cpath fattr", NULL) == -1) - err(1, "pledge"); - } else { - if (pledge("stdio rpath wpath cpath", NULL) == -1) - err(1, "pledge"); - } -#endif - - if (git_repository_open_ext(&repo, repodir, - GIT_REPOSITORY_OPEN_NO_SEARCH, NULL) < 0) { - fprintf(stderr, "%s: cannot open repository\n", argv[0]); - return 1; - } - - /* find HEAD */ - if (!git_revparse_single(&obj, repo, "HEAD")) - head = git_object_id(obj); - git_object_free(obj); - - /* use directory name as name */ - if ((name = strrchr(repodirabs, '/'))) - name++; - else - name = ""; - - /* strip .git suffix */ - if (!(strippedname = strdup(name))) - err(1, "strdup"); - if ((p = strrchr(strippedname, '.'))) - if (!strcmp(p, ".git")) - *p = '\0'; - - /* read description or .git/description */ - joinpath(path, sizeof(path), repodir, "description"); - if (!(fpread = fopen(path, "r"))) { - joinpath(path, sizeof(path), repodir, ".git/description"); - fpread = fopen(path, "r"); - } - if (fpread) { - if (!fgets(description, sizeof(description), fpread)) - description[0] = '\0'; - checkfileerror(fpread, path, 'r'); - fclose(fpread); - } - - /* read url or .git/url */ - joinpath(path, sizeof(path), repodir, "url"); - if (!(fpread = fopen(path, "r"))) { - joinpath(path, sizeof(path), repodir, ".git/url"); - fpread = fopen(path, "r"); - } - if (fpread) { - if (!fgets(cloneurl, sizeof(cloneurl), fpread)) - cloneurl[0] = '\0'; - checkfileerror(fpread, path, 'r'); - fclose(fpread); - cloneurl[strcspn(cloneurl, "\n")] = '\0'; - } - - /* check LICENSE */ - for (i = 0; i < LEN(licensefiles) && !license; i++) { - if (!git_revparse_single(&obj, repo, licensefiles[i]) && - git_object_type(obj) == GIT_OBJ_BLOB) - license = licensefiles[i] + strlen("HEAD:"); - git_object_free(obj); - } - - /* check README */ - for (i = 0; i < LEN(readmefiles) && !readme; i++) { - if (!git_revparse_single(&obj, repo, readmefiles[i]) && - git_object_type(obj) == GIT_OBJ_BLOB) - readme = readmefiles[i] + strlen("HEAD:"); - git_object_free(obj); - } - - if (!git_revparse_single(&obj, repo, "HEAD:.gitmodules") && - git_object_type(obj) == GIT_OBJ_BLOB) - submodules = ".gitmodules"; - git_object_free(obj); - - /* log for HEAD */ - fp = efopen("log.html", "w"); - relpath = ""; - mkdir("commit", S_IRWXU | S_IRWXG | S_IRWXO); - writeheader(fp, "Log"); - fputs("<table id=\"log\"><thead>\n<tr><td><b>Date</b></td>" - "<td><b>Commit message</b></td>" - "<td><b>Author</b></td><td class=\"num\" align=\"right\"><b>Files</b></td>" - "<td class=\"num\" align=\"right\"><b>+</b></td>" - "<td class=\"num\" align=\"right\"><b>-</b></td></tr>\n</thead><tbody>\n", fp); - - if (cachefile && head) { - /* read from cache file (does not need to exist) */ - if ((rcachefp = fopen(cachefile, "r"))) { - if (!fgets(lastoidstr, sizeof(lastoidstr), rcachefp)) - errx(1, "%s: no object id", cachefile); - if (git_oid_fromstr(&lastoid, lastoidstr)) - errx(1, "%s: invalid object id", cachefile); - } - - /* write log to (temporary) cache */ - if ((fd = mkstemp(tmppath)) == -1) - err(1, "mkstemp"); - if (!(wcachefp = fdopen(fd, "w"))) - err(1, "fdopen: '%s'", tmppath); - /* write last commit id (HEAD) */ - git_oid_tostr(buf, sizeof(buf), head); - fprintf(wcachefp, "%s\n", buf); - - writelog(fp, head); - - if (rcachefp) { - /* append previous log to log.html and the new cache */ - while (!feof(rcachefp)) { - n = fread(buf, 1, sizeof(buf), rcachefp); - if (ferror(rcachefp)) - break; - if (fwrite(buf, 1, n, fp) != n || - fwrite(buf, 1, n, wcachefp) != n) - break; - } - checkfileerror(rcachefp, cachefile, 'r'); - fclose(rcachefp); - } - checkfileerror(wcachefp, tmppath, 'w'); - fclose(wcachefp); - } else { - if (head) - writelog(fp, head); - } - - fputs("</tbody></table>", fp); - writefooter(fp); - checkfileerror(fp, "log.html", 'w'); - fclose(fp); - - /* files for HEAD */ - fp = efopen("files.html", "w"); - writeheader(fp, "Files"); - if (head) - writefiles(fp, head); - writefooter(fp); - checkfileerror(fp, "files.html", 'w'); - fclose(fp); - - /* summary page with branches and tags */ - fp = efopen("refs.html", "w"); - writeheader(fp, "Refs"); - writerefs(fp); - writefooter(fp); - checkfileerror(fp, "refs.html", 'w'); - fclose(fp); - - /* Atom feed */ - fp = efopen("atom.xml", "w"); - writeatom(fp, 1); - checkfileerror(fp, "atom.xml", 'w'); - fclose(fp); - - /* Atom feed for tags / releases */ - fp = efopen("tags.xml", "w"); - writeatom(fp, 0); - checkfileerror(fp, "tags.xml", 'w'); - fclose(fp); - - /* rename new cache file on success */ - if (cachefile && head) { - if (rename(tmppath, cachefile)) - err(1, "rename: '%s' to '%s'", tmppath, cachefile); - umask((mask = umask(0))); - if (chmod(cachefile, - (S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH) & ~mask)) - err(1, "chmod: '%s'", cachefile); - } - - /* cleanup */ - git_repository_free(repo); - git_libgit2_shutdown(); - - return 0; -} diff --git a/style.css b/style.css @@ -1,154 +0,0 @@ -body { - color: #000; - background-color: #fff; - font-family: monospace; -} - -h1, h2, h3, h4, h5, h6 { - font-size: 1em; - margin: 0; -} - -img, h1, h2 { - vertical-align: middle; -} - -img { - border: 0; -} - -a:target { - background-color: #ccc; -} - -a.d, -a.h, -a.i, -a.line { - text-decoration: none; -} - -#blob a { - color: #555; -} - -#blob a:hover { - color: blue; - text-decoration: none; -} - -table thead td { - font-weight: bold; -} - -table td { - padding: 0 0.4em; -} - -#content table td { - vertical-align: top; - white-space: nowrap; -} - -#branches tr:hover td, -#tags tr:hover td, -#index tr:hover td, -#log tr:hover td, -#files tr:hover td { - background-color: #eee; -} - -#index tr td:nth-child(2), -#tags tr td:nth-child(3), -#branches tr td:nth-child(3), -#log tr td:nth-child(2) { - white-space: normal; -} - -td.num { - text-align: right; -} - -.desc { - color: #555; -} - -hr { - border: 0; - border-top: 1px solid #555; - height: 1px; -} - -pre { - font-family: monospace; -} - -pre a.h { - color: #00a; -} - -.A, -span.i, -pre a.i { - color: #070; -} - -.D, -span.d, -pre a.d { - color: #e00; -} - -pre a.h:hover, -pre a.i:hover, -pre a.d:hover { - text-decoration: none; -} - -@media (prefers-color-scheme: dark) { - body { - background-color: #000; - color: #bdbdbd; - } - hr { - border-color: #222; - } - a { - color: #56c8ff; - } - a:target { - background-color: #222; - } - .desc { - color: #aaa; - } - #blob a { - color: #555; - } - #blob a:target { - color: #eee; - } - #blob a:hover { - color: #56c8ff; - } - pre a.h { - color: #00cdcd; - } - .A, - span.i, - pre a.i { - color: #00cd00; - } - .D, - span.d, - pre a.d { - color: #cd0000; - } - #branches tr:hover td, - #tags tr:hover td, - #index tr:hover td, - #log tr:hover td, - #files tr:hover td { - background-color: #111; - } -}