From be306cd215cd57614a3e5746fe0939fefbe63678 Mon Sep 17 00:00:00 2001 From: acidvegas Date: Thu, 15 Jun 2023 19:50:38 -0400 Subject: [PATCH] Initial commit --- Makefile | 110 +++ README.md | 31 + assets/acidvegas.png | Bin 0 -> 13992 bytes assets/favicon.png | Bin 0 -> 505 bytes assets/helper | 62 ++ assets/logo.png | Bin 0 -> 505 bytes assets/mostdangerous.png | Bin 0 -> 8200 bytes assets/post-recieve | 40 ++ assets/style.css | 42 ++ compat.h | 6 + reallocarray.c | 39 ++ stagit-index.1 | 47 ++ stagit-index.c | 216 ++++++ stagit.1 | 125 ++++ stagit.c | 1404 ++++++++++++++++++++++++++++++++++++++ strlcat.c | 57 ++ strlcpy.c | 52 ++ 17 files changed, 2231 insertions(+) create mode 100644 Makefile create mode 100644 README.md create mode 100644 assets/acidvegas.png create mode 100644 assets/favicon.png create mode 100755 assets/helper create mode 100644 assets/logo.png create mode 100644 assets/mostdangerous.png create mode 100755 assets/post-recieve create mode 100644 assets/style.css create mode 100644 compat.h create mode 100644 reallocarray.c create mode 100644 stagit-index.1 create mode 100644 stagit-index.c create mode 100644 stagit.1 create mode 100644 stagit.c create mode 100644 strlcat.c create mode 100644 strlcpy.c diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1c4e4a2 --- /dev/null +++ b/Makefile @@ -0,0 +1,110 @@ +.POSIX: + +NAME = stagit +VERSION = 1.2 + +# paths +PREFIX = /usr/local +MANPREFIX = ${PREFIX}/man +DOCPREFIX = ${PREFIX}/share/doc/${NAME} + +LIB_INC = -I/usr/local/include +LIB_LIB = -L/usr/local/lib -lgit2 -lmd4c-html + +# use system flags. +STAGIT_CFLAGS = ${LIB_INC} ${CFLAGS} +STAGIT_LDFLAGS = ${LIB_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.md +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 assets/favicon.png assets/logo.png assets/style.css assets/helper ${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 assets/style.css\ + assets/favicon.png\ + assets/logo.png\ + assets/helper\ + README.md\ + ${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.md + -rmdir ${DESTDIR}${DOCPREFIX} + # removing manual pages. + for m in ${MAN1}; do rm -f ${DESTDIR}${MANPREFIX}/man1/$$m; done + +.PHONY: all clean dist install uninstall diff --git a/README.md b/README.md new file mode 100644 index 0000000..a6566d8 --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +# stagit +> static git page generator + +## Information +This is my personal fork of [stagit](https://codemadness.org/stagit.html) which is running [git.acid.vegas](https://git.acid.vegas/) + +## Dependencies +- C compiler *(C99)* +- libc *(tested with OpenBSD, FreeBSD, NetBSD, Linux: glibc and musl)* +- [libgit2](https://github.com/libgit2/libgit2) *(v0.22+)* +- [md4c](https://github.com/mity/md4c) *(v0.4.4+)* +- POSIX make *(optional)* + +## Setup +```shell +cd stagit +make +sudo make install +``` + +###### New Features +- Markdown rendering to HTML for README files +- Syntax hilighting +- Repository categories +- Direct download to repository tar.gz +- Style changes + +###### Props +- Hiltjo Posthuma *(orignal author of [stagit](https://codemadness.org/git/stagit/))* +- Larry Burns *([stagit-md](https://github.com/lmburns/stagit-md))* +- Oscar Benedito *([md4c implementation](https://oscarbenedito.com/blog/2020/08/adding-about-pages-to-stagit/))* \ No newline at end of file diff --git a/assets/acidvegas.png b/assets/acidvegas.png new file mode 100644 index 0000000000000000000000000000000000000000..1b79ec119ae29fe6add112ba7e212240ea196ce6 GIT binary patch literal 13992 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 O00004nJ zXtsecqtcuEJwQRp64!{5;QX|b^2DN4hVt@qz0ADq;^f4FRK5J7^x5xhq!<_&%RF5i zLn`LHy&ITy$U(&6;*t{Pbw_nyM8!E53Apn9N|vn8b91rQ_^nlgPI`C029G?&dRrMK}e76~0C8KTyB5 zeCy_;EI>6N5jBUL8(J?4EPAT0L>n?fHFFA7@7lZI_19fhe~Wg$*;T)O?eqJyIic!( z8g5)(+vK+M`>IS4VXz544Ko-v=7?@7KmR;*CXfv`1!O!(5-3o9pL^5u&(=s9rrU~t z$$4JCZ3aRI@9(Qe%6~8W^RYtalMF&BHuFG!KYuf=7gs%~d`kKjFq#-VUHx3vIVCg!06n(4S^xk5 literal 0 HcmV?d00001 diff --git a/assets/helper b/assets/helper new file mode 100755 index 0000000..fe43d43 --- /dev/null +++ b/assets/helper @@ -0,0 +1,62 @@ +#!/bin/sh +# stagit setup helper - developed by acidevegas (https://git.acid.vegas/stagit) +# sudo apt-get install libgit2-1.1 libmd4c* + +URL="git.acid.vegas" +PROTO="https" +CLONE_URL="git://$URL" +COMMIT_LIMIT=100 +HTML_DIR="$HOME/dev/git/acidvegas/git.acid.vegas" +REPOS_DIR="$HOME/dev/git" +REPO_EXCLUDE="git.acid.vegas" +REPOS="$(find $REPOS_DIR -type d -name mirror -prune -o -type d -name $REPO_EXCLUDE -prune -o -type d -name .git -printf "%h\\n " | sort | xargs echo)" + +prepair() { + [ -d $HTML_DIR ] && rm -rf $HTML_DIR/* + mkdir -p $HTML_DIR/assets + cp acidvegas.png favicon.png logo.png mostdangerous.png style.css $HTML_DIR/assets + echo $URL > $HTML_DIR/CNAME +} + +make_index() { + echo "[~] creating index..." + args="" + for dir in $(ls $REPOS_DIR | grep -v 'mirror'); do + echo "[~] indexing '$dir' repositories..." + DIR_REPOS="$(find $REPOS_DIR/$dir -type d -name mirror -prune -o -type d -name $REPO_EXCLUDE -prune -o -type d -name .git -printf "%h\\n " | sort | xargs echo)" + args="$args -c \"$dir\" $DIR_REPOS" + done + echo "$args" | xargs stagit-index > $HTML_DIR/index.html + echo "[+] finished" +} + +make_repos() { + for dir in $(echo $REPOS); do + USER=$(basename $(dirname $dir)) + REPO=$(basename $dir) + if [ -f $dir/.git/description ]; then + if [ "$(cat $dir/.git/description)" = "Unnamed repository; edit this file 'description' to name the repository." ]; then + read -p "description for '$USER/$REPO':" desc + echo "$desc" > $dir/.git/description + echo "[+] updated default 'description' file for '$REPO'" + fi + else + read -p "description for '$USER/$REPO':" desc + echo "$desc" > $dir/.git/description + echo "[+] added missing 'description' file for '$REPO'" + fi + if [ ! -f $dir/.git/url ]; then + echo "$CLONE_URL/$REPO.git" > $dir/.git/url + echo "[+] added missing 'url' file for '$REPO'" + fi + echo "[~] processing '$REPO' repository..." + mkdir -p $HTML_DIR/$REPO && cd $HTML_DIR/$REPO && stagit -l $COMMIT_LIMIT -u "$PROTO://$URL/$REPO" $dir + ln -sf log.html index.html + #git --git-dir $dir/.git archive --format=tar.gz -o "$HTML_DIR/$REPO/$REPO.tar.gz" --prefix="$REPO/" HEAD + done +} + +# Main +prepair +make_repos +make_index \ No newline at end of file diff --git a/assets/logo.png b/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..a7bc30200b520d21401b9d543eb73f013342cee6 GIT binary patch literal 505 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGoEX7WqAsj$Z!;#Vf4nJ zXtsecqtcuEJwQRp64!{5;QX|b^2DN4hVt@qz0ADq;^f4FRK5J7^x5xhq!<_&%RF5i zLn`LHy&ITy$U(&6;*t{Pbw_nyM8!E53Apn9N|vn8b91rQ_^nlgPI`C029G?&dRrMK}e76~0C8KTyB5 zeCy_;EI>6N5jBUL8(J?4EPAT0L>n?fHFFA7@7lZI_19fhe~Wg$*;T)O?eqJyIic!( z8g5)(+vK+M`>IS4VXz544Ko-v=7?@7KmR;*CXfv`1!O!(5-3o9pL^5u&(=s9rrU~t z$$4JCZ3aRI@9(Qe%6~8W^RYtalMF&BHuFG!KYuf=7gs%~d`kKjFq#-VUHx3vIVCg!06n(4S^xk5 literal 0 HcmV?d00001 diff --git a/assets/mostdangerous.png b/assets/mostdangerous.png new file mode 100644 index 0000000000000000000000000000000000000000..e1e5d1c9cd5de12ae07911e087986d82bc2f5db9 GIT binary patch literal 8200 zcmV+jAot&iP)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00002 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 $dir/owner + echo "[+] added missing 'owner' file for '$REPO'" + fi + if [ ! -f $dir/url ]; then + echo "$CLONE_URL/$REPO.git" > $dir/url + echo "[+] added missing 'url' file for '$REPO'" + fi + echo "[~] processing '$REPO' repository..." + mkdir -p $HTML_DIR/$REPO && cd $HTML_DIR/$REPO && stagit -c ".cache" -u "$BASE_URL/$REPO" $dir + ln -sf log.html index.html && ln -sf ../style.css style.css && ln -sf ../logo.png logo.png && ln -sf ../favicon.png favicon.png + git --git-dir $dir archive HEAD -o "$HTML_DIR/$REPO/$REPO.zip" # TODO: can we do this with libgit2? +done +echo "[~] creating index..." +stagit-index "$REPOS" > $HTML_DIR/index.html +echo "[+] finished" \ No newline at end of file diff --git a/assets/style.css b/assets/style.css new file mode 100644 index 0000000..b72ad89 --- /dev/null +++ b/assets/style.css @@ -0,0 +1,42 @@ +*{font-family:monospace} +a{color:#8dc;text-decoration:none} +a:hover{color:#8dc} +a:target {background-color:#222;} +body{background-color:#000;color:#bdbdbd;font-size:14px;} +h1{display:inline;} +table thead td{font-weight:bold;} +table td{padding:0 0.3em;} +table tr{margin:0px;} +img{max-width:800px} +.border-bottom{border-bottom:1px solid #222;} +.container{border:1px solid #222;border-radius:5px;padding-top:10px;padding-bottom:10px;overflow-x:auto;} +.desc{color:#aaa;} +td.num{text-align:right;} +#blob{text-align:left;} +#blob a{color:#555;} +#blob a:hover{color:#56c8ff;text-decoration:none;} +#blob a:target{color:#eee;} +#content table td{vertical-align:top;white-space:nowrap;} +#index .category td {font-style: italic;} +#index .item-repo td:first-child{padding-left:1.5em;} +#footer{font-size:smaller;padding-top:10px;} +#files tbody tr:hover td, #index .item-repo:hover td, #log tbody tr:hover td{background-color:#111} +#files tr td:nth-child(2), #index .item-repo td:nth-child(2), #log tr td:nth-child(2){white-space:normal;} +.container, #container, #content, #footer, #index, #log{width:100%;max-width:800px;} + +a.d, a.h, a.i, a.line {text-decoration:none;} +h1, h2, h3, h4, h5, h6{font-size:1em;margin:0;} +img, h1, h2{vertical-align middle;} +#branches tr td:nth-child(3), +#branches tr:hover td, #tags tr:hover td{background-color:#111;} +#tags tr td:nth-child(3){white-space:normal;} +.A, span.i, pre a.i{color:#00cd00;} +.D, span.d, pre a.d {color:#cd0000;} +.md{text-align:left;} +.md h1{font-size:1.5em;} +.md h2{font-size:1.25em;} +.md table{border-collapse:collapse;margin:1em 1em;border:1px solid var(--border);} +.md table td, .md table th{padding:0.25em 1em;border:1px solid var(--border);} +pre a.h{color:#00cdcd;} +pre a.h:hover, pre a.i:hover, pre a.d:hover{text-decoration:none;} +pre:not(#readme){overflow-x:auto;border:1px solid var(--code-border);border-radius:4px;} diff --git a/compat.h b/compat.h new file mode 100644 index 0000000..f97a69b --- /dev/null +++ b/compat.h @@ -0,0 +1,6 @@ +#undef strlcat +size_t strlcat(char *, const char *, size_t); +#undef strlcpy +size_t strlcpy(char *, const char *, size_t); +#undef reallocarray +void *reallocarray(void *, size_t, size_t); diff --git a/reallocarray.c b/reallocarray.c new file mode 100644 index 0000000..b92dae5 --- /dev/null +++ b/reallocarray.c @@ -0,0 +1,39 @@ +/* + * 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); +} diff --git a/stagit-index.1 b/stagit-index.1 new file mode 100644 index 0000000..720941d --- /dev/null +++ b/stagit-index.1 @@ -0,0 +1,47 @@ +.Dd August 2, 2021 +.Dt STAGIT-INDEX 1 +.Os +.Sh NAME +.Nm stagit-index +.Nd static git index page generator +.Sh SYNOPSIS +.Nm +.Op Ar repodir... +.Sh DESCRIPTION +.Nm +will create an index HTML page for the repositories specified and writes +the HTML data to stdout. +The repos in the index are in the same order as the arguments +.Ar repodir +specified. +.Pp +The basename of the directory is used as the repository name. +The suffix ".git" is removed from the basename, this suffix is commonly used +for "bare" repos. +.Pp +The content of the follow files specifies the meta data for each repository: +.Bl -tag -width Ds +.It .git/description or description (bare repos). +description +.It .git/owner or owner (bare repo). +owner of repository +.El +.Pp +For changing the style of the page you can use the following files: +.Bl -tag -width Ds +.It favicon.png +favicon image. +.It logo.png +32x32 logo. +.It style.css +CSS stylesheet. +.El +.Sh EXAMPLES +.Bd -literal +cd htmlroot +stagit-index path/to/gitrepo1 path/to/gitrepo2 > index.html +.Ed +.Sh SEE ALSO +.Xr stagit 1 +.Sh AUTHORS +.An Hiltjo Posthuma Aq Mt hiltjo@codemadness.org diff --git a/stagit-index.c b/stagit-index.c new file mode 100644 index 0000000..f6b5cbf --- /dev/null +++ b/stagit-index.c @@ -0,0 +1,216 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +static git_repository *repo; +static const char *relpath = ""; +static char description[255] = "Acidvegas Repositories"; +static char *name = ""; +static char category[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", intm); + fputs(out, fp); +} + +void writeheader(FILE *fp) { + fputs("\n\n\n", fp); + xmlencode(fp, description, strlen(description)); + fputs("\n\n" + "\n" + "\n", fp); + fputs("\n" + "\n", fp); + fputs("

