commit be306cd215cd57614a3e5746fe0939fefbe63678 Author: acidvegas Date: Thu Jun 15 19:50:38 2023 -0400 Initial commit 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 0000000..1b79ec1 Binary files /dev/null and b/assets/acidvegas.png differ diff --git a/assets/favicon.png b/assets/favicon.png new file mode 100644 index 0000000..a7bc302 Binary files /dev/null and b/assets/favicon.png differ 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 0000000..a7bc302 Binary files /dev/null and b/assets/logo.png differ diff --git a/assets/mostdangerous.png b/assets/mostdangerous.png new file mode 100644 index 0000000..e1e5d1c Binary files /dev/null and b/assets/mostdangerous.png differ diff --git a/assets/post-recieve b/assets/post-recieve new file mode 100755 index 0000000..34c55ef --- /dev/null +++ b/assets/post-recieve @@ -0,0 +1,40 @@ +#!/bin/sh +# stagit setup helper - developed by acidevegas (https://git.acid.vegas/stagit) +# sudo apt-get install libgit2-1.1 libmd4c* + +BASE_URL="https://git.acid.vegas" +CLONE_URL="git://git.acid.vegas" +HTML_DIR="/home/acidvegas/dev/html/stagit/out" +REPOS="$(find $HOME/dev/git -type d -name mirror -prune -o -type d -name .git -print | sort | xargs echo)" + +mkdir -p $HTML_DIR +cp favicon.png logo.png style.css $HTML_DIR +for dir in $(echo $REPOS); do + OWNER=$(basename $(dirname $(dirname $dir))) + REPO=$(basename $(dirname $dir)) + if [ -f $dir/description ]; then + REPO_DESC=$(cat $dir/description) + if [ "$REPO_DESC" = "Unnamed repository; edit this file 'description' to name the repository." ]; then + nano $dir/description + echo "[+] updated default 'description' file for '$REPO'" + fi + else + nano $dir/description + echo "[+] added missing 'description' file for '$REPO'" + fi + if [ ! -f $dir/owner ]; then + echo $OWNER > $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 */ +}