pax_global_header00006660000000000000000000000064152520307170014514gustar00rootroot0000000000000052 comment=519cae2fcab6f2427dc8f2805d6a5d1eee4bdb05 stagit-1.3/000077500000000000000000000000001525203071700126525ustar00rootroot00000000000000stagit-1.3/LICENSE000066400000000000000000000021221525203071700136540ustar00rootroot00000000000000MIT/X Consortium License (c) 2015-2026 Hiltjo Posthuma Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. stagit-1.3/Makefile000066400000000000000000000051411525203071700143130ustar00rootroot00000000000000.POSIX: NAME = stagit VERSION = 1.3 # 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 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} 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 stagit-1.3/README000066400000000000000000000106161525203071700135360ustar00rootroot00000000000000stagit ------ 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. stagit-1.3/compat.h000066400000000000000000000002721525203071700143070ustar00rootroot00000000000000#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); stagit-1.3/example_create.sh000077500000000000000000000022101525203071700161620ustar00rootroot00000000000000#!/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 stagit-1.3/example_post-receive.sh000077500000000000000000000033241525203071700173330ustar00rootroot00000000000000#!/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" stagit-1.3/favicon.png000066400000000000000000000001471525203071700150070ustar00rootroot00000000000000‰PNG  IHDR``ØVå{PLTEßßß1cYBtRNS@æØfIDATc`£`Œ‚á à«ñâIEND®B`‚stagit-1.3/logo.png000066400000000000000000000001471525203071700143220ustar00rootroot00000000000000‰PNG  IHDR``ØVå{PLTEßßß1cYBtRNS@æØfIDATc`£`Œ‚á à«ñâIEND®B`‚stagit-1.3/reallocarray.c000066400000000000000000000024321525203071700154770ustar00rootroot00000000000000/* * Copyright (c) 2008 Otto Moerbeek * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ #include #include #include #include #include "compat.h" /* * This is sqrt(SIZE_MAX+1), as s1*s2 <= SIZE_MAX * if both s1 < MUL_NO_OVERFLOW and s2 < MUL_NO_OVERFLOW */ #define MUL_NO_OVERFLOW (1UL << (sizeof(size_t) * 4)) void * reallocarray(void *optr, size_t nmemb, size_t size) { if ((nmemb >= MUL_NO_OVERFLOW || size >= MUL_NO_OVERFLOW) && nmemb > 0 && SIZE_MAX / nmemb < size) { errno = ENOMEM; return NULL; } return realloc(optr, size * nmemb); } stagit-1.3/stagit-index.1000066400000000000000000000021101525203071700153260ustar00rootroot00000000000000.Dd May 29, 2026 .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 following files specifies the metadata 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 stagit-1.3/stagit-index.c000066400000000000000000000142161525203071700154220ustar00rootroot00000000000000#include #include #include #include #include #include #include #include static git_repository *repo; static const char *relpath = ""; static char description[255] = "Repositories"; static char *name = ""; static char owner[255]; /* 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("\n" "\n\n" "\n" "\n" "", fp); xmlencode(fp, description, strlen(description)); fprintf(fp, "\n\n", relpath); fprintf(fp, "\n", relpath); fputs("\n\n", fp); fprintf(fp, "\n\n" "\n
\"\"", relpath); xmlencode(fp, description, strlen(description)); fputs("
\n" "
\n
\n
\n" "\n" "" "" "\n", fp); } void writefooter(FILE *fp) { fputs("\n
NameDescriptionOwnerLast commit
\n
\n\n\n", fp); } int writelog(FILE *fp) { git_commit *commit = NULL; const git_signature *author; git_revwalk *w = NULL; git_oid id; char *stripped_name = NULL, *p; int ret = 0; git_revwalk_new(&w, repo); git_revwalk_push_head(w); if (git_revwalk_next(&id, w) || git_commit_lookup(&commit, repo, &id)) { ret = -1; goto err; } author = git_commit_author(commit); /* strip .git suffix */ if (!(stripped_name = strdup(name))) err(1, "strdup"); if ((p = strrchr(stripped_name, '.'))) if (!strcmp(p, ".git")) *p = '\0'; fputs("", fp); xmlencode(fp, stripped_name, strlen(stripped_name)); fputs("", fp); xmlencode(fp, description, strlen(description)); fputs("", fp); xmlencode(fp, owner, strlen(owner)); fputs("", fp); if (author) printtimeshort(fp, &(author->when)); fputs("", fp); git_commit_free(commit); err: git_revwalk_free(w); free(stripped_name); return ret; } int main(int argc, char *argv[]) { FILE *fp; char path[PATH_MAX], repodirabs[PATH_MAX + 1]; const char *repodir; int i, ret = 0; if (argc < 2) { fprintf(stderr, "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, "", 'w'); return ret; } stagit-1.3/stagit.1000066400000000000000000000066661525203071700142450ustar00rootroot00000000000000.Dd May 29, 2026 .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 following 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 stagit-1.3/stagit.c000066400000000000000000001067121525203071700143200ustar00rootroot00000000000000#include #include #include #include #include #include #include #include #include #include #include #include #include #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:LICENSE.txt", "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("<", fp); break; case '>': fputs(">", fp); break; case '\'': fputs("'", fp); break; case '&': fputs("&", fp); break; case '"': fputs(""", 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("<", fp); break; case '>': fputs(">", fp); break; case '\'': fputs("'", fp); break; case '&': fputs("&", fp); break; case '"': fputs(""", 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("\n" "\n\n" "\n" "\n" "", fp); xmlencode(fp, title, strlen(title)); if (title[0] && strippedname[0]) fputs(" - ", fp); xmlencode(fp, strippedname, strlen(strippedname)); if (description[0]) fputs(" - ", fp); xmlencode(fp, description, strlen(description)); fprintf(fp, "\n\n", relpath); fputs("\n", relpath); fputs("\n", relpath); fprintf(fp, "\n", relpath); fputs("\n\n", fp); if (cloneurl[0]) { fputs("", fp); } fputs("
", fp); fprintf(fp, "\"\"", relpath, relpath); fputs("

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

", fp); xmlencode(fp, description, strlen(description)); fputs("
git clone ", fp); xmlencode(fp, cloneurl, strlen(cloneurl)); fputs("
\n", fp); fprintf(fp, "Log | ", relpath); fprintf(fp, "Files | ", relpath); fprintf(fp, "Refs", relpath); if (submodules) fprintf(fp, " | Submodules", relpath, submodules); if (readme) fprintf(fp, " | README", relpath, readme); if (license) fprintf(fp, " | LICENSE", relpath, license); fputs("
\n
\n
\n", fp); } void writefooter(FILE *fp) { fputs("
\n\n\n", fp); } size_t writeblobhtml(FILE *fp, const git_blob *blob) { size_t n = 0, i, len, prev; const char *nfmt = "%7zu "; const char *s = git_blob_rawcontent(blob); len = git_blob_rawsize(blob); fputs("
\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("
\n", fp); return n; } void printcommit(FILE *fp, struct commitinfo *ci) { fprintf(fp, "commit %s\n", relpath, ci->oid, ci->oid); if (ci->parentoid[0]) fprintf(fp, "parent %s\n", relpath, ci->parentoid, ci->parentoid); if (ci->author) { fputs("Author: ", fp); xmlencode(fp, ci->author->name, strlen(ci->author->name)); fputs(" <author->email, strlen(ci->author->email)); /* not percent-encoded */ fputs("\">", fp); xmlencode(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); 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("Diffstat:\n", fp); for (i = 0; i < ci->ndeltas; i++) { delta = git_patch_get_delta(ci->deltas[i]->patch); switch (delta->status) { case GIT_DELTA_ADDED: c = 'A'; break; case GIT_DELTA_COPIED: c = 'C'; break; case GIT_DELTA_DELETED: c = 'D'; break; case GIT_DELTA_MODIFIED: c = 'M'; break; case GIT_DELTA_RENAMED: c = 'R'; break; case GIT_DELTA_TYPECHANGE: c = 'T'; break; default: c = ' '; break; } if (c == ' ') fprintf(fp, "\n", fp); } fprintf(fp, "
%c", c); else fprintf(fp, "
%c", c, c); fprintf(fp, "", i); xmlencode(fp, delta->old_file.path, strlen(delta->old_file.path)); if (strcmp(delta->old_file.path, delta->new_file.path)) { fputs(" -> ", fp); xmlencode(fp, delta->new_file.path, strlen(delta->new_file.path)); } add = ci->deltas[i]->addcount; del = ci->deltas[i]->delcount; changed = add + del; total = sizeof(linestr) - 2; if (changed > total) { if (add) add = ((float)total / changed * add) + 1; if (del) del = ((float)total / changed * del) + 1; } memset(&linestr, '+', add); memset(&linestr[add], '-', del); fprintf(fp, " | %zu", ci->deltas[i]->addcount + ci->deltas[i]->delcount); fwrite(&linestr, 1, add, fp); fputs("", fp); fwrite(&linestr[add], 1, del, fp); fputs("
%zu file%s changed, %zu insertion%s(+), %zu deletion%s(-)\n",
		ci->filecount, ci->filecount == 1 ? "" : "s",
	        ci->addcount,  ci->addcount  == 1 ? "" : "s",
	        ci->delcount,  ci->delcount  == 1 ? "" : "s");

	fputs("
", fp); for (i = 0; i < ci->ndeltas; i++) { patch = ci->deltas[i]->patch; delta = git_patch_get_delta(patch); fprintf(fp, "diff --git a/old_file.path, strlen(delta->old_file.path)); fputs(".html\">", fp); xmlencode(fp, delta->old_file.path, strlen(delta->old_file.path)); fprintf(fp, " b/new_file.path, strlen(delta->new_file.path)); fprintf(fp, ".html\">"); xmlencode(fp, delta->new_file.path, strlen(delta->new_file.path)); fprintf(fp, "\n"); /* check binary data */ if (delta->flags & GIT_DIFF_FLAG_BINARY) { fputs("Binary files differ.\n", fp); continue; } nhunks = git_patch_num_hunks(patch); for (j = 0; j < nhunks; j++) { if (git_patch_get_hunk(&hunk, &nhunklines, patch, j)) break; fprintf(fp, "", i, j, i, j); xmlencode(fp, hunk->header, hunk->header_len); fputs("", fp); for (k = 0; ; k++) { if (git_patch_get_line_in_hunk(&line, patch, j, k)) break; if (line->old_lineno == -1) fprintf(fp, "+", i, j, k, i, j, k); else if (line->new_lineno == -1) fprintf(fp, "-", i, j, k, i, j, k); else putc(' ', fp); xmlencodeline(fp, line->content, line->content_len); putc('\n', fp); if (line->old_lineno == -1 || line->new_lineno == -1) fputs("", fp); } } } } void writelogline(FILE *fp, struct commitinfo *ci) { fputs("", fp); if (ci->author) printtimeshort(fp, &(ci->author->when)); fputs("", fp); if (ci->summary) { fprintf(fp, "", relpath, ci->oid); xmlencode(fp, ci->summary, strlen(ci->summary)); fputs("", fp); } fputs("", fp); if (ci->author) xmlencode(fp, ci->author->name, strlen(ci->author->name)); fputs("", fp); fprintf(fp, "%zu", ci->filecount); fputs("", fp); fprintf(fp, "+%zu", ci->addcount); fputs("", fp); fprintf(fp, "-%zu", ci->delcount); fputs("\n", fp); } int writelog(FILE *fp, const git_oid *oid) { struct commitinfo *ci; git_revwalk *w = NULL; git_oid id; char path[PATH_MAX], oidstr[GIT_OID_HEXSZ + 1]; FILE *fpfile; 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("
", fpfile);
			printshowfile(fpfile, ci);
			fputs("
\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, "" "%zu more commits remaining, fetch the repository" "\n", remcommits); } relpath = ""; return 0; } void printcommitatom(FILE *fp, struct commitinfo *ci, const char *tag) { fputs("\n", fp); fprintf(fp, "%s\n", ci->oid); if (ci->author) { fputs("", fp); printtimez(fp, &(ci->author->when)); fputs("\n", fp); } if (ci->committer) { fputs("", fp); printtimez(fp, &(ci->committer->when)); fputs("\n", fp); } if (ci->summary) { fputs("", fp); if (tag && tag[0]) { fputs("[", fp); xmlencode(fp, tag, strlen(tag)); fputs("] ", fp); } xmlencode(fp, ci->summary, strlen(ci->summary)); fputs("\n", fp); } fprintf(fp, "\n", baseurl, ci->oid); if (ci->author) { fputs("\n", fp); xmlencode(fp, ci->author->name, strlen(ci->author->name)); fputs("\n", fp); xmlencode(fp, ci->author->email, strlen(ci->author->email)); fputs("\n\n", fp); } fputs("", fp); fprintf(fp, "commit %s\n", ci->oid); if (ci->parentoid[0]) fprintf(fp, "parent %s\n", ci->parentoid); if (ci->author) { fputs("Author: ", fp); xmlencode(fp, ci->author->name, strlen(ci->author->name)); fputs(" <", fp); xmlencode(fp, ci->author->email, strlen(ci->author->email)); fputs(">\nDate: ", fp); printtime(fp, &(ci->author->when)); putc('\n', fp); } if (ci->msg) { putc('\n', fp); xmlencode(fp, ci->msg, strlen(ci->msg)); } fputs("\n\n\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("\n" "\n", fp); xmlencode(fp, strippedname, strlen(strippedname)); fputs(", branch HEAD\n", fp); xmlencode(fp, description, strlen(description)); fputs("\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("\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("

", fp); xmlencode(fp, filename, strlen(filename)); fprintf(fp, " (%zuB)", filesize); fputs("


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

Binary file.

\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("", fp); fputs(filemode(git_tree_entry_filemode(entry)), fp); fprintf(fp, "", fp); xmlencode(fp, entrypath, strlen(entrypath)); fputs("", fp); if (lc > 0) fprintf(fp, "%zuL", lc); else fprintf(fp, "%zuB", filesize); fputs("\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, "m---------", relpath); xmlencode(fp, entrypath, strlen(entrypath)); fputs(" @ ", fp); git_oid_tostr(oid, sizeof(oid), git_tree_entry_id(entry)); xmlencode(fp, oid, strlen(oid)); fputs("\n", fp); } } return 0; } int writefiles(FILE *fp, const git_oid *id) { git_tree *tree = NULL; git_commit *commit = NULL; int ret = -1; fputs("\n" "" "" "\n\n", fp); if (!git_commit_lookup(&commit, repo, id) && !git_commit_tree(&tree, commit)) ret = writefilestree(fp, tree, ""); fputs("
ModeNameSize
", fp); git_commit_free(commit); git_tree_free(tree); return ret; } int 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("
\n", fp); count = 0; j = 1; } /* print header if it has an entry (first). */ if (++count == 1) { fprintf(fp, "

%s

" "\n" "" "\n\n" "\n", titles[j], ids[j]); } ci = ris[i].ci; s = git_reference_shorthand(ris[i].ref); fputs("\n", fp); } /* table footer */ if (count) fputs("
NameLast commit dateAuthor
", fp); xmlencode(fp, s, strlen(s)); fputs("", fp); if (ci->author) printtimeshort(fp, &(ci->author->when)); fputs("", fp); if (ci->author) xmlencode(fp, ci->author->name, strlen(ci->author->name)); fputs("

\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); /* NOTE: do not git_object_free(), it is used below */ /* 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("\n" "" "" "" "\n\n", fp); if (cachefile && head) { /* read from cache file (does not need to exist) */ if ((rcachefp = fopen(cachefile, "r"))) { if (!fgets(lastoidstr, sizeof(lastoidstr), rcachefp)) errx(1, "%s: no object id", cachefile); if (git_oid_fromstr(&lastoid, lastoidstr)) errx(1, "%s: invalid object id", cachefile); } /* write log to (temporary) cache */ if ((fd = mkstemp(tmppath)) == -1) err(1, "mkstemp"); if (!(wcachefp = fdopen(fd, "w"))) err(1, "fdopen: '%s'", tmppath); /* write last commit id (HEAD) */ git_oid_tostr(buf, sizeof(buf), head); fprintf(wcachefp, "%s\n", buf); writelog(fp, head); if (rcachefp) { /* append previous log to log.html and the new cache */ while (!feof(rcachefp)) { n = fread(buf, 1, sizeof(buf), rcachefp); if (ferror(rcachefp)) 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("
DateCommit messageAuthorFiles+-
", 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; } stagit-1.3/strlcat.c000066400000000000000000000033471525203071700145010ustar00rootroot00000000000000/* $OpenBSD: strlcat.c,v 1.15 2015/03/02 21:41:08 millert Exp $ */ /* * Copyright (c) 1998, 2015 Todd C. Miller * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ #include #include #include "compat.h" /* * Appends src to string dst of size dsize (unlike strncat, dsize is the * full size of dst, not space left). At most dsize-1 characters * will be copied. Always NUL terminates (unless dsize <= strlen(dst)). * Returns strlen(src) + MIN(dsize, strlen(initial dst)). * If retval >= dsize, truncation occurred. */ size_t strlcat(char *dst, const char *src, size_t dsize) { const char *odst = dst; const char *osrc = src; size_t n = dsize; size_t dlen; /* Find the end of dst and adjust bytes left but don't go past end. */ while (n-- != 0 && *dst != '\0') dst++; dlen = dst - odst; n = dsize - dlen; if (n-- == 0) return(dlen + strlen(src)); while (*src != '\0') { if (n != 0) { *dst++ = *src; n--; } src++; } *dst = '\0'; return(dlen + (src - osrc)); /* count does not include NUL */ } stagit-1.3/strlcpy.c000066400000000000000000000031241525203071700145160ustar00rootroot00000000000000/* $OpenBSD: strlcpy.c,v 1.12 2015/01/15 03:54:12 millert Exp $ */ /* * Copyright (c) 1998, 2015 Todd C. Miller * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ #include #include #include "compat.h" /* * Copy string src to buffer dst of size dsize. At most dsize-1 * chars will be copied. Always NUL terminates (unless dsize == 0). * Returns strlen(src); if retval >= dsize, truncation occurred. */ size_t strlcpy(char *dst, const char *src, size_t dsize) { const char *osrc = src; size_t nleft = dsize; /* Copy as many bytes as will fit. */ if (nleft != 0) { while (--nleft != 0) { if ((*dst++ = *src++) == '\0') break; } } /* Not enough room in dst, add NUL and traverse rest of src. */ if (nleft == 0) { if (dsize != 0) *dst = '\0'; /* NUL-terminate dst */ while (*src++) ; } return(src - osrc - 1); /* count does not include NUL */ } stagit-1.3/style.css000066400000000000000000000032251525203071700145260ustar00rootroot00000000000000body { 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; } }