\n
\n

\n" + "
\n\t\n\t\t\n\t\t\t\n\t\t\n\t\t", fp); +} + +void writefooter(FILE *fp) { + fputs("\n\t\t\n\t
NameDescriptionLast commit
\n
\n
\n" + "\t© 2023 acidvegas, inc • generated with stagit\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("\n\t\t\t", fp); + xmlencode(fp, stripped_name, strlen(stripped_name)); + fputs("", fp); + xmlencode(fp, description, strlen(description)); + 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++) { + if (!strcmp(argv[i], "-c")) { + i++; + if (i == argc) + err(1, "missing argument"); + repodir = argv[i]; + fputs("\n\t\t\t", stdout); + xmlencode(stdout, repodir, strlen(repodir)); + fputs("", stdout); + continue; + } + + 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); + } + writelog(stdout); + } + writefooter(stdout); + + /* cleanup */ + git_repository_free(repo); + git_libgit2_shutdown(); + + checkfileerror(stdout, "", 'w'); + + return ret; +} diff --git a/stagit.1 b/stagit.1 new file mode 100644 index 0000000..6585f65 --- /dev/null +++ b/stagit.1 @@ -0,0 +1,125 @@ +.Dd August 2, 2021 +.Dt STAGIT 1 +.Os +.Sh NAME +.Nm stagit +.Nd static git page generator +.Sh SYNOPSIS +.Nm +.Op Fl c Ar cachefile +.Op Fl l Ar commits +.Op Fl u Ar baseurl +.Ar repodir +.Sh DESCRIPTION +.Nm +writes HTML pages for the repository +.Ar repodir +to the current directory. +.Pp +The options are as follows: +.Bl -tag -width Ds +.It Fl c Ar cachefile +Cache the entries of the log page up to the point of +the last commit. +The +.Ar cachefile +will store the last commit id and the entries in the HTML table. +It is up to the user to make sure the state of the +.Ar cachefile +is in sync with the history of the repository. +.It Fl l Ar commits +Write a maximum number of +.Ar commits +to the log.html file only. +However the commit files are written as usual. +.It Fl u Ar baseurl +Base URL to make links in the Atom feeds absolute. +For example: "https://git.codemadness.org/stagit/". +.El +.Pp +The options +.Fl c +and +.Fl l +cannot be used at the same time. +.Pp +The following files will be written: +.Bl -tag -width Ds +.It atom.xml +Atom XML feed of the last 100 commits. +.It tags.xml +Atom XML feed of the tags. +.It files.html +List of files in the latest tree, linking to the file. +.It log.html +List of commits in reverse chronological applied commit order, each commit +links to a page with a diffstat and diff of the commit. +.It refs.html +Lists references of the repository such as branches and tags. +.El +.Pp +For each entry in HEAD a file will be written in the format: +file/filepath.html. +This file will contain the textual data of the file prefixed by line numbers. +The file will have the string "Binary file" if the data is considered to be +non-textual. +.Pp +For each commit a file will be written in the format: +commit/commitid.html. +This file will contain the diffstat and diff of the commit. +It will write the string "Binary files differ" if the data is considered to +be non-textual. +Too large diffs will be suppressed and a string +"Diff is too large, output suppressed" will be written. +.Pp +When a commit HTML file exists it won't be overwritten again, note that if +you've changed +.Nm +or changed one of the metadata files of the repository it is recommended to +recreate all the output files because it will contain old data. +To do this remove the output directory and +.Ar cachefile , +then recreate the files. +.Pp +The basename of the directory is used as the repository name. +The suffix ".git" is removed from the basename, this suffix is commonly used +for "bare" repos. +.Pp +The content of the follow files specifies the metadata for each repository: +.Bl -tag -width Ds +.It .git/description or description (bare repo). +description +.It .git/owner or owner (bare repo). +owner of repository +.It .git/url or url (bare repo). +primary clone URL of the repository, for example: +git://git.codemadness.org/stagit +.El +.Pp +When a README or LICENSE file exists in HEAD or a .gitmodules submodules file +exists in HEAD a direct link in the menu is made. +.Pp +For changing the style of the page you can use the following files: +.Bl -tag -width Ds +.It favicon.png +favicon image. +.It logo.png +32x32 logo. +.It style.css +CSS stylesheet. +.El +.Sh EXIT STATUS +.Ex -std +.Sh EXAMPLES +.Bd -literal +mkdir -p htmlroot/htmlrepo1 && cd htmlroot/htmlrepo1 +stagit path/to/gitrepo1 +# repeat for other repositories. +.Ed +.Pp +To update the HTML files when the repository is changed a git post-receive hook +can be used, see the file example_post-receive.sh for an example. +.Sh SEE ALSO +.Xr stagit-index 1 +.Sh AUTHORS +.An Hiltjo Posthuma Aq Mt hiltjo@codemadness.org diff --git a/stagit.c b/stagit.c new file mode 100644 index 0000000..f67ff9d --- /dev/null +++ b/stagit.c @@ -0,0 +1,1404 @@ +#include +#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: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", intm); + fputs(out, fp); +} + +void writeheader(FILE *fp, const char *title) { + fputs("\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)); + fputs("\n\n" + "\n" + "\n", fp); + fputs("\n" + "\n" + "\n", relpath); + fputs("\n", relpath); + fputs("
\n\n
\n


