├── LICENSE ├── Makefile ├── README ├── compat.h ├── example_create.sh ├── example_post-receive.sh ├── favicon.png ├── logo.png ├── reallocarray.c ├── stagit-index.1 ├── stagit-index.c ├── stagit.1 ├── stagit.c ├── strlcat.c ├── strlcpy.c └── style.css /LICENSE: -------------------------------------------------------------------------------- 1 | MIT/X Consortium License 2 | 3 | (c) 2015-2019 Hiltjo Posthuma 4 | (c) 2015-2016 Dimitris Papastamos 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a 7 | copy of this software and associated documentation files (the "Software"), 8 | to deal in the Software without restriction, including without limitation 9 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 10 | and/or sell copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 19 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 22 | DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .POSIX: 2 | 3 | NAME = stagit 4 | VERSION = 0.9.3 5 | 6 | # paths 7 | PREFIX = /usr/local 8 | MANPREFIX = ${PREFIX}/man 9 | DOCPREFIX = ${PREFIX}/share/doc/${NAME} 10 | 11 | LIBGIT_INC = -I/usr/local/include 12 | LIBGIT_LIB = -L/usr/local/lib -lgit2 13 | 14 | # use system flags. 15 | STAGIT_CFLAGS = ${LIBGIT_INC} ${CFLAGS} 16 | STAGIT_LDFLAGS = ${LIBGIT_LIB} ${LDFLAGS} 17 | STAGIT_CPPFLAGS = -D_XOPEN_SOURCE=700 -D_DEFAULT_SOURCE -D_BSD_SOURCE 18 | 19 | SRC = \ 20 | stagit.c\ 21 | stagit-index.c 22 | COMPATSRC = \ 23 | reallocarray.c\ 24 | strlcat.c\ 25 | strlcpy.c 26 | BIN = \ 27 | stagit\ 28 | stagit-index 29 | MAN1 = \ 30 | stagit.1\ 31 | stagit-index.1 32 | DOC = \ 33 | LICENSE\ 34 | README 35 | HDR = compat.h 36 | 37 | COMPATOBJ = \ 38 | reallocarray.o\ 39 | strlcat.o\ 40 | strlcpy.o 41 | 42 | OBJ = ${SRC:.c=.o} ${COMPATOBJ} 43 | 44 | all: ${BIN} 45 | 46 | .o: 47 | ${CC} -o $@ ${LDFLAGS} 48 | 49 | .c.o: 50 | ${CC} -o $@ -c $< ${STAGIT_CFLAGS} ${STAGIT_CPPFLAGS} 51 | 52 | dist: 53 | rm -rf ${NAME}-${VERSION} 54 | mkdir -p ${NAME}-${VERSION} 55 | cp -f ${MAN1} ${HDR} ${SRC} ${COMPATSRC} ${DOC} \ 56 | Makefile favicon.png logo.png style.css \ 57 | example_create.sh example_post-receive.sh \ 58 | ${NAME}-${VERSION} 59 | # make tarball 60 | tar -cf - ${NAME}-${VERSION} | \ 61 | gzip -c > ${NAME}-${VERSION}.tar.gz 62 | rm -rf ${NAME}-${VERSION} 63 | 64 | ${OBJ}: ${HDR} 65 | 66 | stagit: stagit.o ${COMPATOBJ} 67 | ${CC} -o $@ stagit.o ${COMPATOBJ} ${STAGIT_LDFLAGS} 68 | 69 | stagit-index: stagit-index.o ${COMPATOBJ} 70 | ${CC} -o $@ stagit-index.o ${COMPATOBJ} ${STAGIT_LDFLAGS} 71 | 72 | clean: 73 | rm -f ${BIN} ${OBJ} ${NAME}-${VERSION}.tar.gz 74 | 75 | install: all 76 | # installing executable files. 77 | mkdir -p ${DESTDIR}${PREFIX}/bin 78 | cp -f ${BIN} ${DESTDIR}${PREFIX}/bin 79 | for f in ${BIN}; do chmod 755 ${DESTDIR}${PREFIX}/bin/$$f; done 80 | # installing example files. 81 | mkdir -p ${DESTDIR}${DOCPREFIX} 82 | cp -f style.css\ 83 | favicon.png\ 84 | logo.png\ 85 | example_create.sh\ 86 | example_post-receive.sh\ 87 | README\ 88 | ${DESTDIR}${DOCPREFIX} 89 | # installing manual pages. 90 | mkdir -p ${DESTDIR}${MANPREFIX}/man1 91 | cp -f ${MAN1} ${DESTDIR}${MANPREFIX}/man1 92 | for m in ${MAN1}; do chmod 644 ${DESTDIR}${MANPREFIX}/man1/$$m; done 93 | 94 | uninstall: 95 | # removing executable files. 96 | for f in ${BIN}; do rm -f ${DESTDIR}${PREFIX}/bin/$$f; done 97 | # removing example files. 98 | rm -f \ 99 | ${DESTDIR}${DOCPREFIX}/style.css\ 100 | ${DESTDIR}${DOCPREFIX}/favicon.png\ 101 | ${DESTDIR}${DOCPREFIX}/logo.png\ 102 | ${DESTDIR}${DOCPREFIX}/example_create.sh\ 103 | ${DESTDIR}${DOCPREFIX}/example_post-receive.sh\ 104 | ${DESTDIR}${DOCPREFIX}/README 105 | -rmdir ${DESTDIR}${DOCPREFIX} 106 | # removing manual pages. 107 | for m in ${MAN1}; do rm -f ${DESTDIR}${MANPREFIX}/man1/$$m; done 108 | 109 | .PHONY: all clean dist install uninstall 110 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | stagit 2 | ------ 3 | 4 | static git page generator. 5 | 6 | It generates static HTML pages for a git repository. 7 | 8 | 9 | Usage 10 | ----- 11 | 12 | Make files per repository: 13 | 14 | $ mkdir -p htmldir && cd htmldir 15 | $ stagit path-to-repo 16 | 17 | Make index file for repositories: 18 | 19 | $ stagit-index repodir1 repodir2 repodir3 > index.html 20 | 21 | 22 | Build and install 23 | ----------------- 24 | 25 | $ make 26 | # make install 27 | 28 | 29 | Dependencies 30 | ------------ 31 | 32 | - C compiler (C99). 33 | - libc (tested with OpenBSD, FreeBSD, NetBSD, Linux: glibc and musl). 34 | - libgit2 (v0.22+). 35 | - POSIX make (optional). 36 | 37 | 38 | Documentation 39 | ------------- 40 | 41 | See man pages: stagit(1) and stagit-index(1). 42 | 43 | 44 | Building a static binary 45 | ------------------------ 46 | 47 | It may be useful to build static binaries, for example to run in a chroot. 48 | 49 | It can be done like this at the time of writing (v0.24): 50 | 51 | cd libgit2-src 52 | 53 | # change the options in the CMake file: CMakeLists.txt 54 | BUILD_SHARED_LIBS to OFF (static) 55 | CURL to OFF (not needed) 56 | USE_SSH OFF (not needed) 57 | THREADSAFE OFF (not needed) 58 | USE_OPENSSL OFF (not needed, use builtin) 59 | 60 | mkdir -p build && cd build 61 | cmake ../ 62 | make 63 | make install 64 | 65 | 66 | Extract owner field from git config 67 | ----------------------------------- 68 | 69 | A way to extract the gitweb owner for example in the format: 70 | 71 | [gitweb] 72 | owner = Name here 73 | 74 | Script: 75 | 76 | #!/bin/sh 77 | awk '/^[ ]*owner[ ]=/ { 78 | sub(/^[^=]*=[ ]*/, ""); 79 | print $0; 80 | }' 81 | 82 | 83 | Set clone url for a directory of repos 84 | -------------------------------------- 85 | #!/bin/sh 86 | cd "$dir" 87 | for i in *; do 88 | test -d "$i" && echo "git://git.codemadness.org/$i" > "$i/url" 89 | done 90 | 91 | 92 | Update files on git push 93 | ------------------------ 94 | 95 | Using a post-receive hook the static files can be automatically updated. 96 | Keep in mind git push -f can change the history and the commits may need 97 | to be recreated. This is because stagit checks if a commit file already 98 | exists. It also has a cache (-c) option which can conflict with the new 99 | history. See stagit(1). 100 | 101 | git post-receive hook (repo/.git/hooks/post-receive): 102 | 103 | #!/bin/sh 104 | # detect git push -f 105 | force=0 106 | while read -r old new ref; do 107 | hasrevs=$(git rev-list "$old" "^$new" | sed 1q) 108 | if test -n "$hasrevs"; then 109 | force=1 110 | break 111 | fi 112 | done 113 | 114 | # remove commits and .cache on git push -f 115 | #if test "$force" = "1"; then 116 | # ... 117 | #fi 118 | 119 | # see example_create.sh for normal creation of the files. 120 | 121 | 122 | Create .tar.gz archives by tag 123 | ------------------------------ 124 | #!/bin/sh 125 | name="stagit" 126 | mkdir -p archives 127 | git tag -l | while read -r t; do 128 | f="archives/${name}-$(echo "${t}" | tr '/' '_').tar.gz" 129 | test -f "${f}" && continue 130 | git archive \ 131 | --format tar.gz \ 132 | --prefix "${t}/" \ 133 | -o "${f}" \ 134 | -- \ 135 | "${t}" 136 | done 137 | 138 | 139 | Features 140 | -------- 141 | 142 | - Log of all commits from HEAD. 143 | - Log and diffstat per commit. 144 | - Show file tree with linkable line numbers. 145 | - Show references: local branches and tags. 146 | - Detect README and LICENSE file from HEAD and link it as a webpage. 147 | - Detect submodules (.gitmodules file) from HEAD and link it as a webpage. 148 | - Atom feed log (atom.xml). 149 | - Make index page for multiple repositories with stagit-index. 150 | - After generating the pages (relatively slow) serving the files is very fast, 151 | simple and requires little resources (because the content is static), only 152 | a HTTP file server is required. 153 | - Usable with text-browsers such as dillo, links, lynx and w3m. 154 | 155 | 156 | Cons 157 | ---- 158 | 159 | - Not suitable for large repositories (2000+ commits), because diffstats are 160 | an expensive operation, the cache (-c flag) is a workaround for this in 161 | some cases. 162 | - Not suitable for large repositories with many files, because all files are 163 | written for each execution of stagit. This is because stagit shows the lines 164 | of textfiles and there is no "cache" for file metadata (this would add more 165 | complexity to the code). 166 | - Not suitable for repositories with many branches, a quite linear history is 167 | assumed (from HEAD). 168 | 169 | In these cases it is better to just use cgit or possibly change stagit to 170 | run as a CGI program. 171 | 172 | - Relatively slow to run the first time (about 3 seconds for sbase, 173 | 1500+ commits), incremental updates are faster. 174 | - Does not support some of the dynamic features cgit has, like: 175 | - Snapshot tarballs per commit. 176 | - File tree per commit. 177 | - History log of branches diverged from HEAD. 178 | - Stats (git shortlog -s). 179 | 180 | This is by design, just use git locally. 181 | -------------------------------------------------------------------------------- /compat.h: -------------------------------------------------------------------------------- 1 | #undef strlcat 2 | size_t strlcat(char *, const char *, size_t); 3 | #undef strlcpy 4 | size_t strlcpy(char *, const char *, size_t); 5 | #undef reallocarray 6 | void *reallocarray(void *, size_t, size_t); 7 | -------------------------------------------------------------------------------- /example_create.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # - Makes index for repositories in a single directory. 3 | # - Makes static pages for each repository directory. 4 | # 5 | # NOTE, things to do manually (once) before running this script: 6 | # - copy style.css, logo.png and favicon.png manually, a style.css example 7 | # is included. 8 | # 9 | # - write clone url, for example "git://git.codemadness.org/dir" to the "url" 10 | # file for each repo. 11 | # - write owner of repo to the "owner" file. 12 | # - write description in "description" file. 13 | # 14 | # Usage: 15 | # - mkdir -p htmldir && cd htmldir 16 | # - sh example_create.sh 17 | 18 | # path must be absolute. 19 | reposdir="/var/www/domains/git.codemadness.nl/home/src" 20 | curdir="$(pwd)" 21 | 22 | # make index. 23 | stagit-index "${reposdir}/"*/ > "${curdir}/index.html" 24 | 25 | # make files per repo. 26 | for dir in "${reposdir}/"*/; do 27 | # strip .git suffix. 28 | r=$(basename "${dir}") 29 | d=$(basename "${dir}" ".git") 30 | printf "%s... " "${d}" 31 | 32 | mkdir -p "${curdir}/${d}" 33 | cd "${curdir}/${d}" || continue 34 | stagit -c ".cache" "${reposdir}/${r}" 35 | 36 | # symlinks 37 | ln -sf log.html index.html 38 | ln -sf ../style.css style.css 39 | ln -sf ../logo.png logo.png 40 | ln -sf ../favicon.png favicon.png 41 | 42 | echo "done" 43 | done 44 | -------------------------------------------------------------------------------- /example_post-receive.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # generic git post-receive hook. 3 | # change the config options below and call this script in your post-receive 4 | # hook or symlink it. 5 | # 6 | # usage: $0 [name] 7 | # 8 | # if name is not set the basename of the current directory is used, 9 | # this is the directory of the repo when called from the post-receive script. 10 | 11 | # NOTE: needs to be set for correct locale (expects UTF-8) otherwise the 12 | # default is LC_CTYPE="POSIX". 13 | export LC_CTYPE="en_US.UTF-8" 14 | 15 | name="$1" 16 | if test "${name}" = ""; then 17 | name=$(basename "$(pwd)") 18 | fi 19 | 20 | # config 21 | # paths must be absolute. 22 | reposdir="/home/src/src" 23 | dir="${reposdir}/${name}" 24 | htmldir="/home/www/domains/git.codemadness.org/htdocs" 25 | stagitdir="/" 26 | destdir="${htmldir}${stagitdir}" 27 | cachefile=".htmlcache" 28 | # /config 29 | 30 | if ! test -d "${dir}"; then 31 | echo "${dir} does not exist" >&2 32 | exit 1 33 | fi 34 | cd "${dir}" || exit 1 35 | 36 | # detect git push -f 37 | force=0 38 | while read -r old new ref; do 39 | test "${old}" = "0000000000000000000000000000000000000000" && continue 40 | test "${new}" = "0000000000000000000000000000000000000000" && continue 41 | 42 | hasrevs=$(git rev-list "${old}" "^${new}" | sed 1q) 43 | if test -n "${hasrevs}"; then 44 | force=1 45 | break 46 | fi 47 | done 48 | 49 | # strip .git suffix. 50 | r=$(basename "${name}") 51 | d=$(basename "${name}" ".git") 52 | printf "[%s] stagit HTML pages... " "${d}" 53 | 54 | mkdir -p "${destdir}/${d}" 55 | cd "${destdir}/${d}" || exit 1 56 | 57 | # remove commits and ${cachefile} on git push -f, this recreated later on. 58 | if test "${force}" = "1"; then 59 | rm -f "${cachefile}" 60 | rm -rf "commit" 61 | fi 62 | 63 | # make index. 64 | stagit-index "${reposdir}/"*/ > "${destdir}/index.html" 65 | 66 | # make pages. 67 | stagit -c "${cachefile}" "${reposdir}/${r}" 68 | 69 | ln -sf log.html index.html 70 | ln -sf ../style.css style.css 71 | ln -sf ../logo.png logo.png 72 | 73 | echo "done" 74 | -------------------------------------------------------------------------------- /favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxalorg/stagit/5334f3e0009bb7d5835c3bad60db507bfd146930/favicon.png -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxalorg/stagit/5334f3e0009bb7d5835c3bad60db507bfd146930/logo.png -------------------------------------------------------------------------------- /reallocarray.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2008 Otto Moerbeek 3 | * 4 | * Permission to use, copy, modify, and distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | #include 18 | #include 19 | #include 20 | #include 21 | 22 | #include "compat.h" 23 | 24 | /* 25 | * This is sqrt(SIZE_MAX+1), as s1*s2 <= SIZE_MAX 26 | * if both s1 < MUL_NO_OVERFLOW and s2 < MUL_NO_OVERFLOW 27 | */ 28 | #define MUL_NO_OVERFLOW (1UL << (sizeof(size_t) * 4)) 29 | 30 | void * 31 | reallocarray(void *optr, size_t nmemb, size_t size) 32 | { 33 | if ((nmemb >= MUL_NO_OVERFLOW || size >= MUL_NO_OVERFLOW) && 34 | nmemb > 0 && SIZE_MAX / nmemb < size) { 35 | errno = ENOMEM; 36 | return NULL; 37 | } 38 | return realloc(optr, size * nmemb); 39 | } 40 | -------------------------------------------------------------------------------- /stagit-index.1: -------------------------------------------------------------------------------- 1 | .Dd December 26, 2015 2 | .Dt STAGIT-INDEX 1 3 | .Os 4 | .Sh NAME 5 | .Nm stagit-index 6 | .Nd static git index page generator 7 | .Sh SYNOPSIS 8 | .Nm 9 | .Op Ar repodir... 10 | .Sh DESCRIPTION 11 | .Nm 12 | will create an index HTML page for the repositories specified and writes 13 | the HTML data to stdout. 14 | The repos in the index are in the same order as the arguments 15 | .Ar repodir 16 | specified. 17 | .Pp 18 | The basename of the directory is used as the repository name. 19 | The suffix ".git" is removed from the basename, this suffix is commonly used 20 | for "bare" repos. 21 | .Pp 22 | The content of the follow files specifies the meta data for each repository: 23 | .Bl -tag -width Ds 24 | .It .git/description or description (bare repos). 25 | description 26 | .It .git/owner or owner (bare repo). 27 | owner of repository 28 | .El 29 | .Pp 30 | For changing the style of the page you can use the following files: 31 | .Bl -tag -width Ds 32 | .It favicon.png 33 | favicon image. 34 | .It logo.png 35 | 32x32 logo. 36 | .It style.css 37 | CSS stylesheet. 38 | .El 39 | .Sh SEE ALSO 40 | .Xr stagit 1 41 | .Sh AUTHORS 42 | .An Hiltjo Posthuma Aq Mt hiltjo@codemadness.org 43 | -------------------------------------------------------------------------------- /stagit-index.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | 11 | static git_repository *repo; 12 | 13 | static const char *relpath = ""; 14 | 15 | static char description[255] = "Repositories"; 16 | static char *name = ""; 17 | static char owner[255]; 18 | 19 | void 20 | joinpath(char *buf, size_t bufsiz, const char *path, const char *path2) 21 | { 22 | int r; 23 | 24 | r = snprintf(buf, bufsiz, "%s%s%s", 25 | path, path[0] && path[strlen(path) - 1] != '/' ? "/" : "", path2); 26 | if (r < 0 || (size_t)r >= bufsiz) 27 | errx(1, "path truncated: '%s%s%s'", 28 | path, path[0] && path[strlen(path) - 1] != '/' ? "/" : "", path2); 29 | } 30 | 31 | /* Escape characters below as HTML 2.0 / XML 1.0. */ 32 | void 33 | xmlencode(FILE *fp, const char *s, size_t len) 34 | { 35 | size_t i; 36 | 37 | for (i = 0; *s && i < len; s++, i++) { 38 | switch(*s) { 39 | case '<': fputs("<", fp); break; 40 | case '>': fputs(">", fp); break; 41 | case '\'': fputs("'" , fp); break; 42 | case '&': fputs("&", fp); break; 43 | case '"': fputs(""", fp); break; 44 | default: fputc(*s, fp); 45 | } 46 | } 47 | } 48 | 49 | void 50 | printtimeshort(FILE *fp, const git_time *intime) 51 | { 52 | struct tm *intm; 53 | time_t t; 54 | char out[32]; 55 | 56 | t = (time_t)intime->time; 57 | if (!(intm = gmtime(&t))) 58 | return; 59 | strftime(out, sizeof(out), "%Y-%m-%d %H:%M", intm); 60 | fputs(out, fp); 61 | } 62 | 63 | void 64 | writeheader(FILE *fp) 65 | { 66 | fputs("\n" 67 | "\n\n" 68 | "\n" 69 | "", fp); 70 | xmlencode(fp, description, strlen(description)); 71 | fprintf(fp, "\n\n", relpath); 72 | fprintf(fp, "\n", relpath); 73 | fputs("\n\n", fp); 74 | fprintf(fp, "\n\n" 75 | "\n
\"\"", relpath); 76 | xmlencode(fp, description, strlen(description)); 77 | fputs("
\n" 78 | "
\n
\n
\n" 79 | "\n" 80 | "" 81 | "" 82 | "\n", fp); 83 | } 84 | 85 | void 86 | writefooter(FILE *fp) 87 | { 88 | fputs("\n
NameDescriptionOwnerLast commit
\n
\n\n\n", fp); 89 | } 90 | 91 | int 92 | writelog(FILE *fp) 93 | { 94 | git_commit *commit = NULL; 95 | const git_signature *author; 96 | git_revwalk *w = NULL; 97 | git_oid id; 98 | char *stripped_name = NULL, *p; 99 | int ret = 0; 100 | 101 | git_revwalk_new(&w, repo); 102 | git_revwalk_push_head(w); 103 | git_revwalk_simplify_first_parent(w); 104 | 105 | if (git_revwalk_next(&id, w) || 106 | git_commit_lookup(&commit, repo, &id)) { 107 | ret = -1; 108 | goto err; 109 | } 110 | 111 | author = git_commit_author(commit); 112 | 113 | /* strip .git suffix */ 114 | if (!(stripped_name = strdup(name))) 115 | err(1, "strdup"); 116 | if ((p = strrchr(stripped_name, '.'))) 117 | if (!strcmp(p, ".git")) 118 | *p = '\0'; 119 | 120 | fputs("", fp); 123 | xmlencode(fp, stripped_name, strlen(stripped_name)); 124 | fputs("", fp); 125 | xmlencode(fp, description, strlen(description)); 126 | fputs("", fp); 127 | xmlencode(fp, owner, strlen(owner)); 128 | fputs("", fp); 129 | if (author) 130 | printtimeshort(fp, &(author->when)); 131 | fputs("", fp); 132 | 133 | git_commit_free(commit); 134 | err: 135 | git_revwalk_free(w); 136 | free(stripped_name); 137 | 138 | return ret; 139 | } 140 | 141 | int 142 | main(int argc, char *argv[]) 143 | { 144 | FILE *fp; 145 | char path[PATH_MAX], repodirabs[PATH_MAX + 1]; 146 | const char *repodir; 147 | int i, ret = 0; 148 | 149 | if (argc < 2) { 150 | fprintf(stderr, "%s [repodir...]\n", argv[0]); 151 | return 1; 152 | } 153 | 154 | git_libgit2_init(); 155 | 156 | #ifdef __OpenBSD__ 157 | if (pledge("stdio rpath", NULL) == -1) 158 | err(1, "pledge"); 159 | #endif 160 | 161 | writeheader(stdout); 162 | 163 | for (i = 1; i < argc; i++) { 164 | repodir = argv[i]; 165 | if (!realpath(repodir, repodirabs)) 166 | err(1, "realpath"); 167 | 168 | if (git_repository_open_ext(&repo, repodir, 169 | GIT_REPOSITORY_OPEN_NO_SEARCH, NULL)) { 170 | fprintf(stderr, "%s: cannot open repository\n", argv[0]); 171 | ret = 1; 172 | continue; 173 | } 174 | 175 | /* use directory name as name */ 176 | if ((name = strrchr(repodirabs, '/'))) 177 | name++; 178 | else 179 | name = ""; 180 | 181 | /* read description or .git/description */ 182 | joinpath(path, sizeof(path), repodir, "description"); 183 | if (!(fp = fopen(path, "r"))) { 184 | joinpath(path, sizeof(path), repodir, ".git/description"); 185 | fp = fopen(path, "r"); 186 | } 187 | description[0] = '\0'; 188 | if (fp) { 189 | if (!fgets(description, sizeof(description), fp)) 190 | description[0] = '\0'; 191 | fclose(fp); 192 | } 193 | 194 | /* read owner or .git/owner */ 195 | joinpath(path, sizeof(path), repodir, "owner"); 196 | if (!(fp = fopen(path, "r"))) { 197 | joinpath(path, sizeof(path), repodir, ".git/owner"); 198 | fp = fopen(path, "r"); 199 | } 200 | owner[0] = '\0'; 201 | if (fp) { 202 | if (!fgets(owner, sizeof(owner), fp)) 203 | owner[0] = '\0'; 204 | owner[strcspn(owner, "\n")] = '\0'; 205 | fclose(fp); 206 | } 207 | writelog(stdout); 208 | } 209 | writefooter(stdout); 210 | 211 | /* cleanup */ 212 | git_repository_free(repo); 213 | git_libgit2_shutdown(); 214 | 215 | return ret; 216 | } 217 | -------------------------------------------------------------------------------- /stagit.1: -------------------------------------------------------------------------------- 1 | .Dd July 19, 2020 2 | .Dt STAGIT 1 3 | .Os 4 | .Sh NAME 5 | .Nm stagit 6 | .Nd static git page generator 7 | .Sh SYNOPSIS 8 | .Nm 9 | .Op Fl c Ar cachefile 10 | .Op Fl l Ar commits 11 | .Ar repodir 12 | .Sh DESCRIPTION 13 | .Nm 14 | writes HTML pages for the repository 15 | .Ar repodir 16 | to the current directory. 17 | .Pp 18 | The options are as follows: 19 | .Bl -tag -width Ds 20 | .It Fl c Ar cachefile 21 | Cache the entries of the log page up to the point of 22 | the last commit. 23 | The 24 | .Ar cachefile 25 | will store the last commit id and the entries in the HTML table. 26 | It is up to the user to make sure the state of the 27 | .Ar cachefile 28 | is in sync with the history of the repository. 29 | .It Fl l Ar commits 30 | Write a maximum number of 31 | .Ar commits 32 | to the log.html file only. 33 | However the commit files are written as usual. 34 | .El 35 | .Pp 36 | The options 37 | .Fl c 38 | and 39 | .Fl l 40 | cannot be used at the same time. 41 | .Pp 42 | The following files will be written: 43 | .Bl -tag -width Ds 44 | .It atom.xml 45 | Atom XML feed of the last 100 commits. 46 | .It tags.xml 47 | Atom XML feed of the tags. 48 | .It files.html 49 | List of files in the latest tree, linking to the file. 50 | .It log.html 51 | List of commits in reverse chronological applied commit order, each commit 52 | links to a page with a diffstat and diff of the commit. 53 | .It refs.html 54 | Lists references of the repository such as branches and tags. 55 | .El 56 | .Pp 57 | For each entry in HEAD a file will be written in the format: 58 | file/filepath.html. 59 | This file will contain the textual data of the file prefixed by line numbers. 60 | The file will have the string "Binary file" if the data is considered to be 61 | non-textual. 62 | .Pp 63 | For each commit a file will be written in the format: 64 | commit/commitid.html. 65 | This file will contain the diffstat and diff of the commit. 66 | It will write the string "Binary files differ" if the data is considered to 67 | be non-textual. 68 | Too large diffs will be suppressed and a string 69 | "Diff is too large, output suppressed" will be written. 70 | .Pp 71 | When a commit HTML file exists it won't be overwritten again, note that if 72 | you've changed 73 | .Nm 74 | or changed one of the metadata files of the repository it is recommended to 75 | recreate all the output files because it will contain old data. 76 | To do this remove the output directory and 77 | .Ar cachefile , 78 | then recreate the files. 79 | .Pp 80 | The basename of the directory is used as the repository name. 81 | The suffix ".git" is removed from the basename, this suffix is commonly used 82 | for "bare" repos. 83 | .Pp 84 | The content of the follow files specifies the metadata for each repository: 85 | .Bl -tag -width Ds 86 | .It .git/description or description (bare repo). 87 | description 88 | .It .git/owner or owner (bare repo). 89 | owner of repository 90 | .It .git/url or url (bare repo). 91 | primary clone url of the repository, for example: git://git.2f30.org/stagit 92 | .El 93 | .Pp 94 | When a README or LICENSE file exists in HEAD or a .gitmodules submodules file 95 | exists in HEAD a direct link in the menu is made. 96 | .Pp 97 | For changing the style of the page you can use the following files: 98 | .Bl -tag -width Ds 99 | .It favicon.png 100 | favicon image. 101 | .It logo.png 102 | 32x32 logo. 103 | .It style.css 104 | CSS stylesheet. 105 | .El 106 | .Sh SEE ALSO 107 | .Xr stagit-index 1 108 | .Sh AUTHORS 109 | .An Hiltjo Posthuma Aq Mt hiltjo@codemadness.org 110 | -------------------------------------------------------------------------------- /stagit.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #include 16 | 17 | #include "compat.h" 18 | 19 | struct deltainfo { 20 | git_patch *patch; 21 | 22 | size_t addcount; 23 | size_t delcount; 24 | }; 25 | 26 | struct commitinfo { 27 | const git_oid *id; 28 | 29 | char oid[GIT_OID_HEXSZ + 1]; 30 | char parentoid[GIT_OID_HEXSZ + 1]; 31 | 32 | const git_signature *author; 33 | const git_signature *committer; 34 | const char *summary; 35 | const char *msg; 36 | 37 | git_diff *diff; 38 | git_commit *commit; 39 | git_commit *parent; 40 | git_tree *commit_tree; 41 | git_tree *parent_tree; 42 | 43 | size_t addcount; 44 | size_t delcount; 45 | size_t filecount; 46 | 47 | struct deltainfo **deltas; 48 | size_t ndeltas; 49 | }; 50 | 51 | /* reference and associated data for sorting */ 52 | struct referenceinfo { 53 | struct git_reference *ref; 54 | struct commitinfo *ci; 55 | }; 56 | 57 | static git_repository *repo; 58 | 59 | static const char *relpath = ""; 60 | static const char *repodir; 61 | 62 | static char *name = ""; 63 | static char *strippedname = ""; 64 | static char description[255]; 65 | static char cloneurl[1024]; 66 | static char *submodules; 67 | static char *licensefiles[] = { "HEAD:LICENSE", "HEAD:LICENSE.md", "HEAD:COPYING" }; 68 | static char *license; 69 | static char *readmefiles[] = { "HEAD:README", "HEAD:README.md" }; 70 | static char *readme; 71 | static long long nlogcommits = -1; /* < 0 indicates not used */ 72 | 73 | /* cache */ 74 | static git_oid lastoid; 75 | static char lastoidstr[GIT_OID_HEXSZ + 2]; /* id + newline + NUL byte */ 76 | static FILE *rcachefp, *wcachefp; 77 | static const char *cachefile; 78 | 79 | void 80 | joinpath(char *buf, size_t bufsiz, const char *path, const char *path2) 81 | { 82 | int r; 83 | 84 | r = snprintf(buf, bufsiz, "%s%s%s", 85 | path, path[0] && path[strlen(path) - 1] != '/' ? "/" : "", path2); 86 | if (r < 0 || (size_t)r >= bufsiz) 87 | errx(1, "path truncated: '%s%s%s'", 88 | path, path[0] && path[strlen(path) - 1] != '/' ? "/" : "", path2); 89 | } 90 | 91 | void 92 | deltainfo_free(struct deltainfo *di) 93 | { 94 | if (!di) 95 | return; 96 | git_patch_free(di->patch); 97 | memset(di, 0, sizeof(*di)); 98 | free(di); 99 | } 100 | 101 | int 102 | commitinfo_getstats(struct commitinfo *ci) 103 | { 104 | struct deltainfo *di; 105 | git_diff_options opts; 106 | git_diff_find_options fopts; 107 | const git_diff_delta *delta; 108 | const git_diff_hunk *hunk; 109 | const git_diff_line *line; 110 | git_patch *patch = NULL; 111 | size_t ndeltas, nhunks, nhunklines; 112 | size_t i, j, k; 113 | 114 | if (git_tree_lookup(&(ci->commit_tree), repo, git_commit_tree_id(ci->commit))) 115 | goto err; 116 | if (!git_commit_parent(&(ci->parent), ci->commit, 0)) { 117 | if (git_tree_lookup(&(ci->parent_tree), repo, git_commit_tree_id(ci->parent))) { 118 | ci->parent = NULL; 119 | ci->parent_tree = NULL; 120 | } 121 | } 122 | 123 | git_diff_init_options(&opts, GIT_DIFF_OPTIONS_VERSION); 124 | opts.flags |= GIT_DIFF_DISABLE_PATHSPEC_MATCH | 125 | GIT_DIFF_IGNORE_SUBMODULES | 126 | GIT_DIFF_INCLUDE_TYPECHANGE; 127 | if (git_diff_tree_to_tree(&(ci->diff), repo, ci->parent_tree, ci->commit_tree, &opts)) 128 | goto err; 129 | 130 | if (git_diff_find_init_options(&fopts, GIT_DIFF_FIND_OPTIONS_VERSION)) 131 | goto err; 132 | /* find renames and copies, exact matches (no heuristic) for renames. */ 133 | fopts.flags |= GIT_DIFF_FIND_RENAMES | GIT_DIFF_FIND_COPIES | 134 | GIT_DIFF_FIND_EXACT_MATCH_ONLY; 135 | if (git_diff_find_similar(ci->diff, &fopts)) 136 | goto err; 137 | 138 | ndeltas = git_diff_num_deltas(ci->diff); 139 | if (ndeltas && !(ci->deltas = calloc(ndeltas, sizeof(struct deltainfo *)))) 140 | err(1, "calloc"); 141 | 142 | for (i = 0; i < ndeltas; i++) { 143 | if (git_patch_from_diff(&patch, ci->diff, i)) 144 | goto err; 145 | 146 | if (!(di = calloc(1, sizeof(struct deltainfo)))) 147 | err(1, "calloc"); 148 | di->patch = patch; 149 | ci->deltas[i] = di; 150 | 151 | delta = git_patch_get_delta(patch); 152 | 153 | /* skip stats for binary data */ 154 | if (delta->flags & GIT_DIFF_FLAG_BINARY) 155 | continue; 156 | 157 | nhunks = git_patch_num_hunks(patch); 158 | for (j = 0; j < nhunks; j++) { 159 | if (git_patch_get_hunk(&hunk, &nhunklines, patch, j)) 160 | break; 161 | for (k = 0; ; k++) { 162 | if (git_patch_get_line_in_hunk(&line, patch, j, k)) 163 | break; 164 | if (line->old_lineno == -1) { 165 | di->addcount++; 166 | ci->addcount++; 167 | } else if (line->new_lineno == -1) { 168 | di->delcount++; 169 | ci->delcount++; 170 | } 171 | } 172 | } 173 | } 174 | ci->ndeltas = i; 175 | ci->filecount = i; 176 | 177 | return 0; 178 | 179 | err: 180 | git_diff_free(ci->diff); 181 | ci->diff = NULL; 182 | git_tree_free(ci->commit_tree); 183 | ci->commit_tree = NULL; 184 | git_tree_free(ci->parent_tree); 185 | ci->parent_tree = NULL; 186 | git_commit_free(ci->parent); 187 | ci->parent = NULL; 188 | 189 | if (ci->deltas) 190 | for (i = 0; i < ci->ndeltas; i++) 191 | deltainfo_free(ci->deltas[i]); 192 | free(ci->deltas); 193 | ci->deltas = NULL; 194 | ci->ndeltas = 0; 195 | ci->addcount = 0; 196 | ci->delcount = 0; 197 | ci->filecount = 0; 198 | 199 | return -1; 200 | } 201 | 202 | void 203 | commitinfo_free(struct commitinfo *ci) 204 | { 205 | size_t i; 206 | 207 | if (!ci) 208 | return; 209 | if (ci->deltas) 210 | for (i = 0; i < ci->ndeltas; i++) 211 | deltainfo_free(ci->deltas[i]); 212 | 213 | free(ci->deltas); 214 | git_diff_free(ci->diff); 215 | git_tree_free(ci->commit_tree); 216 | git_tree_free(ci->parent_tree); 217 | git_commit_free(ci->commit); 218 | git_commit_free(ci->parent); 219 | memset(ci, 0, sizeof(*ci)); 220 | free(ci); 221 | } 222 | 223 | struct commitinfo * 224 | commitinfo_getbyoid(const git_oid *id) 225 | { 226 | struct commitinfo *ci; 227 | 228 | if (!(ci = calloc(1, sizeof(struct commitinfo)))) 229 | err(1, "calloc"); 230 | 231 | if (git_commit_lookup(&(ci->commit), repo, id)) 232 | goto err; 233 | ci->id = id; 234 | 235 | git_oid_tostr(ci->oid, sizeof(ci->oid), git_commit_id(ci->commit)); 236 | git_oid_tostr(ci->parentoid, sizeof(ci->parentoid), git_commit_parent_id(ci->commit, 0)); 237 | 238 | ci->author = git_commit_author(ci->commit); 239 | ci->committer = git_commit_committer(ci->commit); 240 | ci->summary = git_commit_summary(ci->commit); 241 | ci->msg = git_commit_message(ci->commit); 242 | 243 | return ci; 244 | 245 | err: 246 | commitinfo_free(ci); 247 | 248 | return NULL; 249 | } 250 | 251 | int 252 | refs_cmp(const void *v1, const void *v2) 253 | { 254 | struct referenceinfo *r1 = (struct referenceinfo *)v1; 255 | struct referenceinfo *r2 = (struct referenceinfo *)v2; 256 | time_t t1, t2; 257 | int r; 258 | 259 | if ((r = git_reference_is_tag(r1->ref) - git_reference_is_tag(r2->ref))) 260 | return r; 261 | 262 | t1 = r1->ci->author ? r1->ci->author->when.time : 0; 263 | t2 = r2->ci->author ? r2->ci->author->when.time : 0; 264 | if ((r = t1 > t2 ? -1 : (t1 == t2 ? 0 : 1))) 265 | return r; 266 | 267 | return strcmp(git_reference_shorthand(r1->ref), 268 | git_reference_shorthand(r2->ref)); 269 | } 270 | 271 | int 272 | getrefs(struct referenceinfo **pris, size_t *prefcount) 273 | { 274 | struct referenceinfo *ris = NULL; 275 | struct commitinfo *ci = NULL; 276 | git_reference_iterator *it = NULL; 277 | const git_oid *id = NULL; 278 | git_object *obj = NULL; 279 | git_reference *dref = NULL, *r, *ref = NULL; 280 | size_t i, refcount; 281 | 282 | *pris = NULL; 283 | *prefcount = 0; 284 | 285 | if (git_reference_iterator_new(&it, repo)) 286 | return -1; 287 | 288 | for (refcount = 0; !git_reference_next(&ref, it); ) { 289 | if (!git_reference_is_branch(ref) && !git_reference_is_tag(ref)) { 290 | git_reference_free(ref); 291 | ref = NULL; 292 | continue; 293 | } 294 | 295 | switch (git_reference_type(ref)) { 296 | case GIT_REF_SYMBOLIC: 297 | if (git_reference_resolve(&dref, ref)) 298 | goto err; 299 | r = dref; 300 | break; 301 | case GIT_REF_OID: 302 | r = ref; 303 | break; 304 | default: 305 | continue; 306 | } 307 | if (!git_reference_target(r) || 308 | git_reference_peel(&obj, r, GIT_OBJ_ANY)) 309 | goto err; 310 | if (!(id = git_object_id(obj))) 311 | goto err; 312 | if (!(ci = commitinfo_getbyoid(id))) 313 | break; 314 | 315 | if (!(ris = reallocarray(ris, refcount + 1, sizeof(*ris)))) 316 | err(1, "realloc"); 317 | ris[refcount].ci = ci; 318 | ris[refcount].ref = r; 319 | refcount++; 320 | 321 | git_object_free(obj); 322 | obj = NULL; 323 | git_reference_free(dref); 324 | dref = NULL; 325 | } 326 | git_reference_iterator_free(it); 327 | 328 | /* sort by type, date then shorthand name */ 329 | qsort(ris, refcount, sizeof(*ris), refs_cmp); 330 | 331 | *pris = ris; 332 | *prefcount = refcount; 333 | 334 | return 0; 335 | 336 | err: 337 | git_object_free(obj); 338 | git_reference_free(dref); 339 | commitinfo_free(ci); 340 | for (i = 0; i < refcount; i++) { 341 | commitinfo_free(ris[i].ci); 342 | git_reference_free(ris[i].ref); 343 | } 344 | free(ris); 345 | 346 | return -1; 347 | } 348 | 349 | FILE * 350 | efopen(const char *name, const char *flags) 351 | { 352 | FILE *fp; 353 | 354 | if (!(fp = fopen(name, flags))) 355 | err(1, "fopen: '%s'", name); 356 | 357 | return fp; 358 | } 359 | 360 | /* Escape characters below as HTML 2.0 / XML 1.0. */ 361 | void 362 | xmlencode(FILE *fp, const char *s, size_t len) 363 | { 364 | size_t i; 365 | 366 | for (i = 0; *s && i < len; s++, i++) { 367 | switch(*s) { 368 | case '<': fputs("<", fp); break; 369 | case '>': fputs(">", fp); break; 370 | case '\'': fputs("'", fp); break; 371 | case '&': fputs("&", fp); break; 372 | case '"': fputs(""", fp); break; 373 | default: fputc(*s, fp); 374 | } 375 | } 376 | } 377 | 378 | int 379 | mkdirp(const char *path) 380 | { 381 | char tmp[PATH_MAX], *p; 382 | 383 | if (strlcpy(tmp, path, sizeof(tmp)) >= sizeof(tmp)) 384 | errx(1, "path truncated: '%s'", path); 385 | for (p = tmp + (tmp[0] == '/'); *p; p++) { 386 | if (*p != '/') 387 | continue; 388 | *p = '\0'; 389 | if (mkdir(tmp, S_IRWXU | S_IRWXG | S_IRWXO) < 0 && errno != EEXIST) 390 | return -1; 391 | *p = '/'; 392 | } 393 | if (mkdir(tmp, S_IRWXU | S_IRWXG | S_IRWXO) < 0 && errno != EEXIST) 394 | return -1; 395 | return 0; 396 | } 397 | 398 | void 399 | printtimez(FILE *fp, const git_time *intime) 400 | { 401 | struct tm *intm; 402 | time_t t; 403 | char out[32]; 404 | 405 | t = (time_t)intime->time; 406 | if (!(intm = gmtime(&t))) 407 | return; 408 | strftime(out, sizeof(out), "%Y-%m-%dT%H:%M:%SZ", intm); 409 | fputs(out, fp); 410 | } 411 | 412 | void 413 | printtime(FILE *fp, const git_time *intime) 414 | { 415 | struct tm *intm; 416 | time_t t; 417 | char out[32]; 418 | 419 | t = (time_t)intime->time + (intime->offset * 60); 420 | if (!(intm = gmtime(&t))) 421 | return; 422 | strftime(out, sizeof(out), "%a, %e %b %Y %H:%M:%S", intm); 423 | if (intime->offset < 0) 424 | fprintf(fp, "%s -%02d%02d", out, 425 | -(intime->offset) / 60, -(intime->offset) % 60); 426 | else 427 | fprintf(fp, "%s +%02d%02d", out, 428 | intime->offset / 60, intime->offset % 60); 429 | } 430 | 431 | void 432 | printtimeshort(FILE *fp, const git_time *intime) 433 | { 434 | struct tm *intm; 435 | time_t t; 436 | char out[32]; 437 | 438 | t = (time_t)intime->time; 439 | if (!(intm = gmtime(&t))) 440 | return; 441 | strftime(out, sizeof(out), "%Y-%m-%d %H:%M", intm); 442 | fputs(out, fp); 443 | } 444 | 445 | void 446 | writeheader(FILE *fp, const char *title) 447 | { 448 | fputs("\n" 449 | "\n\n" 450 | "\n" 451 | "", fp); 452 | xmlencode(fp, title, strlen(title)); 453 | if (title[0] && strippedname[0]) 454 | fputs(" - ", fp); 455 | xmlencode(fp, strippedname, strlen(strippedname)); 456 | if (description[0]) 457 | fputs(" - ", fp); 458 | xmlencode(fp, description, strlen(description)); 459 | fprintf(fp, "\n\n", relpath); 460 | fprintf(fp, "\n", 461 | name, relpath); 462 | fprintf(fp, "\n", 463 | name, relpath); 464 | fprintf(fp, "\n", relpath); 465 | fputs("\n\n", fp); 473 | if (cloneurl[0]) { 474 | fputs("", fp); 479 | } 480 | fputs("
", fp); 466 | fprintf(fp, "\"\"", 467 | relpath, relpath); 468 | fputs("

", fp); 469 | xmlencode(fp, strippedname, strlen(strippedname)); 470 | fputs("

", fp); 471 | xmlencode(fp, description, strlen(description)); 472 | fputs("
git clone ", fp); 477 | xmlencode(fp, cloneurl, strlen(cloneurl)); 478 | fputs("
\n", fp); 481 | fprintf(fp, "Log | ", relpath); 482 | fprintf(fp, "Files | ", relpath); 483 | fprintf(fp, "Refs", relpath); 484 | if (submodules) 485 | fprintf(fp, " | Submodules", 486 | relpath, submodules); 487 | if (readme) 488 | fprintf(fp, " | README", 489 | relpath, readme); 490 | if (license) 491 | fprintf(fp, " | LICENSE", 492 | relpath, license); 493 | fputs("
\n
\n
\n", fp); 494 | } 495 | 496 | void 497 | writefooter(FILE *fp) 498 | { 499 | fputs("
\n\n\n", fp); 500 | } 501 | 502 | int 503 | writeblobhtml(FILE *fp, const git_blob *blob) 504 | { 505 | size_t n = 0, i, prev; 506 | const char *nfmt = "%7d "; 507 | const char *s = git_blob_rawcontent(blob); 508 | git_off_t len = git_blob_rawsize(blob); 509 | 510 | fputs("
\n", fp);
 511 | 
 512 | 	if (len > 0) {
 513 | 		for (i = 0, prev = 0; i < (size_t)len; i++) {
 514 | 			if (s[i] != '\n')
 515 | 				continue;
 516 | 			n++;
 517 | 			fprintf(fp, nfmt, n, n, n);
 518 | 			xmlencode(fp, &s[prev], i - prev + 1);
 519 | 			prev = i + 1;
 520 | 		}
 521 | 		/* trailing data */
 522 | 		if ((len - prev) > 0) {
 523 | 			n++;
 524 | 			fprintf(fp, nfmt, n, n, n);
 525 | 			xmlencode(fp, &s[prev], len - prev);
 526 | 		}
 527 | 	}
 528 | 
 529 | 	fputs("
\n", fp); 530 | 531 | return n; 532 | } 533 | 534 | void 535 | printcommit(FILE *fp, struct commitinfo *ci) 536 | { 537 | fprintf(fp, "commit %s\n", 538 | relpath, ci->oid, ci->oid); 539 | 540 | if (ci->parentoid[0]) 541 | fprintf(fp, "parent %s\n", 542 | relpath, ci->parentoid, ci->parentoid); 543 | 544 | if (ci->author) { 545 | fputs("Author: ", fp); 546 | xmlencode(fp, ci->author->name, strlen(ci->author->name)); 547 | fputs(" <author->email, strlen(ci->author->email)); 549 | fputs("\">", fp); 550 | xmlencode(fp, ci->author->email, strlen(ci->author->email)); 551 | fputs(">\nDate: ", fp); 552 | printtime(fp, &(ci->author->when)); 553 | fputc('\n', fp); 554 | } 555 | if (ci->msg) { 556 | fputc('\n', fp); 557 | xmlencode(fp, ci->msg, strlen(ci->msg)); 558 | fputc('\n', fp); 559 | } 560 | } 561 | 562 | void 563 | printshowfile(FILE *fp, struct commitinfo *ci) 564 | { 565 | const git_diff_delta *delta; 566 | const git_diff_hunk *hunk; 567 | const git_diff_line *line; 568 | git_patch *patch; 569 | size_t nhunks, nhunklines, changed, add, del, total, i, j, k; 570 | char linestr[80]; 571 | int c; 572 | 573 | printcommit(fp, ci); 574 | 575 | if (!ci->deltas) 576 | return; 577 | 578 | if (ci->filecount > 1000 || 579 | ci->ndeltas > 1000 || 580 | ci->addcount > 100000 || 581 | ci->delcount > 100000) { 582 | fputs("Diff is too large, output suppressed.\n", fp); 583 | return; 584 | } 585 | 586 | /* diff stat */ 587 | fputs("Diffstat:\n", fp); 588 | for (i = 0; i < ci->ndeltas; i++) { 589 | delta = git_patch_get_delta(ci->deltas[i]->patch); 590 | 591 | switch (delta->status) { 592 | case GIT_DELTA_ADDED: c = 'A'; break; 593 | case GIT_DELTA_COPIED: c = 'C'; break; 594 | case GIT_DELTA_DELETED: c = 'D'; break; 595 | case GIT_DELTA_MODIFIED: c = 'M'; break; 596 | case GIT_DELTA_RENAMED: c = 'R'; break; 597 | case GIT_DELTA_TYPECHANGE: c = 'T'; break; 598 | default: c = ' '; break; 599 | } 600 | if (c == ' ') 601 | fprintf(fp, "\n", fp); 631 | } 632 | fprintf(fp, "
%c", c); 602 | else 603 | fprintf(fp, "
%c", c, c); 604 | 605 | fprintf(fp, "", i); 606 | xmlencode(fp, delta->old_file.path, strlen(delta->old_file.path)); 607 | if (strcmp(delta->old_file.path, delta->new_file.path)) { 608 | fputs(" -> ", fp); 609 | xmlencode(fp, delta->new_file.path, strlen(delta->new_file.path)); 610 | } 611 | 612 | add = ci->deltas[i]->addcount; 613 | del = ci->deltas[i]->delcount; 614 | changed = add + del; 615 | total = sizeof(linestr) - 2; 616 | if (changed > total) { 617 | if (add) 618 | add = ((float)total / changed * add) + 1; 619 | if (del) 620 | del = ((float)total / changed * del) + 1; 621 | } 622 | memset(&linestr, '+', add); 623 | memset(&linestr[add], '-', del); 624 | 625 | fprintf(fp, " | %zu", 626 | ci->deltas[i]->addcount + ci->deltas[i]->delcount); 627 | fwrite(&linestr, 1, add, fp); 628 | fputs("", fp); 629 | fwrite(&linestr[add], 1, del, fp); 630 | fputs("
%zu file%s changed, %zu insertion%s(+), %zu deletion%s(-)\n",
 633 | 		ci->filecount, ci->filecount == 1 ? "" : "s",
 634 | 	        ci->addcount,  ci->addcount  == 1 ? "" : "s",
 635 | 	        ci->delcount,  ci->delcount  == 1 ? "" : "s");
 636 | 
 637 | 	fputs("
", fp); 638 | 639 | for (i = 0; i < ci->ndeltas; i++) { 640 | patch = ci->deltas[i]->patch; 641 | delta = git_patch_get_delta(patch); 642 | fprintf(fp, "diff --git a/old_file.path, strlen(delta->old_file.path)); 644 | fputs(".html\">", fp); 645 | xmlencode(fp, delta->old_file.path, strlen(delta->old_file.path)); 646 | fprintf(fp, " b/new_file.path, strlen(delta->new_file.path)); 648 | fprintf(fp, ".html\">"); 649 | xmlencode(fp, delta->new_file.path, strlen(delta->new_file.path)); 650 | fprintf(fp, "\n"); 651 | 652 | /* check binary data */ 653 | if (delta->flags & GIT_DIFF_FLAG_BINARY) { 654 | fputs("Binary files differ.\n", fp); 655 | continue; 656 | } 657 | 658 | nhunks = git_patch_num_hunks(patch); 659 | for (j = 0; j < nhunks; j++) { 660 | if (git_patch_get_hunk(&hunk, &nhunklines, patch, j)) 661 | break; 662 | 663 | fprintf(fp, "", i, j, i, j); 664 | xmlencode(fp, hunk->header, hunk->header_len); 665 | fputs("", fp); 666 | 667 | for (k = 0; ; k++) { 668 | if (git_patch_get_line_in_hunk(&line, patch, j, k)) 669 | break; 670 | if (line->old_lineno == -1) 671 | fprintf(fp, "+", 672 | i, j, k, i, j, k); 673 | else if (line->new_lineno == -1) 674 | fprintf(fp, "-", 675 | i, j, k, i, j, k); 676 | else 677 | fputc(' ', fp); 678 | xmlencode(fp, line->content, line->content_len); 679 | if (line->old_lineno == -1 || line->new_lineno == -1) 680 | fputs("", fp); 681 | } 682 | } 683 | } 684 | } 685 | 686 | void 687 | writelogline(FILE *fp, struct commitinfo *ci) 688 | { 689 | fputs("", fp); 690 | if (ci->author) 691 | printtimeshort(fp, &(ci->author->when)); 692 | fputs("", fp); 693 | if (ci->summary) { 694 | fprintf(fp, "", relpath, ci->oid); 695 | xmlencode(fp, ci->summary, strlen(ci->summary)); 696 | fputs("", fp); 697 | } 698 | fputs("", fp); 699 | if (ci->author) 700 | xmlencode(fp, ci->author->name, strlen(ci->author->name)); 701 | fputs("", fp); 702 | fprintf(fp, "%zu", ci->filecount); 703 | fputs("", fp); 704 | fprintf(fp, "+%zu", ci->addcount); 705 | fputs("", fp); 706 | fprintf(fp, "-%zu", ci->delcount); 707 | fputs("\n", fp); 708 | } 709 | 710 | int 711 | writelog(FILE *fp, const git_oid *oid) 712 | { 713 | struct commitinfo *ci; 714 | git_revwalk *w = NULL; 715 | git_oid id; 716 | char path[PATH_MAX], oidstr[GIT_OID_HEXSZ + 1]; 717 | FILE *fpfile; 718 | int r; 719 | 720 | git_revwalk_new(&w, repo); 721 | git_revwalk_push(w, oid); 722 | git_revwalk_simplify_first_parent(w); 723 | 724 | while (!git_revwalk_next(&id, w)) { 725 | relpath = ""; 726 | 727 | if (cachefile && !memcmp(&id, &lastoid, sizeof(id))) 728 | break; 729 | 730 | git_oid_tostr(oidstr, sizeof(oidstr), &id); 731 | r = snprintf(path, sizeof(path), "commit/%s.html", oidstr); 732 | if (r < 0 || (size_t)r >= sizeof(path)) 733 | errx(1, "path truncated: 'commit/%s.html'", oidstr); 734 | r = access(path, F_OK); 735 | 736 | /* optimization: if there are no log lines to write and 737 | the commit file already exists: skip the diffstat */ 738 | if (!nlogcommits && !r) 739 | continue; 740 | 741 | if (!(ci = commitinfo_getbyoid(&id))) 742 | break; 743 | /* diffstat: for stagit HTML required for the log.html line */ 744 | if (commitinfo_getstats(ci) == -1) 745 | goto err; 746 | 747 | if (nlogcommits < 0) { 748 | writelogline(fp, ci); 749 | } else if (nlogcommits > 0) { 750 | writelogline(fp, ci); 751 | nlogcommits--; 752 | if (!nlogcommits && ci->parentoid[0]) 753 | fputs("" 754 | "More commits remaining [...]" 755 | "\n", fp); 756 | } 757 | 758 | if (cachefile) 759 | writelogline(wcachefp, ci); 760 | 761 | /* check if file exists if so skip it */ 762 | if (r) { 763 | relpath = "../"; 764 | fpfile = efopen(path, "w"); 765 | writeheader(fpfile, ci->summary); 766 | fputs("
", fpfile);
 767 | 			printshowfile(fpfile, ci);
 768 | 			fputs("
\n", fpfile); 769 | writefooter(fpfile); 770 | fclose(fpfile); 771 | } 772 | err: 773 | commitinfo_free(ci); 774 | } 775 | git_revwalk_free(w); 776 | 777 | relpath = ""; 778 | 779 | return 0; 780 | } 781 | 782 | void 783 | printcommitatom(FILE *fp, struct commitinfo *ci, const char *tag) 784 | { 785 | fputs("\n", fp); 786 | 787 | fprintf(fp, "%s\n", ci->oid); 788 | if (ci->author) { 789 | fputs("", fp); 790 | printtimez(fp, &(ci->author->when)); 791 | fputs("\n", fp); 792 | } 793 | if (ci->committer) { 794 | fputs("", fp); 795 | printtimez(fp, &(ci->committer->when)); 796 | fputs("\n", fp); 797 | } 798 | if (ci->summary) { 799 | fputs("", fp); 800 | if (tag && tag[0]) { 801 | fputs("[", fp); 802 | xmlencode(fp, tag, strlen(tag)); 803 | fputs("] ", fp); 804 | } 805 | xmlencode(fp, ci->summary, strlen(ci->summary)); 806 | fputs("\n", fp); 807 | } 808 | fprintf(fp, "\n", 809 | ci->oid); 810 | 811 | if (ci->author) { 812 | fputs("\n", fp); 813 | xmlencode(fp, ci->author->name, strlen(ci->author->name)); 814 | fputs("\n", fp); 815 | xmlencode(fp, ci->author->email, strlen(ci->author->email)); 816 | fputs("\n\n", fp); 817 | } 818 | 819 | fputs("", fp); 820 | fprintf(fp, "commit %s\n", ci->oid); 821 | if (ci->parentoid[0]) 822 | fprintf(fp, "parent %s\n", ci->parentoid); 823 | if (ci->author) { 824 | fputs("Author: ", fp); 825 | xmlencode(fp, ci->author->name, strlen(ci->author->name)); 826 | fputs(" <", fp); 827 | xmlencode(fp, ci->author->email, strlen(ci->author->email)); 828 | fputs(">\nDate: ", fp); 829 | printtime(fp, &(ci->author->when)); 830 | fputc('\n', fp); 831 | } 832 | if (ci->msg) { 833 | fputc('\n', fp); 834 | xmlencode(fp, ci->msg, strlen(ci->msg)); 835 | } 836 | fputs("\n\n\n", fp); 837 | } 838 | 839 | int 840 | writeatom(FILE *fp, int all) 841 | { 842 | struct referenceinfo *ris = NULL; 843 | size_t refcount = 0; 844 | struct commitinfo *ci; 845 | git_revwalk *w = NULL; 846 | git_oid id; 847 | size_t i, m = 100; /* last 'm' commits */ 848 | 849 | fputs("\n" 850 | "\n", fp); 851 | xmlencode(fp, strippedname, strlen(strippedname)); 852 | fputs(", branch HEAD\n", fp); 853 | xmlencode(fp, description, strlen(description)); 854 | fputs("\n", fp); 855 | 856 | /* all commits or only tags? */ 857 | if (all) { 858 | git_revwalk_new(&w, repo); 859 | git_revwalk_push_head(w); 860 | git_revwalk_simplify_first_parent(w); 861 | for (i = 0; i < m && !git_revwalk_next(&id, w); i++) { 862 | if (!(ci = commitinfo_getbyoid(&id))) 863 | break; 864 | printcommitatom(fp, ci, ""); 865 | commitinfo_free(ci); 866 | } 867 | git_revwalk_free(w); 868 | } else if (getrefs(&ris, &refcount) != -1) { 869 | /* references: tags */ 870 | for (i = 0; i < refcount; i++) { 871 | if (git_reference_is_tag(ris[i].ref)) 872 | printcommitatom(fp, ris[i].ci, 873 | git_reference_shorthand(ris[i].ref)); 874 | 875 | commitinfo_free(ris[i].ci); 876 | git_reference_free(ris[i].ref); 877 | } 878 | free(ris); 879 | } 880 | 881 | fputs("\n", fp); 882 | 883 | return 0; 884 | } 885 | 886 | int 887 | writeblob(git_object *obj, const char *fpath, const char *filename, git_off_t filesize) 888 | { 889 | char tmp[PATH_MAX] = "", *d; 890 | const char *p; 891 | int lc = 0; 892 | FILE *fp; 893 | 894 | if (strlcpy(tmp, fpath, sizeof(tmp)) >= sizeof(tmp)) 895 | errx(1, "path truncated: '%s'", fpath); 896 | if (!(d = dirname(tmp))) 897 | err(1, "dirname"); 898 | if (mkdirp(d)) 899 | return -1; 900 | 901 | for (p = fpath, tmp[0] = '\0'; *p; p++) { 902 | if (*p == '/' && strlcat(tmp, "../", sizeof(tmp)) >= sizeof(tmp)) 903 | errx(1, "path truncated: '../%s'", tmp); 904 | } 905 | relpath = tmp; 906 | 907 | fp = efopen(fpath, "w"); 908 | writeheader(fp, filename); 909 | fputs("

", fp); 910 | xmlencode(fp, filename, strlen(filename)); 911 | fprintf(fp, " (%juB)", (uintmax_t)filesize); 912 | fputs("


", fp); 913 | 914 | if (git_blob_is_binary((git_blob *)obj)) { 915 | fputs("

Binary file.

\n", fp); 916 | } else { 917 | lc = writeblobhtml(fp, (git_blob *)obj); 918 | if (ferror(fp)) 919 | err(1, "fwrite"); 920 | } 921 | writefooter(fp); 922 | fclose(fp); 923 | 924 | relpath = ""; 925 | 926 | return lc; 927 | } 928 | 929 | const char * 930 | filemode(git_filemode_t m) 931 | { 932 | static char mode[11]; 933 | 934 | memset(mode, '-', sizeof(mode) - 1); 935 | mode[10] = '\0'; 936 | 937 | if (S_ISREG(m)) 938 | mode[0] = '-'; 939 | else if (S_ISBLK(m)) 940 | mode[0] = 'b'; 941 | else if (S_ISCHR(m)) 942 | mode[0] = 'c'; 943 | else if (S_ISDIR(m)) 944 | mode[0] = 'd'; 945 | else if (S_ISFIFO(m)) 946 | mode[0] = 'p'; 947 | else if (S_ISLNK(m)) 948 | mode[0] = 'l'; 949 | else if (S_ISSOCK(m)) 950 | mode[0] = 's'; 951 | else 952 | mode[0] = '?'; 953 | 954 | if (m & S_IRUSR) mode[1] = 'r'; 955 | if (m & S_IWUSR) mode[2] = 'w'; 956 | if (m & S_IXUSR) mode[3] = 'x'; 957 | if (m & S_IRGRP) mode[4] = 'r'; 958 | if (m & S_IWGRP) mode[5] = 'w'; 959 | if (m & S_IXGRP) mode[6] = 'x'; 960 | if (m & S_IROTH) mode[7] = 'r'; 961 | if (m & S_IWOTH) mode[8] = 'w'; 962 | if (m & S_IXOTH) mode[9] = 'x'; 963 | 964 | if (m & S_ISUID) mode[3] = (mode[3] == 'x') ? 's' : 'S'; 965 | if (m & S_ISGID) mode[6] = (mode[6] == 'x') ? 's' : 'S'; 966 | if (m & S_ISVTX) mode[9] = (mode[9] == 'x') ? 't' : 'T'; 967 | 968 | return mode; 969 | } 970 | 971 | int 972 | writefilestree(FILE *fp, git_tree *tree, const char *path) 973 | { 974 | const git_tree_entry *entry = NULL; 975 | git_object *obj = NULL; 976 | git_off_t filesize; 977 | const char *entryname; 978 | char filepath[PATH_MAX], entrypath[PATH_MAX]; 979 | size_t count, i; 980 | int lc, r, ret; 981 | 982 | count = git_tree_entrycount(tree); 983 | for (i = 0; i < count; i++) { 984 | if (!(entry = git_tree_entry_byindex(tree, i)) || 985 | !(entryname = git_tree_entry_name(entry))) 986 | return -1; 987 | joinpath(entrypath, sizeof(entrypath), path, entryname); 988 | 989 | r = snprintf(filepath, sizeof(filepath), "file/%s.html", 990 | entrypath); 991 | if (r < 0 || (size_t)r >= sizeof(filepath)) 992 | errx(1, "path truncated: 'file/%s.html'", entrypath); 993 | 994 | if (!git_tree_entry_to_object(&obj, repo, entry)) { 995 | switch (git_object_type(obj)) { 996 | case GIT_OBJ_BLOB: 997 | break; 998 | case GIT_OBJ_TREE: 999 | /* NOTE: recurses */ 1000 | ret = writefilestree(fp, (git_tree *)obj, 1001 | entrypath); 1002 | git_object_free(obj); 1003 | if (ret) 1004 | return ret; 1005 | continue; 1006 | default: 1007 | git_object_free(obj); 1008 | continue; 1009 | } 1010 | 1011 | filesize = git_blob_rawsize((git_blob *)obj); 1012 | lc = writeblob(obj, filepath, entryname, filesize); 1013 | 1014 | fputs("", fp); 1015 | fputs(filemode(git_tree_entry_filemode(entry)), fp); 1016 | fprintf(fp, "", fp); 1019 | xmlencode(fp, entrypath, strlen(entrypath)); 1020 | fputs("", fp); 1021 | if (lc > 0) 1022 | fprintf(fp, "%dL", lc); 1023 | else 1024 | fprintf(fp, "%juB", (uintmax_t)filesize); 1025 | fputs("\n", fp); 1026 | git_object_free(obj); 1027 | } else if (git_tree_entry_type(entry) == GIT_OBJ_COMMIT) { 1028 | /* commit object in tree is a submodule */ 1029 | fprintf(fp, "m---------", 1030 | relpath); 1031 | xmlencode(fp, entrypath, strlen(entrypath)); 1032 | fputs("\n", fp); 1033 | } 1034 | } 1035 | 1036 | return 0; 1037 | } 1038 | 1039 | int 1040 | writefiles(FILE *fp, const git_oid *id) 1041 | { 1042 | git_tree *tree = NULL; 1043 | git_commit *commit = NULL; 1044 | int ret = -1; 1045 | 1046 | fputs("\n" 1047 | "" 1048 | "" 1049 | "\n\n", fp); 1050 | 1051 | if (!git_commit_lookup(&commit, repo, id) && 1052 | !git_commit_tree(&tree, commit)) 1053 | ret = writefilestree(fp, tree, ""); 1054 | 1055 | fputs("
ModeNameSize
", fp); 1056 | 1057 | git_commit_free(commit); 1058 | git_tree_free(tree); 1059 | 1060 | return ret; 1061 | } 1062 | 1063 | int 1064 | writerefs(FILE *fp) 1065 | { 1066 | struct referenceinfo *ris = NULL; 1067 | struct commitinfo *ci; 1068 | size_t count, i, j, refcount; 1069 | const char *titles[] = { "Branches", "Tags" }; 1070 | const char *ids[] = { "branches", "tags" }; 1071 | const char *s; 1072 | 1073 | if (getrefs(&ris, &refcount) == -1) 1074 | return -1; 1075 | 1076 | for (i = 0, j = 0, count = 0; i < refcount; i++) { 1077 | if (j == 0 && git_reference_is_tag(ris[i].ref)) { 1078 | if (count) 1079 | fputs("
\n", fp); 1080 | count = 0; 1081 | j = 1; 1082 | } 1083 | 1084 | /* print header if it has an entry (first). */ 1085 | if (++count == 1) { 1086 | fprintf(fp, "

%s

" 1087 | "\n" 1088 | "" 1089 | "\n\n" 1090 | "\n", 1091 | titles[j], ids[j]); 1092 | } 1093 | 1094 | ci = ris[i].ci; 1095 | s = git_reference_shorthand(ris[i].ref); 1096 | 1097 | fputs("\n", fp); 1106 | } 1107 | /* table footer */ 1108 | if (count) 1109 | fputs("
NameLast commit dateAuthor
", fp); 1098 | xmlencode(fp, s, strlen(s)); 1099 | fputs("", fp); 1100 | if (ci->author) 1101 | printtimeshort(fp, &(ci->author->when)); 1102 | fputs("", fp); 1103 | if (ci->author) 1104 | xmlencode(fp, ci->author->name, strlen(ci->author->name)); 1105 | fputs("

\n", fp); 1110 | 1111 | for (i = 0; i < refcount; i++) { 1112 | commitinfo_free(ris[i].ci); 1113 | git_reference_free(ris[i].ref); 1114 | } 1115 | free(ris); 1116 | 1117 | return 0; 1118 | } 1119 | 1120 | void 1121 | usage(char *argv0) 1122 | { 1123 | fprintf(stderr, "%s [-c cachefile | -l commits] repodir\n", argv0); 1124 | exit(1); 1125 | } 1126 | 1127 | int 1128 | main(int argc, char *argv[]) 1129 | { 1130 | git_object *obj = NULL; 1131 | const git_oid *head = NULL; 1132 | mode_t mask; 1133 | FILE *fp, *fpread; 1134 | char path[PATH_MAX], repodirabs[PATH_MAX + 1], *p; 1135 | char tmppath[64] = "cache.XXXXXXXXXXXX", buf[BUFSIZ]; 1136 | size_t n; 1137 | int i, fd; 1138 | 1139 | for (i = 1; i < argc; i++) { 1140 | if (argv[i][0] != '-') { 1141 | if (repodir) 1142 | usage(argv[0]); 1143 | repodir = argv[i]; 1144 | } else if (argv[i][1] == 'c') { 1145 | if (nlogcommits > 0 || i + 1 >= argc) 1146 | usage(argv[0]); 1147 | cachefile = argv[++i]; 1148 | } else if (argv[i][1] == 'l') { 1149 | if (cachefile || i + 1 >= argc) 1150 | usage(argv[0]); 1151 | errno = 0; 1152 | nlogcommits = strtoll(argv[++i], &p, 10); 1153 | if (argv[i][0] == '\0' || *p != '\0' || 1154 | nlogcommits <= 0 || errno) 1155 | usage(argv[0]); 1156 | } 1157 | } 1158 | if (!repodir) 1159 | usage(argv[0]); 1160 | 1161 | if (!realpath(repodir, repodirabs)) 1162 | err(1, "realpath"); 1163 | 1164 | git_libgit2_init(); 1165 | 1166 | #ifdef __OpenBSD__ 1167 | if (unveil(repodir, "r") == -1) 1168 | err(1, "unveil: %s", repodir); 1169 | if (unveil(".", "rwc") == -1) 1170 | err(1, "unveil: ."); 1171 | if (cachefile && unveil(cachefile, "rwc") == -1) 1172 | err(1, "unveil: %s", cachefile); 1173 | 1174 | if (cachefile) { 1175 | if (pledge("stdio rpath wpath cpath fattr", NULL) == -1) 1176 | err(1, "pledge"); 1177 | } else { 1178 | if (pledge("stdio rpath wpath cpath", NULL) == -1) 1179 | err(1, "pledge"); 1180 | } 1181 | #endif 1182 | 1183 | if (git_repository_open_ext(&repo, repodir, 1184 | GIT_REPOSITORY_OPEN_NO_SEARCH, NULL) < 0) { 1185 | fprintf(stderr, "%s: cannot open repository\n", argv[0]); 1186 | return 1; 1187 | } 1188 | 1189 | /* find HEAD */ 1190 | if (!git_revparse_single(&obj, repo, "HEAD")) 1191 | head = git_object_id(obj); 1192 | git_object_free(obj); 1193 | 1194 | /* use directory name as name */ 1195 | if ((name = strrchr(repodirabs, '/'))) 1196 | name++; 1197 | else 1198 | name = ""; 1199 | 1200 | /* strip .git suffix */ 1201 | if (!(strippedname = strdup(name))) 1202 | err(1, "strdup"); 1203 | if ((p = strrchr(strippedname, '.'))) 1204 | if (!strcmp(p, ".git")) 1205 | *p = '\0'; 1206 | 1207 | /* read description or .git/description */ 1208 | joinpath(path, sizeof(path), repodir, "description"); 1209 | if (!(fpread = fopen(path, "r"))) { 1210 | joinpath(path, sizeof(path), repodir, ".git/description"); 1211 | fpread = fopen(path, "r"); 1212 | } 1213 | if (fpread) { 1214 | if (!fgets(description, sizeof(description), fpread)) 1215 | description[0] = '\0'; 1216 | fclose(fpread); 1217 | } 1218 | 1219 | /* read url or .git/url */ 1220 | joinpath(path, sizeof(path), repodir, "url"); 1221 | if (!(fpread = fopen(path, "r"))) { 1222 | joinpath(path, sizeof(path), repodir, ".git/url"); 1223 | fpread = fopen(path, "r"); 1224 | } 1225 | if (fpread) { 1226 | if (!fgets(cloneurl, sizeof(cloneurl), fpread)) 1227 | cloneurl[0] = '\0'; 1228 | cloneurl[strcspn(cloneurl, "\n")] = '\0'; 1229 | fclose(fpread); 1230 | } 1231 | 1232 | /* check LICENSE */ 1233 | for (i = 0; i < sizeof(licensefiles) / sizeof(*licensefiles) && !license; i++) { 1234 | if (!git_revparse_single(&obj, repo, licensefiles[i]) && 1235 | git_object_type(obj) == GIT_OBJ_BLOB) 1236 | license = licensefiles[i] + strlen("HEAD:"); 1237 | git_object_free(obj); 1238 | } 1239 | 1240 | /* check README */ 1241 | for (i = 0; i < sizeof(readmefiles) / sizeof(*readmefiles) && !readme; i++) { 1242 | if (!git_revparse_single(&obj, repo, readmefiles[i]) && 1243 | git_object_type(obj) == GIT_OBJ_BLOB) 1244 | readme = readmefiles[i] + strlen("HEAD:"); 1245 | git_object_free(obj); 1246 | } 1247 | 1248 | if (!git_revparse_single(&obj, repo, "HEAD:.gitmodules") && 1249 | git_object_type(obj) == GIT_OBJ_BLOB) 1250 | submodules = ".gitmodules"; 1251 | git_object_free(obj); 1252 | 1253 | /* log for HEAD */ 1254 | fp = efopen("log.html", "w"); 1255 | relpath = ""; 1256 | mkdir("commit", S_IRWXU | S_IRWXG | S_IRWXO); 1257 | writeheader(fp, "Log"); 1258 | fputs("\n" 1259 | "" 1260 | "" 1261 | "" 1262 | "\n\n", fp); 1263 | 1264 | if (cachefile && head) { 1265 | /* read from cache file (does not need to exist) */ 1266 | if ((rcachefp = fopen(cachefile, "r"))) { 1267 | if (!fgets(lastoidstr, sizeof(lastoidstr), rcachefp)) 1268 | errx(1, "%s: no object id", cachefile); 1269 | if (git_oid_fromstr(&lastoid, lastoidstr)) 1270 | errx(1, "%s: invalid object id", cachefile); 1271 | } 1272 | 1273 | /* write log to (temporary) cache */ 1274 | if ((fd = mkstemp(tmppath)) == -1) 1275 | err(1, "mkstemp"); 1276 | if (!(wcachefp = fdopen(fd, "w"))) 1277 | err(1, "fdopen: '%s'", tmppath); 1278 | /* write last commit id (HEAD) */ 1279 | git_oid_tostr(buf, sizeof(buf), head); 1280 | fprintf(wcachefp, "%s\n", buf); 1281 | 1282 | writelog(fp, head); 1283 | 1284 | if (rcachefp) { 1285 | /* append previous log to log.html and the new cache */ 1286 | while (!feof(rcachefp)) { 1287 | n = fread(buf, 1, sizeof(buf), rcachefp); 1288 | if (ferror(rcachefp)) 1289 | err(1, "fread"); 1290 | if (fwrite(buf, 1, n, fp) != n || 1291 | fwrite(buf, 1, n, wcachefp) != n) 1292 | err(1, "fwrite"); 1293 | } 1294 | fclose(rcachefp); 1295 | } 1296 | fclose(wcachefp); 1297 | } else { 1298 | if (head) 1299 | writelog(fp, head); 1300 | } 1301 | 1302 | fputs("
DateCommit messageAuthorFiles+-
", fp); 1303 | writefooter(fp); 1304 | fclose(fp); 1305 | 1306 | /* files for HEAD */ 1307 | fp = efopen("files.html", "w"); 1308 | writeheader(fp, "Files"); 1309 | if (head) 1310 | writefiles(fp, head); 1311 | writefooter(fp); 1312 | fclose(fp); 1313 | 1314 | /* summary page with branches and tags */ 1315 | fp = efopen("refs.html", "w"); 1316 | writeheader(fp, "Refs"); 1317 | writerefs(fp); 1318 | writefooter(fp); 1319 | fclose(fp); 1320 | 1321 | /* Atom feed */ 1322 | fp = efopen("atom.xml", "w"); 1323 | writeatom(fp, 1); 1324 | fclose(fp); 1325 | 1326 | /* Atom feed for tags / releases */ 1327 | fp = efopen("tags.xml", "w"); 1328 | writeatom(fp, 0); 1329 | fclose(fp); 1330 | 1331 | /* rename new cache file on success */ 1332 | if (cachefile && head) { 1333 | if (rename(tmppath, cachefile)) 1334 | err(1, "rename: '%s' to '%s'", tmppath, cachefile); 1335 | umask((mask = umask(0))); 1336 | if (chmod(cachefile, 1337 | (S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH) & ~mask)) 1338 | err(1, "chmod: '%s'", cachefile); 1339 | } 1340 | 1341 | /* cleanup */ 1342 | git_repository_free(repo); 1343 | git_libgit2_shutdown(); 1344 | 1345 | return 0; 1346 | } 1347 | -------------------------------------------------------------------------------- /strlcat.c: -------------------------------------------------------------------------------- 1 | /* $OpenBSD: strlcat.c,v 1.15 2015/03/02 21:41:08 millert Exp $ */ 2 | 3 | /* 4 | * Copyright (c) 1998, 2015 Todd C. Miller 5 | * 6 | * Permission to use, copy, modify, and distribute this software for any 7 | * purpose with or without fee is hereby granted, provided that the above 8 | * copyright notice and this permission notice appear in all copies. 9 | * 10 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | */ 18 | 19 | #include 20 | #include 21 | 22 | #include "compat.h" 23 | 24 | /* 25 | * Appends src to string dst of size dsize (unlike strncat, dsize is the 26 | * full size of dst, not space left). At most dsize-1 characters 27 | * will be copied. Always NUL terminates (unless dsize <= strlen(dst)). 28 | * Returns strlen(src) + MIN(dsize, strlen(initial dst)). 29 | * If retval >= dsize, truncation occurred. 30 | */ 31 | size_t 32 | strlcat(char *dst, const char *src, size_t dsize) 33 | { 34 | const char *odst = dst; 35 | const char *osrc = src; 36 | size_t n = dsize; 37 | size_t dlen; 38 | 39 | /* Find the end of dst and adjust bytes left but don't go past end. */ 40 | while (n-- != 0 && *dst != '\0') 41 | dst++; 42 | dlen = dst - odst; 43 | n = dsize - dlen; 44 | 45 | if (n-- == 0) 46 | return(dlen + strlen(src)); 47 | while (*src != '\0') { 48 | if (n != 0) { 49 | *dst++ = *src; 50 | n--; 51 | } 52 | src++; 53 | } 54 | *dst = '\0'; 55 | 56 | return(dlen + (src - osrc)); /* count does not include NUL */ 57 | } 58 | -------------------------------------------------------------------------------- /strlcpy.c: -------------------------------------------------------------------------------- 1 | /* $OpenBSD: strlcpy.c,v 1.12 2015/01/15 03:54:12 millert Exp $ */ 2 | 3 | /* 4 | * Copyright (c) 1998, 2015 Todd C. Miller 5 | * 6 | * Permission to use, copy, modify, and distribute this software for any 7 | * purpose with or without fee is hereby granted, provided that the above 8 | * copyright notice and this permission notice appear in all copies. 9 | * 10 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | */ 18 | 19 | #include 20 | #include 21 | 22 | #include "compat.h" 23 | 24 | /* 25 | * Copy string src to buffer dst of size dsize. At most dsize-1 26 | * chars will be copied. Always NUL terminates (unless dsize == 0). 27 | * Returns strlen(src); if retval >= dsize, truncation occurred. 28 | */ 29 | size_t 30 | strlcpy(char *dst, const char *src, size_t dsize) 31 | { 32 | const char *osrc = src; 33 | size_t nleft = dsize; 34 | 35 | /* Copy as many bytes as will fit. */ 36 | if (nleft != 0) { 37 | while (--nleft != 0) { 38 | if ((*dst++ = *src++) == '\0') 39 | break; 40 | } 41 | } 42 | 43 | /* Not enough room in dst, add NUL and traverse rest of src. */ 44 | if (nleft == 0) { 45 | if (dsize != 0) 46 | *dst = '\0'; /* NUL-terminate dst */ 47 | while (*src++) 48 | ; 49 | } 50 | 51 | return(src - osrc - 1); /* count does not include NUL */ 52 | } 53 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: #000; 3 | background-color: #fff; 4 | font-family: monospace; 5 | } 6 | 7 | h1, h2, h3, h4, h5, h6 { 8 | font-size: 1em; 9 | margin: 0; 10 | } 11 | 12 | img, h1, h2 { 13 | vertical-align: middle; 14 | } 15 | 16 | img { 17 | border: 0; 18 | } 19 | 20 | a:target { 21 | background-color: #ccc; 22 | } 23 | 24 | a.d, 25 | a.h, 26 | a.i, 27 | a.line { 28 | text-decoration: none; 29 | } 30 | 31 | #blob a { 32 | color: #777; 33 | } 34 | 35 | #blob a:hover { 36 | color: blue; 37 | text-decoration: none; 38 | } 39 | 40 | table thead td { 41 | font-weight: bold; 42 | } 43 | 44 | table td { 45 | padding: 0 0.4em; 46 | } 47 | 48 | #content table td { 49 | vertical-align: top; 50 | white-space: nowrap; 51 | } 52 | 53 | #branches tr:hover td, 54 | #tags tr:hover td, 55 | #index tr:hover td, 56 | #log tr:hover td, 57 | #files tr:hover td { 58 | background-color: #eee; 59 | } 60 | 61 | #index tr td:nth-child(2), 62 | #tags tr td:nth-child(3), 63 | #branches tr td:nth-child(3), 64 | #log tr td:nth-child(2) { 65 | white-space: normal; 66 | } 67 | 68 | td.num { 69 | text-align: right; 70 | } 71 | 72 | .desc { 73 | color: #777; 74 | } 75 | 76 | hr { 77 | border: 0; 78 | border-top: 1px solid #777; 79 | height: 1px; 80 | } 81 | 82 | pre { 83 | font-family: monospace; 84 | } 85 | 86 | pre a.h { 87 | color: #00a; 88 | } 89 | 90 | .A, 91 | span.i, 92 | pre a.i { 93 | color: #070; 94 | } 95 | 96 | .D, 97 | span.d, 98 | pre a.d { 99 | color: #e00; 100 | } 101 | 102 | pre a.h:hover, 103 | pre a.i:hover, 104 | pre a.d:hover { 105 | text-decoration: none; 106 | } 107 | --------------------------------------------------------------------------------