\n
\n
\n\t\n\t\t\n", fp); + if (cloneurl[0]) { + fputs("\t\t", fp); + } + fputs("\t\t\n\t

", 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); + fprintf(fp, " | README", relpath); + if (license) + fprintf(fp, " | LICENSE", + relpath, license); + fputs("
\n
\n
\n", fp); +} + +void writefooter(FILE *fp) { + fputs("
\n\n\n
\n" + "\t© 2023 acidvegas, inc • generated with stagit\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(">\n
Date: ", fp); + printtime(fp, &(ci->author->when)); + putc('\n', fp); + } + if (ci->msg) { + putc('\n', fp); + fputs("

", 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("

\n", + ci->filecount, ci->filecount == 1 ? "" : "s", + ci->addcount, ci->addcount == 1 ? "" : "s", + ci->delcount, ci->delcount == 1 ? "" : "s"); + + for (i = 0; i < ci->ndeltas; i++) { + patch = ci->deltas[i]->patch; + delta = git_patch_get_delta(patch); + fprintf(fp, "", fp); + //if (ci->author) + // xmlencode(fp, ci->author->name, strlen(ci->author->name)); + 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("
%zu file%s changed, %zu insertion%s(+), %zu deletion%s(-)

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); + fprintf(fp, "%zu", ci->filecount); + fputs("", fp); + fprintf(fp, "+%zu", ci->addcount); + fputs("", fp); + fprintf(fp, "-%zu", ci->delcount); + 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); +} + +void +process_output_md(const char* text, unsigned int size, void* fp) +{ + fprintf((FILE *)fp, "%.*s", size, text); +} + +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, r; + + for (i = 1; i < argc; i++) { + if (argv[i][0] != '-') { + if (repodir) + usage(argv[0]); + repodir = argv[i]; + } else if (argv[i][1] == 'c') { + if (nlogcommits > 0 || i + 1 >= argc) + usage(argv[0]); + cachefile = argv[++i]; + } else if (argv[i][1] == 'l') { + if (cachefile || i + 1 >= argc) + usage(argv[0]); + errno = 0; + nlogcommits = strtoll(argv[++i], &p, 10); + if (argv[i][0] == '\0' || *p != '\0' || + nlogcommits <= 0 || errno) + usage(argv[0]); + } else if (argv[i][1] == 'u') { + if (i + 1 >= argc) + usage(argv[0]); + baseurl = argv[++i]; + } + } + if (!repodir) + usage(argv[0]); + + if (!realpath(repodir, repodirabs)) + err(1, "realpath"); + + /* do not search outside the git repository: + GIT_CONFIG_LEVEL_APP is the highest level currently */ + git_libgit2_init(); + for (i = 1; i <= GIT_CONFIG_LEVEL_APP; i++) + git_libgit2_opts(GIT_OPT_SET_SEARCH_PATH, i, ""); + /* do not require the git repository to be owned by the current user */ + git_libgit2_opts(GIT_OPT_SET_OWNER_VALIDATION, 0); + +#ifdef __OpenBSD__ + if (unveil(repodir, "r") == -1) + err(1, "unveil: %s", repodir); + if (unveil(".", "rwc") == -1) + err(1, "unveil: ."); + if (cachefile && unveil(cachefile, "rwc") == -1) + err(1, "unveil: %s", cachefile); + + if (cachefile) { + if (pledge("stdio rpath wpath cpath fattr", NULL) == -1) + err(1, "pledge"); + } else { + if (pledge("stdio rpath wpath cpath", NULL) == -1) + err(1, "pledge"); + } +#endif + + if (git_repository_open_ext(&repo, repodir, + GIT_REPOSITORY_OPEN_NO_SEARCH, NULL) < 0) { + fprintf(stderr, "%s: cannot open repository\n", argv[0]); + return 1; + } + + /* find HEAD */ + if (!git_revparse_single(&obj, repo, "HEAD")) + head = git_object_id(obj); + git_object_free(obj); + + /* use directory name as name */ + if ((name = strrchr(repodirabs, '/'))) + name++; + else + name = ""; + + /* strip .git suffix */ + if (!(strippedname = strdup(name))) + err(1, "strdup"); + if ((p = strrchr(strippedname, '.'))) + if (!strcmp(p, ".git")) + *p = '\0'; + + /* read description or .git/description */ + joinpath(path, sizeof(path), repodir, "description"); + if (!(fpread = fopen(path, "r"))) { + joinpath(path, sizeof(path), repodir, ".git/description"); + fpread = fopen(path, "r"); + } + if (fpread) { + if (!fgets(description, sizeof(description), fpread)) + description[0] = '\0'; + checkfileerror(fpread, path, 'r'); + fclose(fpread); + } + + /* read url or .git/url */ + joinpath(path, sizeof(path), repodir, "url"); + if (!(fpread = fopen(path, "r"))) { + joinpath(path, sizeof(path), repodir, ".git/url"); + fpread = fopen(path, "r"); + } + if (fpread) { + if (!fgets(cloneurl, sizeof(cloneurl), fpread)) + cloneurl[0] = '\0'; + checkfileerror(fpread, path, 'r'); + fclose(fpread); + cloneurl[strcspn(cloneurl, "\n")] = '\0'; + } + + /* check LICENSE */ + for (i = 0; i < LEN(licensefiles) && !license; i++) { + if (!git_revparse_single(&obj, repo, licensefiles[i]) && + git_object_type(obj) == GIT_OBJ_BLOB) + license = licensefiles[i] + strlen("HEAD:"); + git_object_free(obj); + } + + /* check README */ + for (i = 0; i < LEN(readmefiles) && !readme; i++) { + if (!git_revparse_single(&obj, repo, readmefiles[i]) && + git_object_type(obj) == GIT_OBJ_BLOB) + readme = readmefiles[i] + strlen("HEAD:"); + r = i; + 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); + + /* README page */ + if (readme) { + fp = efopen("README.html", "w"); + writeheader(fp, "README"); + git_revparse_single(&obj, repo, readmefiles[r]); + const char *s = git_blob_rawcontent((git_blob *)obj); + if (r == 1) { + git_off_t len = git_blob_rawsize((git_blob *)obj); + fputs("
", fp); + if (md_html(s, len, process_output_md, fp, MD_FLAG_TABLES | MD_FLAG_TASKLISTS | + MD_FLAG_PERMISSIVEEMAILAUTOLINKS | MD_FLAG_PERMISSIVEURLAUTOLINKS, 0)) + fprintf(stderr, "Error parsing markdown\n"); + fputs("
\n", fp); + } else { + fputs("
", fp);
+			xmlencode(fp, s, strlen(s));
+			fputs("
\n", fp); + } + writefooter(fp); + fclose(fp); + } + + /* 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 messageFiles+-
", fp); + writefooter(fp); + checkfileerror(fp, "log.html", 'w'); + fclose(fp); + + /* files for HEAD */ + fp = efopen("files.html", "w"); + writeheader(fp, "Files"); + if (head) + writefiles(fp, head); + writefooter(fp); + checkfileerror(fp, "files.html", 'w'); + fclose(fp); + + /* summary page with branches and tags */ + fp = efopen("refs.html", "w"); + writeheader(fp, "Refs"); + writerefs(fp); + writefooter(fp); + checkfileerror(fp, "refs.html", 'w'); + fclose(fp); + + /* Atom feed */ + fp = efopen("atom.xml", "w"); + writeatom(fp, 1); + checkfileerror(fp, "atom.xml", 'w'); + fclose(fp); + + /* Atom feed for tags / releases */ + fp = efopen("tags.xml", "w"); + writeatom(fp, 0); + checkfileerror(fp, "tags.xml", 'w'); + fclose(fp); + + /* rename new cache file on success */ + if (cachefile && head) { + if (rename(tmppath, cachefile)) + err(1, "rename: '%s' to '%s'", tmppath, cachefile); + umask((mask = umask(0))); + if (chmod(cachefile, + (S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH) & ~mask)) + err(1, "chmod: '%s'", cachefile); + } + + /* cleanup */ + git_repository_free(repo); + git_libgit2_shutdown(); + + return 0; +} diff --git a/strlcat.c b/strlcat.c new file mode 100644 index 0000000..bbfa64f --- /dev/null +++ b/strlcat.c @@ -0,0 +1,57 @@ +/* $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 */ +} diff --git a/strlcpy.c b/strlcpy.c new file mode 100644 index 0000000..ab420b6 --- /dev/null +++ b/strlcpy.c @@ -0,0 +1,52 @@ +/* $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 */ +}