"GOT", but the "O" is a cute, smiling pufferfish. Index | Thread | Search

From:
Tracey Emery <tracey@traceyemery.net>
Subject:
Gotweb
To:
gameoftrees@openbsd.org
Date:
Wed, 15 Jan 2020 07:56:18 -0700

Download raw body.

Thread
  • Tracey Emery:

    Gotweb

    • Klemens Nanni:

      Gotweb

Hello got friends,

After several months, here is the first diff to introduce gotweb to the
got tree. To see a working example of the code, go to got.traceyemery.net.

Gotweb currently has the same functionality as tog: log, diff, tree, and
blame, and is still currently under development. As it stands, I think
it's ready to start the code audit and review, so I can integrate it to
the tree.

Ok?

Thanks,

Tracey

-- 

Tracey Emery

diff refs/heads/master refs/heads/gotweb
blob - 7aaea69b30edf716c5f17759fd96059ac9dd2286
blob + ed241a51a70f1885bc59f89cb7f41f4ce53b66bb
--- Makefile
+++ Makefile
@@ -3,7 +3,7 @@ SUBDIR = libexec got tog
 .PHONY: release dist
 
 .if make(regress) || make(obj) || make(clean) || make(release)
-SUBDIR += regress
+SUBDIR += regress gotweb
 .endif
 
 .include "got-version.mk"
@@ -24,5 +24,15 @@ dist: clean
 		| sort > got-dist.txt.new
 	diff -u got-dist.txt got-dist.txt.new
 	rm got-dist.txt.new
+
+web:
+	sed -i -e "s/MAKEWEB=No/MAKEWEB=Yes/" got-version.mk
+	${MAKE} -C gotweb
+	sed -i -e "s/MAKEWEB=Yes/MAKEWEB=No/" got-version.mk
+
+web-install:
+	sed -i -e "s/MAKEWEB=No/MAKEWEB=Yes/" got-version.mk
+	${MAKE} -C gotweb install
+	sed -i -e "s/MAKEWEB=Yes/MAKEWEB=No/" got-version.mk
 
 .include <bsd.subdir.mk>
blob - bb6ed69a102b03fb61b3fafbe7e4cd7487693514
blob + 92bb0fe0dfb2d9c4a0c0a77e31b6e0b719a28fbf
--- Makefile.inc
+++ Makefile.inc
@@ -27,3 +27,19 @@ DEBUG = -O0 -g
 .endif
 
 .endif
+
+.if ${MAKEWEB} == "Yes"
+LDADD =		-L${PREFIX}/lib -static -lkcgihtml -lkcgi -lz -lutil
+PREFIX =	/usr/local
+CHROOT_DIR =	/var/www
+GOTWEB_DIR =	/cgi-bin/gotweb
+LIBEXECDIR =	${GOTWEB_DIR}/libexec
+LIBEXEC_DIR =	${CHROOT_DIR}${LIBEXECDIR}
+ETC_DIR =	${CHROOT_DIR}/etc
+EXPL_DIR =	${ETC_DIR}/examples
+HTTPD_DIR =	${CHROOT_DIR}/htdocs
+TMP_DIR =	${CHROOT_DIR}/tmp
+PROG_DIR =	${HTTPD_DIR}/${PROG}
+CGI_DIR =	${CHROOT_DIR}${GOTWEB_DIR}
+TMPL_DIR =	${CGI_DIR}/gw_tmpl
+.endif
blob - 425ff087c9c19bea57b73571a2bf14704aefe5f2
blob + cfc139074e80a5efb48c2801604a1812054d2035
--- got-version.mk
+++ got-version.mk
@@ -1,5 +1,6 @@
 GOT_RELEASE=No
 GOT_VERSION_NUMBER=0.25
+MAKEWEB=No
 
 .if ${GOT_RELEASE} == Yes
 GOT_VERSION=${GOT_VERSION_NUMBER}
blob - /dev/null
blob + c865312166b95b41acd5cf9a60547d5c139e4969 (mode 644)
--- /dev/null
+++ gotweb/Makefile
@@ -0,0 +1,52 @@
+.PATH:${.CURDIR}/../lib
+
+SUBDIR = ../libexec
+
+.include "../got-version.mk"
+
+PROG =		gotweb
+SRCS =		gotweb.c parse.y blame.c commit_graph.c delta.c diff.c \
+		diffreg.c error.c fileindex.c object.c object_cache.c \
+		object_idset.c object_parse.c opentemp.c path.c pack.c \
+		privsep.c reference.c repository.c sha1.c worktree.c \
+		inflate.c buf.c rcsutil.c diff3.c lockfile.c \
+		deflate.c object_create.c delta_cache.c
+MAN =		${PROG}.conf.5
+
+CPPFLAGS +=	-I${.CURDIR}/../include -I${.CURDIR}/../lib -I${PREFIX}/include
+
+LDADD +=	-L${PREFIX}/lib -static -lkcgihtml -lkcgi -lz -lutil
+
+.if ${GOT_RELEASE} != "Yes"
+NOMAN = Yes
+.endif
+
+realinstall:
+	if [ ! -d ${CGI_DIR}/. ]; then \
+		${INSTALL} -d -o root -g daemon -m 755 ${CGI_DIR}; \
+	fi
+	${INSTALL} -c -o www -g www -m 0755 ${PROG} ${CGI_DIR}/${PROG}
+	if [ ! -d ${TMPL_DIR}/. ]; then \
+		${INSTALL} -d -o root -g daemon -m 755 ${TMPL_DIR}; \
+	fi
+	${INSTALL} -c -o www -g www -m 0755 files/cgi-bin/gw_tmpl/* ${TMPL_DIR}
+	if [ ! -d ${ETC_DIR}/. ]; then \
+		${INSTALL} -d -o root -g daemon -m 755 ${ETC_DIR}; \
+	fi
+	if [ ! -d ${EXPL_DIR}/. ]; then \
+		${INSTALL} -d -o root -g daemon -m 755 ${EXPL_DIR}; \
+	fi
+	${INSTALL} -c -o www -g www -m 0755 files/etc/gotweb.conf \
+		${ETC_DIR}/examples/gotweb.conf
+	if [ ! -d ${HTTPD_DIR}/. ]; then \
+		${INSTALL} -d -o root -g daemon -m 755 ${HTTPD_DIR}; \
+	fi
+	if [ ! -d ${TMP_DIR}/. ]; then \
+		${INSTALL} -d -o www -g www -m 755 ${TMP_DIR}; \
+	fi
+	if [ ! -d ${PROG_DIR}/. ]; then \
+		${INSTALL} -d -o root -g daemon -m 755 ${PROG_DIR}; \
+	fi
+	${INSTALL} -c -o www -g www -m 0755 files/htdocs/${PROG}/* ${PROG_DIR}
+
+.include <bsd.prog.mk>
blob - /dev/null
blob + c63ca834be73da69a2851808511a8bada49b3d10 (mode 644)
--- /dev/null
+++ gotweb/README
@@ -0,0 +1,61 @@
+Game of Trees Web (Gotweb) is a read-only web implementation of Got.
+
+Gotweb is still under development; it is being developed exclusively
+on OpenBSD and its target audience are OpenBSD developers. Gotweb is
+ISC-licensed and was designed with pledge(2) and unveil(2) in mind.
+
+Gotweb uses bare Git repositories to read versioned data and is designed to
+work with httpd(8).
+
+To compile the Gotweb tool suite on OpenBSD, run:
+
+ $ make web
+ # make web-install
+
+This will install Gotweb and all the required files to /var/www by default.
+
+Man page files in the Gotweb source tree can be viewed with 'man -l':
+
+ $ man -l gotweb/gotweb.conf.5
+
+Guidelines for reporting problems:
+
+Report all Gotweb problems in as much detail as possible. Gotweb code is not
+covered by automated tests.
+
+Mail problem reports to: gameoftrees@openbsd.org
+
+Example configuration for httpd.conf:
+
+ext_if = "*"
+
+types { include "/usr/share/misc/mime.types" }
+
+server "localhost" {
+	listen on $ext_if port 80
+
+	root "/htdocs/gotweb"
+
+	location "/cgi-bin/*" {
+		root "/"
+		fastcgi
+	}
+	location "/*" {
+		directory index "index.html"
+	}
+}
+
+Guidelines for submitting patches:
+
+Mail patches to: gameoftrees@openbsd.org
+Pull requests via any Git hosting sites will likely be overlooked.
+Please keep the intended target audience in mind when contributing to Gotweb.
+
+
+Subscribing to the gameoftrees@openbsd.org mailing list:
+
+The mailing list is used for patch reviews, bug reports, and user questions.
+To subscribe, send mail to majordomo@openbsd.org with a message body of:
+subscribe gameoftrees
+
+See https://www.openbsd.org/mail.html for more information.
blob - /dev/null
blob + a55dfa209eaad11211cb2f51ad6f29220cb4ce97 (mode 644)
--- /dev/null
+++ gotweb/TODO
@@ -0,0 +1,16 @@
+Complete templates.
+Complete stylesheets.
+Complete gw_funcs
+----
+
+- Add Prev/Next to shortlogs/logs that have more entries than
+  got_max_commits_display
+
+- Alter gw_get_repo_log full_log int to type more logs output for other options,
+  such as header displays for commit, commit_diff, etc.
+
+- Discuss how much of the gitweb functionality we really need or want. A user
+  can really go down an endless rabbit hole. Is it all worthwhile?
+
+- Redo index header, so columns are removed when content is set to not display
+  in gotweb.conf.
blob - /dev/null
blob + 12819268eddbfa9b3a5d68d8897c937bb20f7cd4 (mode 644)
--- /dev/null
+++ gotweb/files/cgi-bin/gw_tmpl/err.tmpl
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html>
+	<head>
+		<title>@@title@@</title>
+		@@head@@
+	</head>
+	<body>
+	<div id="gw_body">
+		<div id="header">
+			@@header@@
+		</div>
+		<div id="site_path">
+			@@sitepath@@
+			@@search@@
+		</div>
+		<div id="content">
+			@@content@@
+		</div>
+		@@siteowner@@
+	</div>
+	</body>
+</html>
blob - /dev/null
blob + 12819268eddbfa9b3a5d68d8897c937bb20f7cd4 (mode 644)
--- /dev/null
+++ gotweb/files/cgi-bin/gw_tmpl/index.tmpl
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html>
+	<head>
+		<title>@@title@@</title>
+		@@head@@
+	</head>
+	<body>
+	<div id="gw_body">
+		<div id="header">
+			@@header@@
+		</div>
+		<div id="site_path">
+			@@sitepath@@
+			@@search@@
+		</div>
+		<div id="content">
+			@@content@@
+		</div>
+		@@siteowner@@
+	</div>
+	</body>
+</html>
blob - /dev/null
blob + 2922ef159120afa07a3dd281602a31b6479d5deb (mode 644)
--- /dev/null
+++ gotweb/files/cgi-bin/gw_tmpl/summary.tmpl
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html>
+	<head>
+		<title>@@title@@</title>
+		@@head@@
+	</head>
+	<body>
+	<div id="gw_body">
+		<div id="header">
+			@@header@@
+		</div>
+		<div id="site_path">
+			@@sitepath@@
+			@@search@@
+		</div>
+		<div id="content">
+			<div id="summary_wrapper">
+				@@description@@
+				@@repo_owner@@
+				@@repo_age@@
+				@@cloneurl@@
+			</div>
+			@@content@@
+		</div>
+		@@siteowner@@
+	</div>
+	</body>
+</html>
blob - /dev/null
blob + c13c81b65d0c6ee672c348a1b1f122f822e97f10 (mode 644)
--- /dev/null
+++ gotweb/files/etc/gotweb.conf
@@ -0,0 +1,25 @@
+#
+# gotweb options
+# all paths relative to /var/www (httpd chroot jail)
+#
+
+got_repos_path			"/got/public"
+got_www_path			"/gotweb"
+
+#got_max_repos			100
+#got_max_repos_display		25
+got_max_commits_display		50
+
+got_site_name			"my public repos"
+got_site_owner			"Got Owner"
+got_site_link			"repos"
+
+got_logo			"got.png"
+got_logo_url			"https://gameoftrees.org"
+
+# on by default
+#got_show_site_owner		off
+#got_show_repo_owner		off
+#got_show_repo_age		false
+#got_show_repo_description	no
+#got_show_repo_cloneurl		off
blob - /dev/null
blob + f841f054bc2941b0cdca7e496ea69621671d6766 (mode 644)
blob - /dev/null
blob + 653a1510ce933f7fe9fbab2fcd171f04fa0b24cc (mode 644)
blob - /dev/null
blob + 460aa1299f8e9f37773618bcab2619794416fb49 (mode 644)
blob - /dev/null
blob + b3930d0f047184047cb81d620436d91653438b8b (mode 644)
--- /dev/null
+++ gotweb/files/htdocs/gotweb/browserconfig.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<browserconfig>
+    <msapplication>
+        <tile>
+            <square150x150logo src="/mstile-150x150.png"/>
+            <TileColor>#da532c</TileColor>
+        </tile>
+    </msapplication>
+</browserconfig>
blob - /dev/null
blob + f6c1a7c289faa4a48e03c97e68b1ba7a11dfddd1 (mode 644)
blob - /dev/null
blob + 0ceea8c0eabe73e8d12cf106d73c34abb1999cb2 (mode 644)
blob - /dev/null
blob + ee414573031ea5b310539196d2530a1e52d49b64 (mode 644)
blob - /dev/null
blob + 33933f80ee46217039804bc96672ede12b352b93 (mode 644)
blob - /dev/null
blob + 97ace786464b193baf1cd51e54016aea3016e62f (mode 644)
blob - /dev/null
blob + b78b527080051a7069ad50a643490667f4c6e85b (mode 644)
--- /dev/null
+++ gotweb/files/htdocs/gotweb/gotweb.css
@@ -0,0 +1,802 @@
+/*
+ * Copyright (c) 2019 Jerome Kasper <neon.king.fr@gmail.com>
+ * Copyright (c) 2019, 2020 Tracey Emery <tracey@traceyemery.net>
+ *
+ * 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.
+ */
+
+/* general sections */
+
+a {
+	color: #444444;
+	text-decoration: none;
+}
+a:hover {
+	color: Gold;
+	text-decoration: none;
+}
+body {
+	background-color: #ffffff;
+	color: #000000;
+	margin: 0;
+	padding: 0;
+	font-family: Arial, sans-serif;
+}
+
+.diff_minus, .diff_submodule {
+	color: magenta;
+}
+.diff_plus, .diff_symlink, .diff_author {
+	color: darkcyan;
+}
+.diff_chunk_header, .diff_date {
+	background-color: LightSlateGray;
+	color: yellow;
+}
+.diff_meta, .diff_executable, .diff_commit {
+	color: green;
+}
+.diff_directory {
+	color: blue;
+}
+
+#dotted_line {
+	clear: left;
+	float: left;
+	width: 100%;
+	border-top: 1px dotted #444444;
+}
+#solid_line {
+	clear: left;
+	float: left;
+	width: 100%;
+	border-top: 1px solid #444444;
+}
+#header {
+	overflow: auto;
+	width: 100%;
+	background-image: linear-gradient(to right, White, LightSlateGray);
+}
+#header a {
+	color: #ffffff;
+	font-size: 1.2em;
+	text-decoration: none;
+}
+#header a:hover {
+	color: Gold;
+	font-size: 1.2em;
+	text-decoration: none;
+}
+#site_path {
+	clear: left;
+	float: left;
+	overflow: auto;
+	width: 100%;
+	background-color: #243647;
+}
+#site_link {
+	float: left;
+	width: 40%;
+	padding-left: 10px;
+	padding-top: 5px;
+	padding-bottom: 5px;
+	color: #ffffff;
+	overflow: hidden;
+}
+#site_link a {
+	color: #ffffff;
+	text-decoration: none;
+}
+#search {
+	float: right;
+	padding-right: 10px;
+	padding-top: 5px;
+	padding-bottom: 5px;
+}
+#got_link {
+	float: left;
+	padding-bottom: 10px;
+	padding-top: 10px;
+}
+#content {
+	width: 100%;
+}
+#np_wrapper {
+	clear: left;
+	float: left;
+	width: 100%;
+	border-bottom: 1px dotted #444444;
+	background-color: #f5fcfb;
+	overflow: hidden;
+}
+#nav_prev {
+	float: left;
+	padding-left: 10px;
+	padding-top: 5px;
+	padding-bottom: 5px;
+	overflow: visible;
+}
+#nav_next {
+	padding-right: 10px;
+	padding-top: 5px;
+	padding-bottom: 5px;
+	text-align: right;
+	overflow: hidden;
+}
+#navs_wrapper {
+	clear: left;
+	float: left;
+	width: 100%;
+	background-color: #ced7e0;
+}
+#navs {
+	padding-left: 10px;
+	padding-top: 2px;
+	padding-bottom: 2px;
+	font-size: .8em;
+}
+#site_owner_wrapper {
+	clear: left;
+	float: left;
+	width: 100%;
+	background-color: LightSlateGray;
+	color: #ffffff;
+}
+#site_owner {
+	padding-left: 10px;
+	padding-top: 5px;
+	padding-bottom: 5px;
+}
+#description_title {
+	clear: left;
+	float: left;
+	width: 6.5em;
+	padding-left: 10px;
+	padding-top: 5px;
+	padding-bottom: 5px;
+}
+#description {
+	float: left;
+	width: 72%;
+	padding-top: 5px;
+	padding-bottom: 5px;
+}
+#repo_owner_title {
+	clear: left;
+	float: left;
+	width: 6.5em;
+	padding-left: 10px;
+	padding-top: 5px;
+	padding-bottom: 5px;
+}
+#repo_owner {
+	float: left;
+	width: 72%;
+	padding-top: 5px;
+	padding-bottom: 5px;
+}
+#last_change_title {
+	clear: left;
+	float: left;
+	width: 6.5em;
+	padding-left: 10px;
+	padding-top: 5px;
+	padding-bottom: 5px;
+}
+#last_change {
+	float: left;
+	width: 72%;
+	padding-top: 5px;
+	padding-bottom: 5px;
+}
+#cloneurl_title {
+	clear: left;
+	float: left;
+	width: 6.5em;
+	padding-left: 10px;
+	padding-top: 5px;
+	padding-bottom: 5px;
+}
+#cloneurl {
+	float: left;
+	width: 72%;
+	padding-top: 5px;
+	padding-bottom: 5px;
+	overflow: auto;
+}
+#logbriefs_wrapper {
+	clear: left;
+	float: left;
+	width: 100%;
+}
+#logbriefs_age {
+	padding-left: 10px;
+	padding-top: 5px;
+	padding-bottom: 5px;
+	float: left;
+	width: 7.5em;
+	overflow: auto;
+}
+#logbriefs_author {
+	float: left;
+	padding-top: 5px;
+	padding-bottom: 5px;
+	width: 8.5em;
+	font-style: italic;
+	overflow: auto;
+}
+#logbriefs_log {
+	float: left;
+	padding-left: 10px;
+	padding-right: 10px;
+	padding-top: 5px;
+	padding-bottom: 5px;
+	width: 65%;
+}
+#tags_wrapper {
+	clear: left;
+	float: left;
+	width: 100%;
+}
+#tags_age {
+	padding-left: 10px;
+	padding-top: 5px;
+	padding-bottom: 5px;
+	float: left;
+	width: 7.5em;
+	overflow: auto;
+}
+#tag {
+	float: left;
+	width: 8.5em;
+	font-style: italic;
+	padding-top: 5px;
+	padding-bottom: 5px;
+}
+#tag_name {
+	float: left;
+	padding-left: 10px;
+	padding-right: 10px;
+	padding-top: 5px;
+	padding-bottom: 5px;
+}
+#heads_wrapper {
+	clear: left;
+	float: left;
+	width: 100%;
+}
+#heads_age {
+	padding-left: 10px;
+	padding-top: 5px;
+	padding-bottom: 5px;
+	float: left;
+	width: 7.5em;
+	overflow: auto;
+}
+#head {
+	float: left;
+	padding-right: 10px;
+	padding-top: 5px;
+	padding-bottom: 5px;
+}
+#commit_commit_title {
+	clear: left;
+	float: left;
+	width: 6.5em;
+	padding-left: 10px;
+	padding-top: 2px;
+	padding-bottom: 2px;
+}
+#commit_commit {
+	float: left;
+	width: 72%;
+	padding-top: 2px;
+	padding-bottom: 2px;
+}
+#commit_diff_title {
+	clear: left;
+	float: left;
+	width: 6.5em;
+	padding-left: 10px;
+	padding-top: 2px;
+	padding-bottom: 2px;
+}
+#commit_diff {
+	float: left;
+	width: 72%;
+	padding-top: 2px;
+	padding-bottom: 2px;
+}
+#commit_author_title {
+	clear: left;
+	float: left;
+	width: 6.5em;
+	padding-left: 10px;
+	padding-top: 2px;
+	padding-bottom: 2px;
+}
+#commit_author {
+	float: left;
+	width: 72%;
+	padding-top: 2px;
+	padding-bottom: 2px;
+}
+#commit_committer_title {
+	clear: left;
+	float: left;
+	width: 6.5em;
+	padding-left: 10px;
+	padding-top: 2px;
+	padding-bottom: 2px;
+}
+#commit_committer {
+	float: left;
+	width: 72%;
+	padding-top: 2px;
+	padding-bottom: 2px;
+}
+#commit_age_title {
+	clear: left;
+	float: left;
+	width: 6.5em;
+	padding-left: 10px;
+	padding-top: 2px;
+	padding-bottom: 2px;
+}
+#commit_age {
+	float: left;
+	width: 72%;
+	padding-top: 2px;
+	padding-bottom: 2px;
+}
+#commit_log_title {
+	clear: left;
+	float: left;
+	width: 6.5em;
+	padding-left: 10px;
+	padding-top: 2px;
+	padding-bottom: 2px;
+}
+#commit_log {
+	float: left;
+	width: 72%;
+	padding-top: 2px;
+	padding-bottom: 2px;
+}
+
+/* index.tmpl */
+
+#index_header {
+	clear: left;
+	float: left;
+	overflow: auto;
+	width: 100%;
+	background-color: Khaki;
+}
+#index_header_project {
+	clear: left;
+	float: left;
+	width: 20%;
+	padding: 10px;
+}
+#index_header_description {
+	float: left;
+	width: 30%;
+	padding: 10px;
+}
+#index_header_owner {
+	float: left;
+	width: 12%;
+	padding: 10px;
+}
+#index_header_age {
+	padding: 10px;
+	overflow: hidden;
+}
+#index_wrapper {
+	clear: left;
+	float: left;
+	width: 100%;
+}
+#index_project {
+	float: left;
+	width: 20%;
+	padding: 10px;
+	overflow: hidden;
+}
+#index_project_description {
+	float: left;
+	width: 30%;
+	padding: 10px;
+	overflow: auto;
+}
+#index_project_owner {
+	float: left;
+	width: 12%;
+	padding: 10px;
+	overflow: hidden;
+}
+#index_project_age {
+	float: left;
+	width: 14%;
+	padding: 10px;
+	overflow: visible;
+}
+#index_project a {
+	color: #444444;
+	text-decoration: none;
+}
+#index_project a:hover {
+	color: SteelBlue;
+	text-decoration: none;
+}
+#index_project_navs a {
+	color: #444444;
+	text-decoration: none;
+}
+#index_project_navs a:hover {
+	color: SteelBlue;
+	text-decoration: none;
+}
+#index_next a {
+	color: #444444;
+	text-decoration: none;
+}
+#index_next a:hover {
+	color: SteelBlue;
+	text-decoration: none;
+}
+#index_prev a {
+	color: #444444;
+	text-decoration: none;
+}
+#index_prev a:hover {
+	color: SteelBlue;
+	text-decoration: none;
+}
+
+/* logs.tmpl */
+
+#logs_title_wrapper {
+	clear: left;
+	float: left;
+	width: 100%;
+	background-color: LightSlateGray;
+	color: #ffffff;
+}
+#logs_title {
+	padding-left: 10px;
+	padding-top: 5px;
+	padding-bottom: 5px;
+}
+#logs_content {
+	clear: left;
+	float: left;
+	width: 100%;
+}
+#logs_row_wrapper {
+	clear: left;
+	float: left;
+	background-color: #f5fcfb;
+	padding-top: 3px;
+	padding-bottom: 3px;
+	width: 100%;
+}
+#log {
+	clear: left;
+	float: left;
+	padding-left: 20px;
+	padding-top: 20px;
+	padding-bottom: 20px;
+}
+
+/* tag.tmpl */
+
+#log_tag_title_wrapper {
+	clear: left;
+	float: left;
+	width: 100%;
+	background-color: LightSlateGray;
+	color: #ffffff;
+}
+#log_tag_title {
+	padding-left: 10px;
+	padding-top: 5px;
+	padding-bottom: 5px;
+}
+#log_tag_content {
+	clear: left;
+	float: left;
+	width: 100%;
+}
+#log_tag_row_wrapper {
+	clear: left;
+	float: left;
+	background-color: #f5fcfb;
+	width: 100%;
+}
+#log_tag_commit {
+	clear: left;
+	float: left;
+	padding-left: 10px;
+	padding-top: 5px;
+	padding-bottom: 2px;
+}
+#log_tag {
+	clear: left;
+	float: left;
+	padding: 20px;
+	font-family: monospace;
+}
+
+/* blame.tmpl */
+
+#log_blame_title_wrapper {
+	clear: left;
+	float: left;
+	width: 100%;
+	background-color: LightSlateGray;
+	color: #ffffff;
+}
+#log_blame_title {
+	padding-left: 10px;
+	padding-top: 5px;
+	padding-bottom: 5px;
+}
+#log_blame_content {
+	clear: left;
+	float: left;
+	width: 100%;
+}
+#log_blame_row_wrapper {
+	clear: left;
+	float: left;
+	background-color: #f5fcfb;
+	width: 100%;
+}
+#log_blame_commit {
+	clear: left;
+	float: left;
+	padding-left: 10px;
+	padding-top: 5px;
+	padding-bottom: 2px;
+}
+#log_blame {
+	clear: left;
+	float: left;
+	padding: 20px;
+	font-family: monospace;
+	white-space: pre;
+	overflow: auto;
+}
+#blame_wrapper {
+	clear: left;
+	float: left;
+	width: 100%;
+}
+#blame_number {
+	float: left;
+	width: 3em;
+	overflow: hidden;
+}
+#blame_hash {
+	float: left;
+	width: 6em;
+	overflow: auto;
+}
+#blame_date {
+	float: left;
+	width: 7em;
+	overflow: auto;
+}
+#blame_author {
+	float: left;
+	width: 6em;
+	overflow: hidden;
+}
+#blame_code {
+	float:left;
+	width: 50%;
+	overflow: visible;
+}
+
+/* tree.tmpl */
+
+#log_tree_title_wrapper {
+	clear: left;
+	float: left;
+	width: 100%;
+	background-color: LightSlateGray;
+	color: #ffffff;
+}
+#log_tree_title {
+	padding-left: 10px;
+	padding-top: 5px;
+	padding-bottom: 5px;
+}
+#log_tree_content {
+	clear: left;
+	float: left;
+	width: 100%;
+}
+#log_tree_row_wrapper {
+	clear: left;
+	float: left;
+	background-color: #f5fcfb;
+	width: 100%;
+}
+#log_tree_commit {
+	clear: left;
+	float: left;
+	padding-left: 10px;
+	padding-top: 5px;
+	padding-bottom: 2px;
+}
+#log_tree {
+	clear: left;
+	float: left;
+	padding: 20px;
+	font-family: monospace;
+}
+#tree_wrapper {
+	clear: left;
+	float: left;
+	width: 100%;
+}
+#tree_id {
+	float: left;
+	padding: 2px;
+}
+#tree {
+	float:left ;
+	padding: 2px;
+}
+
+/* commit.tmpl */
+
+#log_commit_title_wrapper {
+	clear: left;
+	float: left;
+	width: 100%;
+	background-color: LightSlateGray;
+	color: #ffffff;
+}
+#log_commit_title {
+	padding-left: 10px;
+	padding-top: 5px;
+	padding-bottom: 5px;
+}
+#log_commit_content {
+	clear: left;
+	float: left;
+	width: 100%;
+}
+#log_commit_row_wrapper {
+	clear: left;
+	float: left;
+	background-color: #f5fcfb;
+	width: 100%;
+}
+#log_commit_commit {
+	clear: left;
+	float: left;
+	padding-left: 10px;
+	padding-top: 5px;
+	padding-bottom: 2px;
+}
+#log_commit {
+	clear: left;
+	float: left;
+	padding: 20px;
+	font-family: monospace;
+}
+
+/* diff.tmpl */
+
+#log_diff_title_wrapper {
+	clear: left;
+	float: left;
+	width: 100%;
+	background-color: LightSlateGray;
+	color: #ffffff;
+}
+#log_diff_title {
+	padding-left: 10px;
+	padding-top: 5px;
+	padding-bottom: 5px;
+}
+#log_diff_content {
+	clear: left;
+	float: left;
+	width: 100%;
+}
+#log_diff_row_wrapper {
+	clear: left;
+	float: left;
+	background-color: #f5fcfb;
+	width: 100%;
+}
+#log_commit_diff {
+	clear: left;
+	float: left;
+	padding-left: 10px;
+	padding-top: 5px;
+	padding-bottom: 2px;
+}
+#log_diff {
+	clear: left;
+	float: left;
+	padding: 20px;
+	font-family: monospace;
+	white-space: pre;
+}
+
+/* summary.tmpl */
+
+#summary_wrapper {
+	clear: left;
+	float: left;
+	width: 100%;
+	background-color: Khaki;
+}
+#summary_logbriefs_title_wrapper {
+	clear: left;
+	float: left;
+	width: 100%;
+	background-color: LightSlateGray;
+	color: #ffffff;
+}
+#summary_logbriefs_title {
+	padding-left: 10px;
+	padding-top: 5px;
+	padding-bottom: 5px;
+}
+#summary_logbriefs_content {
+	clear: left;
+	float: left;
+	width: 100%;
+}
+#summary_tags_title_wrapper {
+	clear: left;
+	float: left;
+	width: 100%;
+	background-color: LightSlateGray;
+	color: #ffffff;
+}
+#summary_tags_title {
+	padding-left: 10px;
+	padding-top: 5px;
+	padding-bottom: 5px;
+}
+#summary_tags_content {
+	clear: left;
+	float: left;
+	width: 100%;
+}
+#summary_heads_title_wrapper {
+	clear: left;
+	float: left;
+	width: 100%;
+	background-color: LightSlateGray;
+	color: #ffffff;
+}
+#summary_heads_title {
+	padding-left: 10px;
+	padding-top: 5px;
+	padding-bottom: 5px;
+}
+#summary_heads_content {
+	clear: left;
+	float: left;
+	width: 100%;
+}
blob - /dev/null
blob + 791e49544c8c5f82a710137fee5a2a4becaad616 (mode 644)
--- /dev/null
+++ gotweb/files/htdocs/gotweb/index.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+	<head>
+		<meta http-equiv="Refresh" content="0; url=/cgi-bin/gotweb/gotweb" />
+	</head>
+	<body>
+		<p><a href="/cgi-bin/gotweb/gotweb">gotweb</a></p>
+	</body>
+</html>
\ No newline at end of file
blob - /dev/null
blob + 0c47027971e9e0a5060e23fe73e7cb0399eacea8 (mode 644)
blob - /dev/null
blob + 96e67c7c4b7cb9b1b395281fae8d7cffa834a991 (mode 644)
--- /dev/null
+++ gotweb/files/htdocs/gotweb/safari-pinned-tab.svg
@@ -0,0 +1,15 @@
+<?xml version="1.0" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
+ "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
+<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
+ width="400.000000pt" height="400.000000pt" viewBox="0 0 400.000000 400.000000"
+ preserveAspectRatio="xMidYMid meet">
+<metadata>
+Created by potrace 1.11, written by Peter Selinger 2001-2013
+</metadata>
+<g transform="translate(0.000000,400.000000) scale(0.100000,-0.100000)"
+fill="#000000" stroke="none">
+<path d="M0 1995 l0 -1215 2000 0 2000 0 0 1215 0 1215 -2000 0 -2000 0 0
+-1215z"/>
+</g>
+</svg>
blob - /dev/null
blob + a1553eb86b573da072c732c9aabac5a80968461f (mode 644)
--- /dev/null
+++ gotweb/files/htdocs/gotweb/site.webmanifest
@@ -0,0 +1,19 @@
+{
+    "name": "",
+    "short_name": "",
+    "icons": [
+        {
+            "src": "/android-chrome-192x192.png",
+            "sizes": "192x192",
+            "type": "image/png"
+        },
+        {
+            "src": "/android-chrome-384x384.png",
+            "sizes": "384x384",
+            "type": "image/png"
+        }
+    ],
+    "theme_color": "#ffffff",
+    "background_color": "#ffffff",
+    "display": "standalone"
+}
blob - /dev/null
blob + b48c564d711082196e6cfe4e6a4bdb88ac4052fc (mode 644)
--- /dev/null
+++ gotweb/gotweb.c
@@ -0,0 +1,3005 @@
+/*
+ * Copyright (c) 2019, 2020 Tracey Emery <tracey@traceyemery.net>
+ * Copyright (c) 2018, 2019 Stefan Sperling <stsp@openbsd.org>
+ *
+ * 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 <sys/queue.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#include <dirent.h>
+#include <err.h>
+#include <regex.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <got_object.h>
+#include <got_reference.h>
+#include <got_repository.h>
+#include <got_path.h>
+#include <got_cancel.h>
+#include <got_worktree.h>
+#include <got_diff.h>
+#include <got_commit_graph.h>
+#include <got_blame.h>
+#include <got_privsep.h>
+#include <got_opentemp.h>
+
+#include <kcgi.h>
+#include <kcgihtml.h>
+
+#include "buf.h"
+#include "gotweb.h"
+#include "gotweb_ui.h"
+
+#ifndef nitems
+#define nitems(_a)	(sizeof((_a)) / sizeof((_a)[0]))
+#endif
+
+struct trans {
+	TAILQ_HEAD(dirs, gw_dir) gw_dirs;
+	struct gw_dir		*gw_dir;
+	struct gotweb_conf	*gw_conf;
+	struct ktemplate	*gw_tmpl;
+	struct khtmlreq		*gw_html_req;
+	struct kreq		*gw_req;
+	char			*repo_name;
+	char			*repo_path;
+	char			*commit;
+	char			*repo_file;
+	char			*repo_folder;
+	char			*action_name;
+	char			*headref;
+	unsigned int		 action;
+	unsigned int		 page;
+	unsigned int		 repos_total;
+	enum kmime		 mime;
+};
+
+enum gw_key {
+	KEY_ACTION,
+	KEY_COMMIT_ID,
+	KEY_FILE,
+	KEY_FOLDER,
+	KEY_HEADREF,
+	KEY_PAGE,
+	KEY_PATH,
+	KEY__ZMAX
+};
+
+struct gw_dir {
+	TAILQ_ENTRY(gw_dir)	 entry;
+	char			*name;
+	char			*owner;
+	char			*description;
+	char			*url;
+	char			*age;
+	char			*path;
+};
+
+enum tmpl {
+	TEMPL_HEAD,
+	TEMPL_HEADER,
+	TEMPL_SITEPATH,
+	TEMPL_SITEOWNER,
+	TEMPL_TITLE,
+	TEMPL_SEARCH,
+	TEMPL_CONTENT,
+	TEMPL__MAX
+};
+
+enum ref_tm {
+	TM_DIFF,
+	TM_LONG,
+};
+
+enum logs {
+	LOGBRIEF,
+	LOGCOMMIT,
+	LOGFULL,
+	LOGTREE,
+	LOGDIFF,
+	LOGBLAME,
+	LOGTAG,
+};
+
+enum tags {
+	TAGBRIEF,
+	TAGFULL,
+};
+
+struct buf {
+	u_char	*cb_buf;
+	size_t	 cb_size;
+	size_t	 cb_len;
+};
+
+static const char *const templs[TEMPL__MAX] = {
+	"head",
+	"header",
+	"sitepath",
+	"siteowner",
+	"title",
+	"search",
+	"content",
+};
+
+static const struct kvalid gw_keys[KEY__ZMAX] = {
+	{ kvalid_stringne,	"action" },
+	{ kvalid_stringne,	"commit" },
+	{ kvalid_stringne,	"file" },
+	{ kvalid_stringne,	"folder" },
+	{ kvalid_stringne,	"headref" },
+	{ kvalid_int,		"page" },
+	{ kvalid_stringne,	"path" },
+};
+
+int				 gw_get_repo_log_count(struct trans *, char *);
+
+static struct gw_dir		*gw_init_gw_dir(char *);
+
+static char			*gw_get_repo_description(struct trans *,
+				    char *);
+static char			*gw_get_repo_owner(struct trans *,
+				    char *);
+static char			*gw_get_time_str(time_t, int);
+static char			*gw_get_repo_age(struct trans *,
+				    char *, char *, int);
+static char			*gw_get_repo_log(struct trans *, const char *,
+				    char *, int, int);
+static char			*gw_get_file_blame(struct trans *, char *);
+static char			*gw_get_repo_tree(struct trans *, char *);
+static char			*gw_get_repo_diff(struct trans *, char *,
+				    char *);
+static char			*gw_get_repo_tags(struct trans *, int, int);
+static char			*gw_get_repo_heads(struct trans *);
+static char			*gw_get_clone_url(struct trans *, char *);
+static char			*gw_get_got_link(struct trans *);
+static char			*gw_get_site_link(struct trans *);
+static char			*gw_html_escape(const char *);
+static char			*color_diff_line(char *);
+
+static void			 gw_display_open(struct trans *, enum khttp,
+				    enum kmime);
+static void			 gw_display_index(struct trans *,
+				    const struct got_error *);
+
+static int			 gw_template(size_t, void *);
+
+static const struct got_error*	 apply_unveil(const char *, const char *);
+static const struct got_error*	 cmp_tags(void *, int *,
+				    struct got_reference *,
+				    struct got_reference *);
+static const struct got_error*	resolve_commit_arg(struct got_object_id **,
+				    const char *, struct got_repository *);
+static const struct got_error*	match_object_id(struct got_object_id **,
+				    char **, const char *r, int, int,
+				    struct got_repository *);
+static const struct got_error*	 blame_cb(void *, int, int,
+				    struct got_object_id *);
+static const struct got_error*	 gw_load_got_paths(struct trans *);
+static const struct got_error*	 gw_load_got_path(struct trans *,
+				    struct gw_dir *);
+static const struct got_error*	 gw_parse_querystring(struct trans *);
+static const struct got_error*	 match_logmsg(int *, struct got_object_id *,
+				    struct got_commit_object *, regex_t *);
+
+static const struct got_error*	 gw_blame(struct trans *);
+static const struct got_error*	 gw_commit(struct trans *);
+static const struct got_error*	 gw_commitdiff(struct trans *);
+static const struct got_error*	 gw_index(struct trans *);
+static const struct got_error*	 gw_log(struct trans *);
+static const struct got_error*	 gw_raw(struct trans *);
+static const struct got_error*	 gw_logbriefs(struct trans *);
+static const struct got_error*	 gw_summary(struct trans *);
+static const struct got_error*	 gw_tag(struct trans *);
+static const struct got_error*	 gw_tree(struct trans *);
+
+struct gw_query_action {
+	unsigned int		 func_id;
+	const char		*func_name;
+	const struct got_error	*(*func_main)(struct trans *);
+	char			*template;
+};
+
+enum gw_query_actions {
+	GW_BLAME,
+	GW_COMMIT,
+	GW_COMMITDIFF,
+	GW_ERR,
+	GW_INDEX,
+	GW_LOG,
+	GW_RAW,
+	GW_LOGBRIEFS,
+	GW_SUMMARY,
+	GW_TAG,
+	GW_TREE,
+};
+
+static struct gw_query_action gw_query_funcs[] = {
+	{ GW_BLAME,	 "blame",	gw_blame,	"gw_tmpl/index.tmpl" },
+	{ GW_COMMIT,	 "commit",	gw_commit,	"gw_tmpl/index.tmpl" },
+	{ GW_COMMITDIFF, "commitdiff",	gw_commitdiff,	"gw_tmpl/index.tmpl" },
+	{ GW_ERR,	 NULL,		NULL,		"gw_tmpl/index.tmpl" },
+	{ GW_INDEX,	 "index",	gw_index,	"gw_tmpl/index.tmpl" },
+	{ GW_LOG,	 "log",		gw_log,		"gw_tmpl/index.tmpl" },
+	{ GW_RAW,	 "raw",		gw_raw,		"gw_tmpl/index.tmpl" },
+	{ GW_LOGBRIEFS,	 "logbriefs",	gw_logbriefs,	"gw_tmpl/index.tmpl" },
+	{ GW_SUMMARY,	 "summary",	gw_summary,	"gw_tmpl/index.tmpl" },
+	{ GW_TAG,	 "tag",		gw_tag,		"gw_tmpl/index.tmpl" },
+	{ GW_TREE,	 "tree",	gw_tree,	"gw_tmpl/index.tmpl" },
+};
+
+static const struct got_error *
+apply_unveil(const char *repo_path, const char *repo_file)
+{
+	const struct got_error *err;
+
+	if (repo_path && repo_file) {
+		char *full_path;
+		if ((asprintf(&full_path, "%s/%s", repo_path, repo_file)) == -1)
+			return got_error_from_errno("asprintf unveil");
+		if (unveil(full_path, "r") != 0)
+			return got_error_from_errno2("unveil", full_path);
+	}
+
+	if (repo_path && unveil(repo_path, "r") != 0)
+		return got_error_from_errno2("unveil", repo_path);
+
+	if (unveil("/tmp", "rwc") != 0)
+		return got_error_from_errno2("unveil", "/tmp");
+
+	err = got_privsep_unveil_exec_helpers();
+	if (err != NULL)
+		return err;
+
+	if (unveil(NULL, NULL) != 0)
+		return got_error_from_errno("unveil");
+
+	return NULL;
+}
+
+static const struct got_error *
+cmp_tags(void *arg, int *cmp, struct got_reference *ref1,
+    struct got_reference *ref2)
+{
+	const struct got_error *err = NULL;
+	struct got_repository *repo = arg;
+	struct got_object_id *id1, *id2 = NULL;
+	struct got_tag_object *tag1 = NULL, *tag2 = NULL;
+	time_t time1, time2;
+
+	*cmp = 0;
+
+	err = got_ref_resolve(&id1, repo, ref1);
+	if (err)
+		return err;
+	err = got_object_open_as_tag(&tag1, repo, id1);
+	if (err)
+		goto done;
+
+	err = got_ref_resolve(&id2, repo, ref2);
+	if (err)
+		goto done;
+	err = got_object_open_as_tag(&tag2, repo, id2);
+	if (err)
+		goto done;
+
+	time1 = got_object_tag_get_tagger_time(tag1);
+	time2 = got_object_tag_get_tagger_time(tag2);
+
+	/* Put latest tags first. */
+	if (time1 < time2)
+		*cmp = 1;
+	else if (time1 > time2)
+		*cmp = -1;
+	else
+		err = got_ref_cmp_by_name(NULL, cmp, ref2, ref1);
+done:
+	free(id1);
+	free(id2);
+	if (tag1)
+		got_object_tag_close(tag1);
+	if (tag2)
+		got_object_tag_close(tag2);
+	return err;
+}
+
+static const struct got_error *
+resolve_commit_arg(struct got_object_id **commit_id,
+    const char *commit_id_arg, struct got_repository *repo)
+{
+	const struct got_error *err;
+	struct got_reference *ref;
+	struct got_tag_object *tag;
+
+	err = got_repo_object_match_tag(&tag, commit_id_arg,
+	    GOT_OBJ_TYPE_COMMIT, repo);
+	if (err == NULL) {
+		*commit_id = got_object_id_dup(
+		    got_object_tag_get_object_id(tag));
+		if (*commit_id == NULL)
+			err = got_error_from_errno("got_object_id_dup");
+		got_object_tag_close(tag);
+		return err;
+	} else if (err->code != GOT_ERR_NO_OBJ)
+		return err;
+
+	err = got_ref_open(&ref, repo, commit_id_arg, 0);
+	if (err == NULL) {
+		err = got_ref_resolve(commit_id, repo, ref);
+		got_ref_close(ref);
+	} else {
+		if (err->code != GOT_ERR_NOT_REF)
+			return err;
+		err = got_repo_match_object_id_prefix(commit_id,
+		    commit_id_arg, GOT_OBJ_TYPE_COMMIT, repo);
+	}
+	return err;
+}
+
+static const struct got_error *
+match_object_id(struct got_object_id **id, char **label,
+    const char *id_str, int obj_type, int resolve_tags,
+    struct got_repository *repo)
+{
+	const struct got_error *err;
+	struct got_tag_object *tag;
+	struct got_reference *ref = NULL;
+
+	*id = NULL;
+	*label = NULL;
+
+	if (resolve_tags) {
+		err = got_repo_object_match_tag(&tag, id_str, GOT_OBJ_TYPE_ANY,
+		    repo);
+		if (err == NULL) {
+			*id = got_object_id_dup(
+			    got_object_tag_get_object_id(tag));
+			if (*id == NULL)
+				err = got_error_from_errno("got_object_id_dup");
+			else if (asprintf(label, "refs/tags/%s",
+			    got_object_tag_get_name(tag)) == -1) {
+				err = got_error_from_errno("asprintf");
+				free(*id);
+				*id = NULL;
+			}
+			got_object_tag_close(tag);
+			return err;
+		} else if (err->code != GOT_ERR_NO_OBJ)
+			return err;
+	}
+
+	err = got_repo_match_object_id_prefix(id, id_str, obj_type, repo);
+	if (err) {
+		if (err->code != GOT_ERR_BAD_OBJ_ID_STR)
+			return err;
+		err = got_ref_open(&ref, repo, id_str, 0);
+		if (err != NULL)
+			goto done;
+		*label = strdup(got_ref_get_name(ref));
+		if (*label == NULL) {
+			err = got_error_from_errno("strdup");
+			goto done;
+		}
+		err = got_ref_resolve(id, repo, ref);
+	} else {
+		err = got_object_id_str(label, *id);
+		if (*label == NULL) {
+			err = got_error_from_errno("strdup");
+			goto done;
+		}
+	}
+done:
+	if (ref)
+		got_ref_close(ref);
+	return err;
+}
+
+int
+gw_get_repo_log_count(struct trans *gw_trans, char *start_commit)
+{
+	const struct got_error *error;
+	struct got_repository *repo = NULL;
+	struct got_reflist_head refs;
+	struct got_commit_object *commit = NULL;
+	struct got_object_id *id = NULL;
+	struct got_commit_graph *graph = NULL;
+	char *in_repo_path = NULL, *path = NULL;
+	int log_count = 0;
+
+	error = got_repo_open(&repo, gw_trans->repo_path, NULL);
+	if (error)
+		return 0;
+
+	SIMPLEQ_INIT(&refs);
+
+	if (start_commit == NULL) {
+		struct got_reference *head_ref;
+		error = got_ref_open(&head_ref, repo, gw_trans->headref, 0);
+		if (error)
+			goto done;
+
+		error = got_ref_resolve(&id, repo, head_ref);
+		got_ref_close(head_ref);
+		if (error)
+			goto done;
+
+		error = got_object_open_as_commit(&commit, repo, id);
+	} else {
+		struct got_reference *ref;
+		error = got_ref_open(&ref, repo, start_commit, 0);
+		if (error == NULL) {
+			int obj_type;
+			error = got_ref_resolve(&id, repo, ref);
+			got_ref_close(ref);
+			if (error)
+				goto done;
+			error = got_object_get_type(&obj_type, repo, id);
+			if (error)
+				goto done;
+			if (obj_type == GOT_OBJ_TYPE_TAG) {
+				struct got_tag_object *tag;
+				error = got_object_open_as_tag(&tag, repo, id);
+				if (error)
+					goto done;
+				if (got_object_tag_get_object_type(tag) !=
+				    GOT_OBJ_TYPE_COMMIT) {
+					got_object_tag_close(tag);
+					error = got_error(GOT_ERR_OBJ_TYPE);
+					goto done;
+				}
+				free(id);
+				id = got_object_id_dup(
+				    got_object_tag_get_object_id(tag));
+				if (id == NULL)
+					error = got_error_from_errno(
+					    "got_object_id_dup");
+				got_object_tag_close(tag);
+				if (error)
+					goto done;
+			} else if (obj_type != GOT_OBJ_TYPE_COMMIT) {
+				error = got_error(GOT_ERR_OBJ_TYPE);
+				goto done;
+			}
+			error = got_object_open_as_commit(&commit, repo, id);
+			if (error)
+				goto done;
+		}
+		if (commit == NULL) {
+			error = got_repo_match_object_id_prefix(&id,
+			    start_commit, GOT_OBJ_TYPE_COMMIT, repo);
+			if (error)
+				goto done;
+		}
+		error = got_repo_match_object_id_prefix(&id,
+			    start_commit, GOT_OBJ_TYPE_COMMIT, repo);
+			if (error)
+				goto done;
+	}
+
+	error = got_object_open_as_commit(&commit, repo, id);
+	if (error)
+		goto done;
+
+	error = got_repo_map_path(&in_repo_path, repo, gw_trans->repo_path, 1);
+	if (error)
+		goto done;
+
+	if (in_repo_path) {
+		free(path);
+		path = in_repo_path;
+	}
+
+	error = got_ref_list(&refs, repo, NULL, got_ref_cmp_by_name, NULL);
+	if (error)
+		goto done;
+
+	error = got_commit_graph_open(&graph, path, 0);
+	if (error)
+		goto done;
+
+	error = got_commit_graph_iter_start(graph, id, repo, NULL, NULL);
+	if (error)
+		goto done;
+
+	for (;;) {
+		error = got_commit_graph_iter_next(&id, graph, repo, NULL,
+		    NULL);
+		if (error) {
+			if (error->code == GOT_ERR_ITER_COMPLETED)
+				error = NULL;
+			break;
+		}
+		if (id == NULL)
+			break;
+
+		if (error)
+			break;
+		log_count++;
+	}
+done:
+	free(in_repo_path);
+	if (graph)
+		got_commit_graph_close(graph);
+	if (repo) {
+		error = got_repo_close(repo);
+		if (error)
+			return 0;
+	}
+	if (error) {
+		khttp_puts(gw_trans->gw_req, "Error: ");
+		khttp_puts(gw_trans->gw_req, error->msg);
+		return 0;
+	} else
+		return log_count;
+}
+
+static const struct got_error *
+gw_blame(struct trans *gw_trans)
+{
+	const struct got_error *error = NULL;
+
+	char *log, *log_html;
+
+	error = apply_unveil(gw_trans->gw_dir->path, NULL);
+	if (error)
+		return error;
+
+	log = gw_get_repo_log(gw_trans, NULL, gw_trans->commit, 1, LOGBLAME);
+
+	if (log != NULL && strcmp(log, "") != 0) {
+		if ((asprintf(&log_html, log_blame, log)) == -1)
+			return got_error_from_errno("asprintf");
+		khttp_puts(gw_trans->gw_req, log_html);
+		free(log_html);
+		free(log);
+	}
+	return error;
+}
+
+static const struct got_error *
+gw_commit(struct trans *gw_trans)
+{
+	const struct got_error *error = NULL;
+	char *log, *log_html;
+
+	error = apply_unveil(gw_trans->gw_dir->path, NULL);
+	if (error)
+		return error;
+
+	log = gw_get_repo_log(gw_trans, NULL, gw_trans->commit, 1, LOGCOMMIT);
+
+	if (log != NULL && strcmp(log, "") != 0) {
+		if ((asprintf(&log_html, log_commit, log)) == -1)
+			return got_error_from_errno("asprintf");
+		khttp_puts(gw_trans->gw_req, log_html);
+		free(log_html);
+		free(log);
+	}
+	return error;
+}
+
+static const struct got_error *
+gw_commitdiff(struct trans *gw_trans)
+{
+	const struct got_error *error = NULL;
+	char *log, *log_html;
+
+	error = apply_unveil(gw_trans->gw_dir->path, NULL);
+	if (error)
+		return error;
+
+	log = gw_get_repo_log(gw_trans, NULL, gw_trans->commit, 1, LOGDIFF);
+
+	if (log != NULL && strcmp(log, "") != 0) {
+		if ((asprintf(&log_html, log_diff, log)) == -1)
+			return got_error_from_errno("asprintf");
+		khttp_puts(gw_trans->gw_req, log_html);
+		free(log_html);
+		free(log);
+	}
+	return error;
+}
+
+static const struct got_error *
+gw_index(struct trans *gw_trans)
+{
+	const struct got_error *error = NULL;
+	struct gw_dir *gw_dir = NULL;
+	char *html, *navs, *next, *prev;
+	unsigned int prev_disp = 0, next_disp = 1, dir_c = 0;
+
+	error = apply_unveil(gw_trans->gw_conf->got_repos_path, NULL);
+	if (error)
+		return error;
+
+	error = gw_load_got_paths(gw_trans);
+	if (error)
+		return error;
+
+	khttp_puts(gw_trans->gw_req, index_projects_header);
+
+	TAILQ_FOREACH(gw_dir, &gw_trans->gw_dirs, entry)
+		dir_c++;
+
+	TAILQ_FOREACH(gw_dir, &gw_trans->gw_dirs, entry) {
+		if (gw_trans->page > 0 && (gw_trans->page *
+		    gw_trans->gw_conf->got_max_repos_display) > prev_disp) {
+			prev_disp++;
+			continue;
+		}
+
+		prev_disp++;
+		if((asprintf(&navs, index_navs, gw_dir->name, gw_dir->name,
+		    gw_dir->name, gw_dir->name)) == -1)
+			return got_error_from_errno("asprintf");
+
+		if ((asprintf(&html, index_projects, gw_dir->name, gw_dir->name,
+		    gw_dir->description, gw_dir->owner, gw_dir->age,
+		    navs)) == -1)
+			return got_error_from_errno("asprintf");
+
+		khttp_puts(gw_trans->gw_req, html);
+
+		free(navs);
+		free(html);
+
+		if (gw_trans->gw_conf->got_max_repos_display == 0)
+			continue;
+
+		if (next_disp == gw_trans->gw_conf->got_max_repos_display)
+			khttp_puts(gw_trans->gw_req, np_wrapper_start);
+		else if ((gw_trans->gw_conf->got_max_repos_display > 0) &&
+		    (gw_trans->page > 0) &&
+		    (next_disp == gw_trans->gw_conf->got_max_repos_display ||
+		    prev_disp == gw_trans->repos_total))
+			khttp_puts(gw_trans->gw_req, np_wrapper_start);
+
+		if ((gw_trans->gw_conf->got_max_repos_display > 0) &&
+		    (gw_trans->page > 0) &&
+		    (next_disp == gw_trans->gw_conf->got_max_repos_display ||
+		    prev_disp == gw_trans->repos_total)) {
+			if ((asprintf(&prev, nav_prev,
+			    gw_trans->page - 1)) == -1)
+				return got_error_from_errno("asprintf");
+			khttp_puts(gw_trans->gw_req, prev);
+			free(prev);
+		}
+
+		khttp_puts(gw_trans->gw_req, div_end);
+
+		if (gw_trans->gw_conf->got_max_repos_display > 0 &&
+		    next_disp == gw_trans->gw_conf->got_max_repos_display &&
+		    dir_c != (gw_trans->page + 1) *
+		    gw_trans->gw_conf->got_max_repos_display) {
+			if ((asprintf(&next, nav_next,
+			    gw_trans->page + 1)) == -1)
+				return got_error_from_errno("calloc");
+			khttp_puts(gw_trans->gw_req, next);
+			khttp_puts(gw_trans->gw_req, div_end);
+			free(next);
+			next_disp = 0;
+			break;
+		}
+
+		if ((gw_trans->gw_conf->got_max_repos_display > 0) &&
+		    (gw_trans->page > 0) &&
+		    (next_disp == gw_trans->gw_conf->got_max_repos_display ||
+		    prev_disp == gw_trans->repos_total))
+			khttp_puts(gw_trans->gw_req, div_end);
+
+		next_disp++;
+	}
+	return error;
+}
+
+static const struct got_error *
+gw_log(struct trans *gw_trans)
+{
+	const struct got_error *error = NULL;
+	char *log, *log_html;
+
+	error = apply_unveil(gw_trans->gw_dir->path, NULL);
+	if (error)
+		return error;
+
+	log = gw_get_repo_log(gw_trans, NULL, gw_trans->commit,
+	    gw_trans->gw_conf->got_max_commits_display, LOGFULL);
+
+	if (log != NULL && strcmp(log, "") != 0) {
+		if ((asprintf(&log_html, logs, log)) == -1)
+			return got_error_from_errno("asprintf");
+		khttp_puts(gw_trans->gw_req, log_html);
+		free(log_html);
+		free(log);
+	}
+	return error;
+}
+
+static const struct got_error *
+gw_raw(struct trans *gw_trans)
+{
+	const struct got_error *error = NULL;
+
+	return error;
+}
+
+static const struct got_error *
+gw_logbriefs(struct trans *gw_trans)
+{
+	const struct got_error *error = NULL;
+	char *log, *log_html;
+
+	error = apply_unveil(gw_trans->gw_dir->path, NULL);
+	if (error)
+		return error;
+
+	log = gw_get_repo_log(gw_trans, NULL, gw_trans->commit,
+	    gw_trans->gw_conf->got_max_commits_display, LOGBRIEF);
+
+	if (log != NULL && strcmp(log, "") != 0) {
+		if ((asprintf(&log_html, summary_logbriefs,
+		    log)) == -1)
+			return got_error_from_errno("asprintf");
+		khttp_puts(gw_trans->gw_req, log_html);
+		free(log_html);
+		free(log);
+	}
+	return error;
+}
+
+static const struct got_error *
+gw_summary(struct trans *gw_trans)
+{
+	const struct got_error *error = NULL;
+	char *description_html, *repo_owner_html, *repo_age_html,
+	     *cloneurl_html, *log, *log_html, *tags, *heads, *tags_html,
+	     *heads_html, *age;
+
+	error = apply_unveil(gw_trans->gw_dir->path, NULL);
+	if (error)
+		return error;
+
+	khttp_puts(gw_trans->gw_req, summary_wrapper);
+	if (gw_trans->gw_conf->got_show_repo_description) {
+		if (gw_trans->gw_dir->description != NULL &&
+		    (strcmp(gw_trans->gw_dir->description, "") != 0)) {
+			if ((asprintf(&description_html, description,
+			    gw_trans->gw_dir->description)) == -1)
+				return got_error_from_errno("asprintf");
+
+			khttp_puts(gw_trans->gw_req, description_html);
+			free(description_html);
+		}
+	}
+
+	if (gw_trans->gw_conf->got_show_repo_owner) {
+		if (gw_trans->gw_dir->owner != NULL &&
+		    (strcmp(gw_trans->gw_dir->owner, "") != 0)) {
+			if ((asprintf(&repo_owner_html, repo_owner,
+			    gw_trans->gw_dir->owner)) == -1)
+				return got_error_from_errno("asprintf");
+
+			khttp_puts(gw_trans->gw_req, repo_owner_html);
+			free(repo_owner_html);
+		}
+	}
+
+	if (gw_trans->gw_conf->got_show_repo_age) {
+		age = gw_get_repo_age(gw_trans, gw_trans->gw_dir->path,
+		    "refs/heads", TM_LONG);
+		if (age != NULL && (strcmp(age, "") != 0)) {
+			if ((asprintf(&repo_age_html, last_change, age)) == -1)
+				return got_error_from_errno("asprintf");
+
+			khttp_puts(gw_trans->gw_req, repo_age_html);
+			free(repo_age_html);
+			free(age);
+		}
+	}
+
+	if (gw_trans->gw_conf->got_show_repo_cloneurl) {
+		if (gw_trans->gw_dir->url != NULL &&
+		    (strcmp(gw_trans->gw_dir->url, "") != 0)) {
+			if ((asprintf(&cloneurl_html, cloneurl,
+			    gw_trans->gw_dir->url)) == -1)
+				return got_error_from_errno("asprintf");
+
+			khttp_puts(gw_trans->gw_req, cloneurl_html);
+			free(cloneurl_html);
+		}
+	}
+	khttp_puts(gw_trans->gw_req, div_end);
+
+	log = gw_get_repo_log(gw_trans, NULL, NULL, D_MAXSLCOMMDISP, 0);
+	tags = gw_get_repo_tags(gw_trans, D_MAXSLCOMMDISP, TAGBRIEF);
+	heads = gw_get_repo_heads(gw_trans);
+
+	if (log != NULL && strcmp(log, "") != 0) {
+		if ((asprintf(&log_html, summary_logbriefs,
+		    log)) == -1)
+			return got_error_from_errno("asprintf");
+		khttp_puts(gw_trans->gw_req, log_html);
+		free(log_html);
+		free(log);
+	}
+
+	if (tags != NULL && strcmp(tags, "") != 0) {
+		if ((asprintf(&tags_html, summary_tags,
+		    tags)) == -1)
+			return got_error_from_errno("asprintf");
+		khttp_puts(gw_trans->gw_req, tags_html);
+		free(tags_html);
+		free(tags);
+	}
+
+	if (heads != NULL && strcmp(heads, "") != 0) {
+		if ((asprintf(&heads_html, summary_heads,
+		    heads)) == -1)
+			return got_error_from_errno("asprintf");
+		khttp_puts(gw_trans->gw_req, heads_html);
+		free(heads_html);
+		free(heads);
+	}
+	return error;
+}
+
+static const struct got_error *
+gw_tag(struct trans *gw_trans)
+{
+	const struct got_error *error = NULL;
+	char *log, *log_html;
+
+	error = apply_unveil(gw_trans->gw_dir->path, NULL);
+	if (error)
+		return error;
+
+	log = gw_get_repo_log(gw_trans, NULL, gw_trans->commit, 1, LOGTAG);
+
+	if (log != NULL && strcmp(log, "") != 0) {
+		if ((asprintf(&log_html, log_tag, log)) == -1)
+			return got_error_from_errno("asprintf");
+		khttp_puts(gw_trans->gw_req, log_html);
+		free(log_html);
+		free(log);
+	}
+	return error;
+}
+
+static const struct got_error *
+gw_tree(struct trans *gw_trans)
+{
+	const struct got_error *error = NULL;
+	char *log, *log_html;
+
+	error = apply_unveil(gw_trans->gw_dir->path, NULL);
+	if (error)
+		return error;
+
+	log = gw_get_repo_log(gw_trans, NULL, gw_trans->commit, 1, LOGTREE);
+
+	if (log != NULL && strcmp(log, "") != 0) {
+		if ((asprintf(&log_html, log_tree, log)) == -1)
+			return got_error_from_errno("asprintf");
+		khttp_puts(gw_trans->gw_req, log_html);
+		free(log_html);
+		free(log);
+	}
+	return error;
+}
+
+static const struct got_error *
+gw_load_got_path(struct trans *gw_trans, struct gw_dir *gw_dir)
+{
+	const struct got_error *error = NULL;
+	DIR *dt;
+	char *dir_test;
+	int opened = 0;
+
+	if ((asprintf(&dir_test, "%s/%s/%s",
+	    gw_trans->gw_conf->got_repos_path, gw_dir->name,
+	    GOTWEB_GIT_DIR)) == -1)
+		return got_error_from_errno("asprintf");
+
+	dt = opendir(dir_test);
+	if (dt == NULL) {
+		free(dir_test);
+	} else {
+		gw_dir->path = strdup(dir_test);
+		opened = 1;
+		goto done;
+	}
+
+	if ((asprintf(&dir_test, "%s/%s/%s",
+	    gw_trans->gw_conf->got_repos_path, gw_dir->name,
+	    GOTWEB_GOT_DIR)) == -1)
+		return got_error_from_errno("asprintf");
+
+	dt = opendir(dir_test);
+	if (dt == NULL)
+		free(dir_test);
+	else {
+		opened = 1;
+		error = got_error(GOT_ERR_NOT_GIT_REPO);
+		goto errored;
+	}
+
+	if ((asprintf(&dir_test, "%s/%s",
+	    gw_trans->gw_conf->got_repos_path, gw_dir->name)) == -1)
+		return got_error_from_errno("asprintf");
+
+	gw_dir->path = strdup(dir_test);
+
+done:
+	gw_dir->description = gw_get_repo_description(gw_trans,
+	    gw_dir->path);
+	gw_dir->owner = gw_get_repo_owner(gw_trans, gw_dir->path);
+	gw_dir->age = gw_get_repo_age(gw_trans, gw_dir->path, "refs/heads",
+	    TM_DIFF);
+	gw_dir->url = gw_get_clone_url(gw_trans, gw_dir->path);
+
+errored:
+	free(dir_test);
+	if (opened)
+		closedir(dt);
+	return error;
+}
+
+static const struct got_error *
+gw_load_got_paths(struct trans *gw_trans)
+{
+	const struct got_error *error = NULL;
+	DIR *d;
+	struct dirent **sd_dent;
+	struct gw_dir *gw_dir;
+	struct stat st;
+	unsigned int d_cnt, d_i;
+
+	d = opendir(gw_trans->gw_conf->got_repos_path);
+	if (d == NULL) {
+		error = got_error_from_errno2("opendir",
+		    gw_trans->gw_conf->got_repos_path);
+		return error;
+	}
+
+	d_cnt = scandir(gw_trans->gw_conf->got_repos_path, &sd_dent, NULL,
+	    alphasort);
+	if (d_cnt == -1) {
+		error = got_error_from_errno2("scandir",
+		    gw_trans->gw_conf->got_repos_path);
+		return error;
+	}
+
+	for (d_i = 0; d_i < d_cnt; d_i++) {
+		if (gw_trans->gw_conf->got_max_repos > 0 &&
+		    (d_i - 2) == gw_trans->gw_conf->got_max_repos)
+			break; /* account for parent and self */
+
+		if (strcmp(sd_dent[d_i]->d_name, ".") == 0 ||
+		    strcmp(sd_dent[d_i]->d_name, "..") == 0)
+			continue;
+
+		if ((gw_dir = gw_init_gw_dir(sd_dent[d_i]->d_name)) == NULL)
+			return got_error_from_errno("gw_dir malloc");
+
+		error = gw_load_got_path(gw_trans, gw_dir);
+		if (error && error->code == GOT_ERR_NOT_GIT_REPO)
+			continue;
+		else if (error)
+			return error;
+
+		if (lstat(gw_dir->path, &st) == 0 && S_ISDIR(st.st_mode) &&
+		    !got_path_dir_is_empty(gw_dir->path)) {
+			TAILQ_INSERT_TAIL(&gw_trans->gw_dirs, gw_dir,
+			    entry);
+			gw_trans->repos_total++;
+		}
+	}
+
+	closedir(d);
+	return error;
+}
+
+static const struct got_error *
+gw_parse_querystring(struct trans *gw_trans)
+{
+	const struct got_error *error = NULL;
+	struct kpair *p;
+	struct gw_query_action *action = NULL;
+	unsigned int i;
+
+	if (gw_trans->gw_req->fieldnmap[0]) {
+		error = got_error_from_errno("bad parse");
+		return error;
+	} else if ((p = gw_trans->gw_req->fieldmap[KEY_PATH])) {
+		/* define gw_trans->repo_path */
+		if ((asprintf(&gw_trans->repo_name, "%s", p->parsed.s)) == -1)
+			return got_error_from_errno("asprintf");
+
+		if ((asprintf(&gw_trans->repo_path, "%s/%s",
+		    gw_trans->gw_conf->got_repos_path, p->parsed.s)) == -1)
+			return got_error_from_errno("asprintf");
+
+		/* get action and set function */
+		if ((p = gw_trans->gw_req->fieldmap[KEY_ACTION]))
+			for (i = 0; i < nitems(gw_query_funcs); i++) {
+				action = &gw_query_funcs[i];
+				if (action->func_name == NULL)
+					continue;
+
+				if (strcmp(action->func_name,
+				    p->parsed.s) == 0) {
+					gw_trans->action = i;
+					if ((asprintf(&gw_trans->action_name,
+					    "%s", action->func_name)) == -1)
+						return
+						    got_error_from_errno(
+						    "asprintf");
+
+					break;
+				}
+
+				action = NULL;
+			}
+
+ 		if ((p = gw_trans->gw_req->fieldmap[KEY_COMMIT_ID]))
+			if ((asprintf(&gw_trans->commit, "%s",
+			    p->parsed.s)) == -1)
+				return got_error_from_errno("asprintf");
+
+		if ((p = gw_trans->gw_req->fieldmap[KEY_FILE]))
+			if ((asprintf(&gw_trans->repo_file, "%s",
+			    p->parsed.s)) == -1)
+				return got_error_from_errno("asprintf");
+
+		if ((p = gw_trans->gw_req->fieldmap[KEY_FOLDER]))
+			if ((asprintf(&gw_trans->repo_folder, "%s",
+			    p->parsed.s)) == -1)
+				return got_error_from_errno("asprintf");
+
+		if ((p = gw_trans->gw_req->fieldmap[KEY_HEADREF]))
+			if ((asprintf(&gw_trans->headref, "%s",
+			    p->parsed.s)) == -1)
+				return got_error_from_errno("asprintf");
+
+		if (action == NULL) {
+			error = got_error_from_errno("invalid action");
+			return error;
+		}
+		if ((gw_trans->gw_dir =
+		    gw_init_gw_dir(gw_trans->repo_name)) == NULL)
+			return got_error_from_errno("gw_dir malloc");
+
+		error = gw_load_got_path(gw_trans, gw_trans->gw_dir);
+		if (error)
+			return error;
+	} else
+		gw_trans->action = GW_INDEX;
+
+	if ((p = gw_trans->gw_req->fieldmap[KEY_PAGE]))
+		gw_trans->page = p->parsed.i;
+
+	if (gw_trans->action == GW_RAW)
+		gw_trans->mime = KMIME_TEXT_PLAIN;
+
+	return error;
+}
+
+static struct gw_dir *
+gw_init_gw_dir(char *dir)
+{
+	struct gw_dir *gw_dir;
+
+	if ((gw_dir = malloc(sizeof(*gw_dir))) == NULL)
+		return NULL;
+
+	if ((asprintf(&gw_dir->name, "%s", dir)) == -1)
+		return NULL;
+
+	return gw_dir;
+}
+
+static const struct got_error*
+match_logmsg(int *have_match, struct got_object_id *id,
+    struct got_commit_object *commit, regex_t *regex)
+{
+	const struct got_error *err = NULL;
+	regmatch_t regmatch;
+	char *id_str = NULL, *logmsg = NULL;
+
+	*have_match = 0;
+
+	err = got_object_id_str(&id_str, id);
+	if (err)
+		return err;
+
+	err = got_object_commit_get_logmsg(&logmsg, commit);
+	if (err)
+		goto done;
+
+	if (regexec(regex, logmsg, 1, &regmatch, 0) == 0)
+		*have_match = 1;
+done:
+	free(id_str);
+	free(logmsg);
+	return err;
+}
+
+static void
+gw_display_open(struct trans *gw_trans, enum khttp code, enum kmime mime)
+{
+	khttp_head(gw_trans->gw_req, kresps[KRESP_ALLOW], "GET");
+	khttp_head(gw_trans->gw_req, kresps[KRESP_STATUS], "%s",
+	    khttps[code]);
+	khttp_head(gw_trans->gw_req, kresps[KRESP_CONTENT_TYPE], "%s",
+	    kmimetypes[mime]);
+	khttp_head(gw_trans->gw_req, "X-Content-Type-Options", "nosniff");
+	khttp_head(gw_trans->gw_req, "X-Frame-Options", "DENY");
+	khttp_head(gw_trans->gw_req, "X-XSS-Protection", "1; mode=block");
+	khttp_body(gw_trans->gw_req);
+}
+
+static void
+gw_display_index(struct trans *gw_trans, const struct got_error *err)
+{
+	gw_display_open(gw_trans, KHTTP_200, gw_trans->mime);
+	khtml_open(gw_trans->gw_html_req, gw_trans->gw_req, 0);
+
+	if (err)
+		khttp_puts(gw_trans->gw_req, err->msg);
+	else
+		khttp_template(gw_trans->gw_req, gw_trans->gw_tmpl,
+		    gw_query_funcs[gw_trans->action].template);
+
+	khtml_close(gw_trans->gw_html_req);
+}
+
+static int
+gw_template(size_t key, void *arg)
+{
+	const struct got_error *error = NULL;
+	struct trans *gw_trans = arg;
+	char *gw_got_link, *gw_site_link;
+	char *site_owner_name, *site_owner_name_h;
+
+	switch (key) {
+	case (TEMPL_HEAD):
+		khttp_puts(gw_trans->gw_req, head);
+		break;
+	case(TEMPL_HEADER):
+		gw_got_link = gw_get_got_link(gw_trans);
+		if (gw_got_link != NULL)
+			khttp_puts(gw_trans->gw_req, gw_got_link);
+
+		free(gw_got_link);
+		break;
+	case (TEMPL_SITEPATH):
+		gw_site_link = gw_get_site_link(gw_trans);
+		if (gw_site_link != NULL)
+			khttp_puts(gw_trans->gw_req, gw_site_link);
+
+		free(gw_site_link);
+		break;
+	case(TEMPL_TITLE):
+		if (gw_trans->gw_conf->got_site_name != NULL)
+			khtml_puts(gw_trans->gw_html_req,
+			    gw_trans->gw_conf->got_site_name);
+
+		break;
+	case (TEMPL_SEARCH):
+		khttp_puts(gw_trans->gw_req, search);
+		break;
+	case(TEMPL_SITEOWNER):
+		if (gw_trans->gw_conf->got_site_owner != NULL &&
+		    gw_trans->gw_conf->got_show_site_owner) {
+			site_owner_name =
+			    gw_html_escape(gw_trans->gw_conf->got_site_owner);
+			if ((asprintf(&site_owner_name_h, site_owner,
+			    site_owner_name))
+			    == -1)
+				return 0;
+
+			khttp_puts(gw_trans->gw_req, site_owner_name_h);
+			free(site_owner_name);
+			free(site_owner_name_h);
+		}
+		break;
+	case(TEMPL_CONTENT):
+		error = gw_query_funcs[gw_trans->action].func_main(gw_trans);
+		if (error)
+			khttp_puts(gw_trans->gw_req, error->msg);
+
+		break;
+	default:
+		return 0;
+		break;
+	}
+	return 1;
+}
+
+static char *
+gw_get_repo_description(struct trans *gw_trans, char *dir)
+{
+	FILE *f;
+	char *description = NULL, *d_file = NULL;
+	unsigned int len;
+
+	if (gw_trans->gw_conf->got_show_repo_description == false)
+		goto err;
+
+	if ((asprintf(&d_file, "%s/description", dir)) == -1)
+		goto err;
+
+	if ((f = fopen(d_file, "r")) == NULL)
+		goto err;
+
+	fseek(f, 0, SEEK_END);
+	len = ftell(f) + 1;
+	fseek(f, 0, SEEK_SET);
+	if ((description = calloc(len, sizeof(char *))) == NULL)
+		goto err;
+
+	fread(description, 1, len, f);
+	fclose(f);
+	free(d_file);
+	return description;
+err:
+	if ((asprintf(&description, "%s", "")) == -1)
+		return NULL;
+
+	return description;
+}
+
+static char *
+gw_get_time_str(time_t committer_time, int ref_tm)
+{
+	struct tm tm;
+	time_t diff_time;
+	char *years = "years ago", *months = "months ago";
+	char *weeks = "weeks ago", *days = "days ago", *hours = "hours ago";
+	char *minutes = "minutes ago", *seconds = "seconds ago";
+	char *now = "right now";
+	char *repo_age, *s;
+	char datebuf[29];
+
+	switch (ref_tm) {
+	case TM_DIFF:
+		diff_time = time(NULL) - committer_time;
+		if (diff_time > 60 * 60 * 24 * 365 * 2) {
+			if ((asprintf(&repo_age, "%lld %s",
+			    (diff_time / 60 / 60 / 24 / 365), years)) == -1)
+				return NULL;
+		} else if (diff_time > 60 * 60 * 24 * (365 / 12) * 2) {
+			if ((asprintf(&repo_age, "%lld %s",
+			    (diff_time / 60 / 60 / 24 / (365 / 12)),
+			    months)) == -1)
+				return NULL;
+		} else if (diff_time > 60 * 60 * 24 * 7 * 2) {
+			if ((asprintf(&repo_age, "%lld %s",
+			    (diff_time / 60 / 60 / 24 / 7), weeks)) == -1)
+				return NULL;
+		} else if (diff_time > 60 * 60 * 24 * 2) {
+			if ((asprintf(&repo_age, "%lld %s",
+			    (diff_time / 60 / 60 / 24), days)) == -1)
+				return NULL;
+		} else if (diff_time > 60 * 60 * 2) {
+			if ((asprintf(&repo_age, "%lld %s",
+			    (diff_time / 60 / 60), hours)) == -1)
+				return NULL;
+		} else if (diff_time > 60 * 2) {
+			if ((asprintf(&repo_age, "%lld %s", (diff_time / 60),
+			    minutes)) == -1)
+				return NULL;
+		} else if (diff_time > 2) {
+			if ((asprintf(&repo_age, "%lld %s", diff_time,
+			    seconds)) == -1)
+				return NULL;
+		} else {
+			if ((asprintf(&repo_age, "%s", now)) == -1)
+				return NULL;
+		}
+		break;
+	case TM_LONG:
+		if (gmtime_r(&committer_time, &tm) == NULL)
+			return NULL;
+
+		s = asctime_r(&tm, datebuf);
+		if (s == NULL)
+			return NULL;
+
+		if ((asprintf(&repo_age, "%s UTC", datebuf)) == -1)
+			return NULL;
+		break;
+	}
+	return repo_age;
+}
+
+static char *
+gw_get_repo_age(struct trans *gw_trans, char *dir, char *repo_ref, int ref_tm)
+{
+	const struct got_error *error = NULL;
+	struct got_object_id *id = NULL;
+	struct got_repository *repo = NULL;
+	struct got_commit_object *commit = NULL;
+	struct got_reflist_head refs;
+	struct got_reflist_entry *re;
+	struct got_reference *head_ref;
+	int is_head = 0;
+	time_t committer_time = 0, cmp_time = 0;
+	const char *refname;
+	char *repo_age = NULL;
+
+	if (repo_ref == NULL)
+		return NULL;
+
+	if (strncmp(repo_ref, "refs/heads/", 11) == 0)
+		is_head = 1;
+
+	SIMPLEQ_INIT(&refs);
+	if (gw_trans->gw_conf->got_show_repo_age == false) {
+		if ((asprintf(&repo_age, "")) == -1)
+			return NULL;
+		return repo_age;
+	}
+
+	error = got_repo_open(&repo, dir, NULL);
+	if (error)
+		goto err;
+
+	if (is_head)
+		error = got_ref_list(&refs, repo, "refs/heads",
+		    got_ref_cmp_by_name, NULL);
+	else
+		error = got_ref_list(&refs, repo, repo_ref,
+		    got_ref_cmp_by_name, NULL);
+	if (error)
+		goto err;
+
+	SIMPLEQ_FOREACH(re, &refs, entry) {
+		if (is_head)
+			refname = strdup(repo_ref);
+		else
+			refname = got_ref_get_name(re->ref);
+		error = got_ref_open(&head_ref, repo, refname, 0);
+		if (error)
+			goto err;
+
+		error = got_ref_resolve(&id, repo, head_ref);
+		got_ref_close(head_ref);
+		if (error)
+			goto err;
+
+		error = got_object_open_as_commit(&commit, repo, id);
+		if (error)
+			goto err;
+
+		committer_time =
+		    got_object_commit_get_committer_time(commit);
+
+		if (cmp_time < committer_time)
+			cmp_time = committer_time;
+	}
+
+	if (cmp_time != 0) {
+		committer_time = cmp_time;
+		repo_age = gw_get_time_str(committer_time, ref_tm);
+	} else
+		if ((asprintf(&repo_age, "")) == -1)
+			return NULL;
+	got_ref_list_free(&refs);
+	free(id);
+	return repo_age;
+err:
+	if ((asprintf(&repo_age, "%s", error->msg)) == -1)
+		return NULL;
+
+	return repo_age;
+}
+
+static char *
+gw_get_repo_diff(struct trans *gw_trans, char *id_str1, char *id_str2)
+{
+	const struct got_error *error;
+	FILE *f = NULL;
+	struct got_object_id *id1 = NULL, *id2 = NULL;
+	struct got_repository *repo = NULL;
+	struct buf *diffbuf = NULL;
+	char *label1 = NULL, *label2 = NULL, *diff_html = NULL, *buf = NULL,
+	     *buf_color = NULL;
+	int type1, type2;
+	size_t newsize;
+
+	f = got_opentemp();
+	if (f == NULL)
+		return NULL;
+
+	error = buf_alloc(&diffbuf, 0);
+	if (error)
+		return NULL;
+
+	error = got_repo_open(&repo, gw_trans->repo_path, NULL);
+	if (error)
+		goto done;
+
+	error = match_object_id(&id1, &label1, id_str1, GOT_OBJ_TYPE_ANY, 1,
+	    repo);
+	if (error)
+		goto done;
+
+	if (id_str2) {
+		error = match_object_id(&id2, &label2, id_str2,
+		    GOT_OBJ_TYPE_ANY, 1, repo);
+		if (error)
+			goto done;
+
+		error = got_object_get_type(&type2, repo, id2);
+		if (error)
+			goto done;
+	}
+
+	error = got_object_get_type(&type1, repo, id1);
+	if (error)
+		goto done;
+
+	if (id_str2 && type1 != type2) {
+		error = got_error(GOT_ERR_OBJ_TYPE);
+		goto done;
+	}
+
+	switch (type1) {
+	case GOT_OBJ_TYPE_BLOB:
+		error = got_diff_objects_as_blobs(id2, id1, NULL, NULL, 3, 0,
+		    repo, f);
+		break;
+	case GOT_OBJ_TYPE_TREE:
+		error = got_diff_objects_as_trees(id2, id1, "", "", 3, 0, repo,
+		    f);
+		break;
+	case GOT_OBJ_TYPE_COMMIT:
+		error = got_diff_objects_as_commits(id2, id1, 3, 0, repo, f);
+		break;
+	default:
+		error = got_error(GOT_ERR_OBJ_TYPE);
+	}
+
+	if ((buf = calloc(128, sizeof(char *))) == NULL)
+		goto done;
+
+	fseek(f, 0, SEEK_SET);
+
+	while ((fgets(buf, 128, f)) != NULL) {
+		buf_color = color_diff_line(buf);
+		error = buf_puts(&newsize, diffbuf, buf_color);
+		if (error)
+			return NULL;
+
+		error = buf_puts(&newsize, diffbuf, div_end);
+		if (error)
+			return NULL;
+	}
+
+	if (buf_len(diffbuf) > 0) {
+		error = buf_putc(diffbuf, '\0');
+		diff_html = strdup(buf_get(diffbuf));
+	}
+done:
+	fclose(f);
+	free(buf_color);
+	free(buf);
+	free(diffbuf);
+	free(label1);
+	free(label2);
+	free(id1);
+	free(id2);
+	if (repo)
+		got_repo_close(repo);
+
+	if (error)
+		return NULL;
+	else
+		return diff_html;
+}
+
+static char *
+gw_get_repo_owner(struct trans *gw_trans, char *dir)
+{
+	FILE *f;
+	char *owner = NULL, *d_file = NULL;
+	char *gotweb = "[gotweb]", *gitweb = "[gitweb]", *gw_owner = "owner";
+	char *comp, *pos, *buf;
+	unsigned int i;
+
+	if (gw_trans->gw_conf->got_show_repo_owner == false)
+		goto err;
+
+	if ((asprintf(&d_file, "%s/config", dir)) == -1)
+		goto err;
+
+	if ((f = fopen(d_file, "r")) == NULL)
+		goto err;
+
+	if ((buf = calloc(128, sizeof(char *))) == NULL)
+		goto err;
+
+	while ((fgets(buf, 128, f)) != NULL) {
+		if ((pos = strstr(buf, gotweb)) != NULL)
+			break;
+
+		if ((pos = strstr(buf, gitweb)) != NULL)
+			break;
+	}
+
+	if (pos == NULL)
+		goto err;
+
+	do {
+		fgets(buf, 128, f);
+	} while ((comp = strcasestr(buf, gw_owner)) == NULL);
+
+	if (comp == NULL)
+		goto err;
+
+	if (strncmp(gw_owner, comp, strlen(gw_owner)) != 0)
+		goto err;
+
+	for (i = 0; i < 2; i++) {
+		owner = strsep(&buf, "\"");
+	}
+
+	if (owner == NULL)
+		goto err;
+
+	fclose(f);
+	free(d_file);
+	return owner;
+err:
+	if ((asprintf(&owner, "%s", "")) == -1)
+		return NULL;
+
+	return owner;
+}
+
+static char *
+gw_get_clone_url(struct trans *gw_trans, char *dir)
+{
+	FILE *f;
+	char *url = NULL, *d_file = NULL;
+	unsigned int len;
+
+	if ((asprintf(&d_file, "%s/cloneurl", dir)) == -1)
+		return NULL;
+
+	if ((f = fopen(d_file, "r")) == NULL)
+		return NULL;
+
+	fseek(f, 0, SEEK_END);
+	len = ftell(f) + 1;
+	fseek(f, 0, SEEK_SET);
+
+	if ((url = calloc(len, sizeof(char *))) == NULL)
+		return NULL;
+
+	fread(url, 1, len, f);
+	fclose(f);
+	free(d_file);
+	return url;
+}
+
+static char *
+gw_get_repo_log(struct trans *gw_trans, const char *search_pattern,
+    char *start_commit, int limit, int log_type)
+{
+	const struct got_error *error;
+	struct got_repository *repo = NULL;
+	struct got_reflist_head refs;
+	struct got_reflist_entry *re;
+	struct got_commit_object *commit = NULL;
+	struct got_object_id *id1 = NULL, *id2 = NULL;
+	struct got_object_qid *parent_id;
+	struct got_commit_graph *graph = NULL;
+	char *logs = NULL, *id_str1 = NULL, *id_str2 = NULL, *path = NULL,
+	     *in_repo_path = NULL, *refs_str = NULL, *refs_str_disp = NULL,
+	     *treeid = NULL, *commit_row = NULL, *commit_commit = NULL,
+	     *commit_commit_disp = NULL, *commit_age_diff = NULL,
+	     *commit_age_diff_disp = NULL, *commit_age_long = NULL,
+	     *commit_age_long_disp = NULL, *commit_author = NULL,
+	     *commit_author_disp = NULL, *commit_committer = NULL,
+	     *commit_committer_disp = NULL, *commit_log = NULL,
+	     *commit_log_disp = NULL, *commit_parent = NULL,
+	     *commit_diff_disp = NULL, *logbriefs_navs_html = NULL,
+	     *log_tree_html = NULL, *log_commit_html = NULL,
+	     *log_diff_html = NULL, *commit_tree = NULL,
+	     *commit_tree_disp = NULL, *log_tag_html = NULL,
+	     *log_blame_html = NULL;
+	char *commit_log0, *newline;
+	regex_t regex;
+	int have_match, log_count = 0, has_parent = 1;
+	size_t newsize;
+	struct buf *diffbuf = NULL;
+	time_t committer_time;
+
+	if (gw_trans->action == GW_LOG || gw_trans->action == GW_LOGBRIEFS)
+		log_count = gw_get_repo_log_count(gw_trans, start_commit);
+
+	error = buf_alloc(&diffbuf, 0);
+	if (error)
+		return NULL;
+
+	if (search_pattern &&
+	    regcomp(&regex, search_pattern, REG_EXTENDED | REG_NOSUB |
+	    REG_NEWLINE))
+		return NULL;
+
+	error = got_repo_open(&repo, gw_trans->repo_path, NULL);
+	if (error)
+		return NULL;
+
+	SIMPLEQ_INIT(&refs);
+
+	if (start_commit == NULL) {
+		struct got_reference *head_ref;
+		error = got_ref_open(&head_ref, repo, gw_trans->headref, 0);
+		if (error)
+			goto done;
+
+		error = got_ref_resolve(&id1, repo, head_ref);
+		got_ref_close(head_ref);
+		if (error)
+			goto done;
+
+		error = got_object_open_as_commit(&commit, repo, id1);
+	} else {
+		struct got_reference *ref;
+		error = got_ref_open(&ref, repo, start_commit, 0);
+		if (error == NULL) {
+			int obj_type;
+			error = got_ref_resolve(&id1, repo, ref);
+			got_ref_close(ref);
+			if (error)
+				goto done;
+			error = got_object_get_type(&obj_type, repo, id1);
+			if (error)
+				goto done;
+			if (obj_type == GOT_OBJ_TYPE_TAG) {
+				struct got_tag_object *tag;
+				error = got_object_open_as_tag(&tag, repo, id1);
+				if (error)
+					goto done;
+				if (got_object_tag_get_object_type(tag) !=
+				    GOT_OBJ_TYPE_COMMIT) {
+					got_object_tag_close(tag);
+					error = got_error(GOT_ERR_OBJ_TYPE);
+					goto done;
+				}
+				free(id1);
+				id1 = got_object_id_dup(
+				    got_object_tag_get_object_id(tag));
+				if (id1 == NULL)
+					error = got_error_from_errno(
+					    "got_object_id_dup");
+				got_object_tag_close(tag);
+				if (error)
+					goto done;
+			} else if (obj_type != GOT_OBJ_TYPE_COMMIT) {
+				error = got_error(GOT_ERR_OBJ_TYPE);
+				goto done;
+			}
+			error = got_object_open_as_commit(&commit, repo, id1);
+			if (error)
+				goto done;
+		}
+		if (commit == NULL) {
+			error = got_repo_match_object_id_prefix(&id1,
+			    start_commit, GOT_OBJ_TYPE_COMMIT, repo);
+			if (error)
+				goto done;
+		}
+		error = got_repo_match_object_id_prefix(&id1,
+			    start_commit, GOT_OBJ_TYPE_COMMIT, repo);
+	}
+
+	if (error)
+		goto done;
+
+	error = got_repo_map_path(&in_repo_path, repo, gw_trans->repo_path, 1);
+	if (error)
+		goto done;
+
+	if (in_repo_path) {
+		free(path);
+		path = in_repo_path;
+	}
+
+	error = got_ref_list(&refs, repo, NULL, got_ref_cmp_by_name, NULL);
+	if (error)
+		goto done;
+
+	error = got_commit_graph_open(&graph, path, 0);
+	if (error)
+		goto done;
+
+	error = got_commit_graph_iter_start(graph, id1, repo, NULL, NULL);
+	if (error)
+		goto done;
+
+	for (;;) {
+		error = got_commit_graph_iter_next(&id1, graph, repo, NULL,
+		    NULL);
+		if (error) {
+			if (error->code == GOT_ERR_ITER_COMPLETED)
+				error = NULL;
+			break;
+		}
+		if (id1 == NULL)
+			break;
+
+		error = got_object_open_as_commit(&commit, repo, id1);
+		if (error)
+			break;
+
+		if (search_pattern) {
+			error = match_logmsg(&have_match, id1, commit,
+			    &regex);
+			if (error) {
+				got_object_commit_close(commit);
+				break;
+			}
+			if (have_match == 0) {
+				got_object_commit_close(commit);
+				continue;
+			}
+		}
+
+		SIMPLEQ_FOREACH(re, &refs, entry) {
+			char *s;
+			const char *name;
+			struct got_tag_object *tag = NULL;
+			int cmp;
+
+			name = got_ref_get_name(re->ref);
+			if (strcmp(name, GOT_REF_HEAD) == 0)
+				continue;
+			if (strncmp(name, "refs/", 5) == 0)
+				name += 5;
+			if (strncmp(name, "got/", 4) == 0)
+				continue;
+			if (strncmp(name, "heads/", 6) == 0)
+				name += 6;
+			if (strncmp(name, "remotes/", 8) == 0)
+				name += 8;
+			if (strncmp(name, "tags/", 5) == 0) {
+				error = got_object_open_as_tag(&tag, repo,
+				    re->id);
+				if (error) {
+					if (error->code != GOT_ERR_OBJ_TYPE)
+						continue;
+					/*
+					 * Ref points at something other
+					 * than a tag.
+					 */
+					error = NULL;
+					tag = NULL;
+				}
+			}
+			cmp = got_object_id_cmp(tag ?
+			    got_object_tag_get_object_id(tag) : re->id, id1);
+			if (tag)
+				got_object_tag_close(tag);
+			if (cmp != 0)
+				continue;
+			s = refs_str;
+			if ((asprintf(&refs_str, "%s%s%s", s ? s : "",
+			    s ? ", " : "", name)) == -1) {
+				error = got_error_from_errno("asprintf");
+				free(s);
+				goto done;
+			}
+			free(s);
+		}
+
+		if (refs_str == NULL)
+			refs_str_disp = strdup("");
+		else {
+			if ((asprintf(&refs_str_disp, "(%s)",
+			    refs_str)) == -1) {
+				error = got_error_from_errno("asprintf");
+				free(refs_str);
+				goto done;
+			}
+		}
+
+		error = got_object_id_str(&id_str1, id1);
+		if (error)
+			goto done;
+
+		error = got_object_id_str(&treeid,
+		    got_object_commit_get_tree_id(commit));
+		if (error)
+			goto done;
+
+		if (gw_trans->action == GW_COMMIT ||
+		    gw_trans->action == GW_COMMITDIFF) {
+			parent_id =
+			    SIMPLEQ_FIRST(
+			    got_object_commit_get_parent_ids(commit));
+			if (parent_id != NULL) {
+				id2 = got_object_id_dup(parent_id->id);
+				free (parent_id);
+				error = got_object_id_str(&id_str2, id2);
+				if (error)
+					goto done;
+				free(id2);
+			} else {
+				has_parent = 0;
+				id_str2 = strdup("/dev/null");
+			}
+		}
+
+		committer_time =
+		    got_object_commit_get_committer_time(commit);
+
+		if ((asprintf(&commit_parent, "%s", id_str2)) == -1) {
+			error = got_error_from_errno("asprintf");
+			goto done;
+		}
+
+		if ((asprintf(&commit_tree, "%s", treeid)) == -1) {
+			error = got_error_from_errno("asprintf");
+			goto done;
+		}
+
+		if ((asprintf(&commit_tree_disp, commit_tree_html,
+		    treeid)) == -1) {
+			error = got_error_from_errno("asprintf");
+			goto done;
+		}
+
+		if ((asprintf(&commit_diff_disp, commit_diff_html, id_str2,
+			id_str1)) == -1) {
+			error = got_error_from_errno("asprintf");
+			goto done;
+		}
+
+		if ((asprintf(&commit_commit, "%s", id_str1)) == -1) {
+			error = got_error_from_errno("asprintf");
+			goto done;
+		}
+
+		if ((asprintf(&commit_commit_disp, commit_commit_html,
+		    commit_commit, refs_str_disp)) == -1) {
+			error = got_error_from_errno("asprintf");
+			goto done;
+		}
+
+		if ((asprintf(&commit_age_long, "%s",
+		    gw_get_time_str(committer_time, TM_LONG))) == -1) {
+			error = got_error_from_errno("asprintf");
+			goto done;
+		}
+
+		if ((asprintf(&commit_age_long_disp, commit_age_html,
+		    commit_age_long)) == -1) {
+			error = got_error_from_errno("asprintf");
+			goto done;
+		}
+
+		if ((asprintf(&commit_age_diff, "%s",
+		    gw_get_time_str(committer_time, TM_DIFF))) == -1) {
+			error = got_error_from_errno("asprintf");
+			goto done;
+		}
+
+		if ((asprintf(&commit_age_diff_disp, commit_age_html,
+		    commit_age_diff)) == -1) {
+			error = got_error_from_errno("asprintf");
+			goto done;
+		}
+
+		if ((asprintf(&commit_author, "%s",
+		    got_object_commit_get_author(commit))) == -1) {
+			error = got_error_from_errno("asprintf");
+			goto done;
+		}
+
+		if ((asprintf(&commit_author_disp, commit_author_html,
+		    gw_html_escape(commit_author))) == -1) {
+			error = got_error_from_errno("asprintf");
+			goto done;
+		}
+
+		if ((asprintf(&commit_committer, "%s",
+		    got_object_commit_get_committer(commit))) == -1) {
+			error = got_error_from_errno("asprintf");
+			goto done;
+		}
+
+		if ((asprintf(&commit_committer_disp, commit_committer_html,
+		    gw_html_escape(commit_committer))) == -1) {
+			error = got_error_from_errno("asprintf");
+			goto done;
+		}
+
+		if (strcmp(commit_author, commit_committer) == 0) {
+			free(commit_committer_disp);
+			commit_committer_disp = strdup("");
+		}
+
+		error = got_object_commit_get_logmsg(&commit_log0, commit);
+		if (error)
+			goto done;
+
+		commit_log = commit_log0;
+		while (*commit_log == '\n')
+			commit_log++;
+
+		switch(log_type) {
+		case (LOGBRIEF):
+			newline = strchr(commit_log, '\n');
+			if (newline)
+				*newline = '\0';
+
+			if ((asprintf(&logbriefs_navs_html, logbriefs_navs,
+			    gw_trans->repo_name, id_str1, gw_trans->repo_name,
+			    id_str1, gw_trans->repo_name, id_str1,
+			    gw_trans->repo_name, id_str1)) == -1) {
+				error = got_error_from_errno("asprintf");
+				goto done;
+			}
+
+			if ((asprintf(&commit_row, logbriefs_row,
+			    commit_age_diff, commit_author, commit_log,
+			    logbriefs_navs_html)) == -1) {
+				error = got_error_from_errno("asprintf");
+				goto done;
+			}
+
+			free(logbriefs_navs_html);
+			logbriefs_navs_html = NULL;
+			break;
+		case (LOGFULL):
+			if ((asprintf(&logbriefs_navs_html, logbriefs_navs,
+			    gw_trans->repo_name, id_str1, gw_trans->repo_name,
+			    id_str1, gw_trans->repo_name, id_str1,
+			    gw_trans->repo_name, id_str1)) == -1) {
+				error = got_error_from_errno("asprintf");
+				goto done;
+			}
+
+			if ((asprintf(&commit_row, logs_row, commit_commit_disp,
+			    commit_author_disp, commit_committer_disp,
+			    commit_age_long_disp, gw_html_escape(commit_log),
+			    logbriefs_navs_html)) == -1) {
+				error = got_error_from_errno("asprintf");
+				goto done;
+			}
+
+			free(logbriefs_navs_html);
+			logbriefs_navs_html = NULL;
+			break;
+		case (LOGTAG):
+			log_tag_html = strdup("tag log here");
+
+			if ((asprintf(&commit_row, log_tag_row,
+			    gw_html_escape(commit_log), log_tag_html)) == -1) {
+				error = got_error_from_errno("asprintf");
+				goto done;
+			}
+
+			free(log_tag_html);
+			break;
+		case (LOGBLAME):
+			log_blame_html = gw_get_file_blame(gw_trans,
+			    start_commit);
+
+			if ((asprintf(&commit_row, log_blame_row,
+			    gw_html_escape(commit_log), log_blame_html)) == -1) {
+				error = got_error_from_errno("asprintf");
+				goto done;
+			}
+
+			free(log_blame_html);
+			break;
+		case (LOGTREE):
+			log_tree_html = gw_get_repo_tree(gw_trans,
+			    start_commit);
+
+			if ((asprintf(&commit_row, log_tree_row,
+			    gw_html_escape(commit_log), log_tree_html)) == -1) {
+				error = got_error_from_errno("asprintf");
+				goto done;
+			}
+
+			free(log_tree_html);
+			break;
+		case (LOGCOMMIT):
+			if ((asprintf(&commit_log_disp, commit_log_html,
+			    gw_html_escape(commit_log))) == -1) {
+				error = got_error_from_errno("asprintf");
+				goto done;
+			}
+
+			log_commit_html = strdup("commit here");
+
+			if ((asprintf(&commit_row, log_commit_row,
+			    commit_diff_disp, commit_commit_disp,
+			    commit_tree_disp, commit_author_disp,
+			    commit_committer_disp, commit_age_long_disp,
+			    commit_log_disp, log_commit_html)) == -1) {
+				error = got_error_from_errno("asprintf");
+				goto done;
+			}
+			free(commit_log_disp);
+			free(log_commit_html);
+
+			break;
+		case (LOGDIFF):
+			if ((asprintf(&commit_log_disp, commit_log_html,
+			    gw_html_escape(commit_log))) == -1) {
+				error = got_error_from_errno("asprintf");
+				goto done;
+			}
+
+			if (has_parent)
+				log_diff_html = gw_get_repo_diff(gw_trans,
+				    commit_commit, commit_parent);
+			else
+				log_diff_html = gw_get_repo_diff(gw_trans,
+				    commit_commit, NULL);
+
+			if ((asprintf(&commit_row, log_diff_row,
+			    commit_diff_disp, commit_commit_disp,
+			    commit_tree_disp, commit_author_disp,
+			    commit_committer_disp, commit_age_long_disp,
+			    commit_log_disp, log_diff_html)) == -1) {
+				error = got_error_from_errno("asprintf");
+				goto done;
+			}
+			free(commit_log_disp);
+			free(log_diff_html);
+
+			break;
+		default:
+			return NULL;
+		}
+
+		error = buf_puts(&newsize, diffbuf, commit_row);
+
+		free(commit_parent);
+		free(commit_diff_disp);
+		free(commit_tree_disp);
+		free(commit_age_diff);
+		free(commit_age_diff_disp);
+		free(commit_age_long);
+		free(commit_age_long_disp);
+		free(commit_author);
+		free(commit_author_disp);
+		free(commit_committer);
+		free(commit_committer_disp);
+		free(commit_log0);
+		free(commit_row);
+		free(refs_str_disp);
+		free(refs_str);
+		refs_str = NULL;
+		free(id_str1);
+		id_str1 = NULL;
+		free(id_str2);
+		id_str2 = NULL;
+
+		if (error || (limit && --limit == 0))
+			break;
+	}
+
+	if (error)
+		goto done;
+
+	if (buf_len(diffbuf) > 0) {
+		error = buf_putc(diffbuf, '\0');
+		logs = strdup(buf_get(diffbuf));
+	}
+done:
+	buf_free(diffbuf);
+	free(in_repo_path);
+	if (commit != NULL)
+		got_object_commit_close(commit);
+	if (search_pattern)
+		regfree(&regex);
+	if (graph)
+		got_commit_graph_close(graph);
+	if (repo) {
+		error = got_repo_close(repo);
+		if (error)
+			return NULL;
+	}
+	if (error) {
+		khttp_puts(gw_trans->gw_req, "Error: ");
+		khttp_puts(gw_trans->gw_req, error->msg);
+		return NULL;
+	} else
+		return logs;
+}
+
+static char *
+gw_get_repo_tags(struct trans *gw_trans, int limit, int tag_type)
+{
+	const struct got_error *error = NULL;
+	struct got_repository *repo = NULL;
+	struct got_reflist_head refs;
+	struct got_reflist_entry *re;
+	char *tags = NULL, *tag_row = NULL, *tags_navs_disp = NULL,
+	     *age = NULL;
+	char *newline;
+	struct buf *diffbuf = NULL;
+	size_t newsize;
+
+	error = buf_alloc(&diffbuf, 0);
+	if (error)
+		return NULL;
+	SIMPLEQ_INIT(&refs);
+
+	error = got_repo_open(&repo, gw_trans->repo_path, NULL);
+	if (error)
+		goto done;
+
+	error = got_ref_list(&refs, repo, "refs/tags", cmp_tags, repo);
+	if (error)
+		goto done;
+
+	SIMPLEQ_FOREACH(re, &refs, entry) {
+		const char *refname;
+		char *refstr, *tag_log0, *tag_log, *id_str;
+		time_t tagger_time;
+		struct got_object_id *id;
+		struct got_tag_object *tag;
+
+		refname = got_ref_get_name(re->ref);
+		if (strncmp(refname, "refs/tags/", 10) != 0)
+			continue;
+		refname += 10;
+		refstr = got_ref_to_str(re->ref);
+		if (refstr == NULL) {
+			error = got_error_from_errno("got_ref_to_str");
+			goto done;
+		}
+
+		error = got_ref_resolve(&id, repo, re->ref);
+		if (error)
+			goto done;
+		error = got_object_open_as_tag(&tag, repo, id);
+		free(id);
+		if (error)
+			goto done;
+
+		tagger_time = got_object_tag_get_tagger_time(tag);
+
+		error = got_object_id_str(&id_str,
+		    got_object_tag_get_object_id(tag));
+		if (error)
+			goto done;
+
+		tag_log0 = strdup(got_object_tag_get_message(tag));
+
+		if (tag_log0 == NULL) {
+			error = got_error_from_errno("strdup");
+			goto done;
+		}
+
+		tag_log = tag_log0;
+		while (*tag_log == '\n')
+			tag_log++;
+
+		switch (tag_type) {
+		case TAGBRIEF:
+			newline = strchr(tag_log, '\n');
+			if (newline)
+				*newline = '\0';
+
+			if ((asprintf(&age, "%s", gw_get_time_str(tagger_time,
+			    TM_DIFF))) == -1) {
+				error = got_error_from_errno("asprintf");
+				goto done;
+			}
+
+			if ((asprintf(&tags_navs_disp, tags_navs,
+			    gw_trans->repo_name, id_str, gw_trans->repo_name,
+			    id_str, gw_trans->repo_name, id_str,
+			    gw_trans->repo_name, id_str)) == -1) {
+				error = got_error_from_errno("asprintf");
+				goto done;
+			}
+
+			if ((asprintf(&tag_row, tags_row, age, refname, tag_log,
+			    tags_navs_disp)) == -1) {
+				error = got_error_from_errno("asprintf");
+				goto done;
+			}
+
+			free(tags_navs_disp);
+			break;
+		case TAGFULL:
+			break;
+		default:
+			break;
+		}
+
+		got_object_tag_close(tag);
+
+		error = buf_puts(&newsize, diffbuf, tag_row);
+
+		free(id_str);
+		free(refstr);
+		free(age);
+		free(tag_log0);
+		free(tag_row);
+
+		if (error || (limit && --limit == 0))
+			break;
+	}
+
+	if (buf_len(diffbuf) > 0) {
+		error = buf_putc(diffbuf, '\0');
+		tags = strdup(buf_get(diffbuf));
+	}
+done:
+	buf_free(diffbuf);
+	got_ref_list_free(&refs);
+	if (repo)
+		got_repo_close(repo);
+	if (error)
+		return NULL;
+	else
+		return tags;
+}
+
+struct blame_line {
+	int annotated;
+	char *id_str;
+	char *committer;
+	char datebuf[11]; /* YYYY-MM-DD + NUL */
+};
+
+struct blame_cb_args {
+	struct blame_line *lines;
+	int nlines;
+	int nlines_prec;
+	int lineno_cur;
+	off_t *line_offsets;
+	FILE *f;
+	struct got_repository *repo;
+	struct trans *gw_trans;
+	struct buf *blamebuf;
+};
+
+static const struct got_error *
+blame_cb(void *arg, int nlines, int lineno, struct got_object_id *id)
+{
+	const struct got_error *err = NULL;
+	struct blame_cb_args *a = arg;
+	struct blame_line *bline;
+	char *line = NULL;
+	size_t linesize = 0, newsize;
+	struct got_commit_object *commit = NULL;
+	off_t offset;
+	struct tm tm;
+	time_t committer_time;
+
+	if (nlines != a->nlines ||
+	    (lineno != -1 && lineno < 1) || lineno > a->nlines)
+		return got_error(GOT_ERR_RANGE);
+
+	if (lineno == -1)
+		return NULL; /* no change in this commit */
+
+	/* Annotate this line. */
+	bline = &a->lines[lineno - 1];
+	if (bline->annotated)
+		return NULL;
+	err = got_object_id_str(&bline->id_str, id);
+	if (err)
+		return err;
+
+	err = got_object_open_as_commit(&commit, a->repo, id);
+	if (err)
+		goto done;
+
+	bline->committer = strdup(got_object_commit_get_committer(commit));
+	if (bline->committer == NULL) {
+		err = got_error_from_errno("strdup");
+		goto done;
+	}
+
+	committer_time = got_object_commit_get_committer_time(commit);
+	if (localtime_r(&committer_time, &tm) == NULL)
+		return got_error_from_errno("localtime_r");
+	if (strftime(bline->datebuf, sizeof(bline->datebuf), "%G-%m-%d",
+	    &tm) >= sizeof(bline->datebuf)) {
+		err = got_error(GOT_ERR_NO_SPACE);
+		goto done;
+	}
+	bline->annotated = 1;
+
+	/* Print lines annotated so far. */
+	bline = &a->lines[a->lineno_cur - 1];
+	if (!bline->annotated)
+		goto done;
+
+	offset = a->line_offsets[a->lineno_cur - 1];
+	if (fseeko(a->f, offset, SEEK_SET) == -1) {
+		err = got_error_from_errno("fseeko");
+		goto done;
+	}
+
+	while (bline->annotated) {
+		char *smallerthan, *at, *nl, *committer, *blame_row = NULL;
+		size_t len;
+
+		if (getline(&line, &linesize, a->f) == -1) {
+			if (ferror(a->f))
+				err = got_error_from_errno("getline");
+			break;
+		}
+
+		committer = bline->committer;
+		smallerthan = strchr(committer, '<');
+		if (smallerthan && smallerthan[1] != '\0')
+			committer = smallerthan + 1;
+		at = strchr(committer, '@');
+		if (at)
+			*at = '\0';
+		len = strlen(committer);
+		if (len >= 9)
+			committer[8] = '\0';
+
+		nl = strchr(line, '\n');
+		if (nl)
+			*nl = '\0';
+		asprintf(&blame_row, log_blame_line, a->nlines_prec,
+		    a->lineno_cur, bline->id_str, bline->datebuf, committer,
+		    line);
+		a->lineno_cur++;
+		err = buf_puts(&newsize, a->blamebuf, blame_row);
+		if (err)
+			return err;
+
+		bline = &a->lines[a->lineno_cur - 1];
+		free(blame_row);
+	}
+done:
+	if (commit)
+		got_object_commit_close(commit);
+	free(line);
+	return err;
+}
+
+static char*
+gw_get_file_blame(struct trans *gw_trans, char *commit_str)
+{
+	const struct got_error *error = NULL;
+	struct got_repository *repo = NULL;
+	struct got_object_id *obj_id = NULL;
+	struct got_object_id *commit_id = NULL;
+	struct got_blob_object *blob = NULL;
+	char *blame_html = NULL, *path = NULL, *in_repo_path = NULL,
+	     *folder = NULL;
+	struct blame_cb_args bca;
+	int i, obj_type;
+	size_t filesize;
+
+	error = got_repo_open(&repo, gw_trans->repo_path, NULL);
+	if (error)
+		goto done;
+
+	if (gw_trans->repo_folder != NULL) {
+		if ((asprintf(&folder, "%s/", gw_trans->repo_folder)) == -1) {
+			error = got_error_from_errno("asprintf");
+			goto done;
+		}
+	} else
+		folder = strdup("");
+
+	if ((asprintf(&path, "%s%s", folder, gw_trans->repo_file)) == -1) {
+		error = got_error_from_errno("asprintf");
+		goto done;
+	}
+	free(folder);
+
+	error = got_repo_map_path(&in_repo_path, repo, path, 1);
+	if (error)
+		goto done;
+
+	error = resolve_commit_arg(&commit_id, commit_str, repo);
+	if (error)
+		goto done;
+
+	error = got_object_id_by_path(&obj_id, repo, commit_id, in_repo_path);
+	if (error)
+		goto done;
+
+	if (obj_id == NULL) {
+		error = got_error(GOT_ERR_NO_OBJ);
+		goto done;
+	}
+
+	error = got_object_get_type(&obj_type, repo, obj_id);
+	if (error)
+		goto done;
+
+	if (obj_type != GOT_OBJ_TYPE_BLOB) {
+		error = got_error(GOT_ERR_OBJ_TYPE);
+		goto done;
+	}
+
+	error = got_object_open_as_blob(&blob, repo, obj_id, 8192);
+	if (error)
+		goto done;
+
+	error = buf_alloc(&bca.blamebuf, 0);
+	if (error)
+		goto done;
+
+	bca.f = got_opentemp();
+	if (bca.f == NULL) {
+		error = got_error_from_errno("got_opentemp");
+		goto done;
+	}
+	error = got_object_blob_dump_to_file(&filesize, &bca.nlines,
+	    &bca.line_offsets, bca.f, blob);
+	if (error || bca.nlines == 0)
+		goto done;
+
+	/* Don't include \n at EOF in the blame line count. */
+	if (bca.line_offsets[bca.nlines - 1] == filesize)
+		bca.nlines--;
+
+	bca.lines = calloc(bca.nlines, sizeof(*bca.lines));
+	if (bca.lines == NULL) {
+		error = got_error_from_errno("calloc");
+		goto done;
+	}
+	bca.lineno_cur = 1;
+	bca.nlines_prec = 0;
+	i = bca.nlines;
+	while (i > 0) {
+		i /= 10;
+		bca.nlines_prec++;
+	}
+	bca.repo = repo;
+	bca.gw_trans = gw_trans;
+
+	error = got_blame(in_repo_path, commit_id, repo, blame_cb, &bca, NULL,
+	    NULL);
+	if (buf_len(bca.blamebuf) > 0) {
+		error = buf_putc(bca.blamebuf, '\0');
+		blame_html = strdup(buf_get(bca.blamebuf));
+	}
+done:
+	free(bca.blamebuf);
+	free(in_repo_path);
+	free(commit_id);
+	free(obj_id);
+	free(path);
+
+	if (blob)
+		error = got_object_blob_close(blob);
+	if (repo)
+		error = got_repo_close(repo);
+	if (error)
+		return NULL;
+	if (bca.lines) {
+		for (i = 0; i < bca.nlines; i++) {
+			struct blame_line *bline = &bca.lines[i];
+			free(bline->id_str);
+			free(bline->committer);
+		}
+		free(bca.lines);
+	}
+	free(bca.line_offsets);
+	if (bca.f && fclose(bca.f) == EOF && error == NULL)
+		error = got_error_from_errno("fclose");
+	if (error)
+		return NULL;
+	else
+		return blame_html;
+}
+
+static char*
+gw_get_repo_tree(struct trans *gw_trans, char *commit_str)
+{
+	const struct got_error *error = NULL;
+	struct got_repository *repo = NULL;
+	struct got_object_id *tree_id = NULL, *commit_id = NULL;
+	struct got_tree_object *tree = NULL;
+	struct buf *diffbuf = NULL;
+	size_t newsize;
+	char *tree_html = NULL, *path = NULL, *in_repo_path = NULL,
+	    *tree_row = NULL, *id_str;
+	int nentries, i;
+
+	error = buf_alloc(&diffbuf, 0);
+	if (error)
+		return NULL;
+
+	error = got_repo_open(&repo, gw_trans->repo_path, NULL);
+	if (error)
+		goto done;
+
+	error = got_repo_map_path(&in_repo_path, repo, gw_trans->repo_path, 1);
+	if (error)
+		goto done;
+
+	if (gw_trans->repo_folder != NULL)
+		path = strdup(gw_trans->repo_folder);
+	else if (in_repo_path) {
+		free(path);
+		path = in_repo_path;
+	}
+
+	if (commit_str == NULL) {
+		struct got_reference *head_ref;
+		error = got_ref_open(&head_ref, repo, gw_trans->headref, 0);
+		if (error)
+			goto done;
+
+		error = got_ref_resolve(&commit_id, repo, head_ref);
+		got_ref_close(head_ref);
+
+	} else
+		error = resolve_commit_arg(&commit_id, commit_str, repo);
+	if (error)
+		goto done;
+
+	error = got_object_id_by_path(&tree_id, repo, commit_id, path);
+	if (error)
+		goto done;
+
+	error = got_object_open_as_tree(&tree, repo, tree_id);
+	if (error)
+		goto done;
+
+	nentries = got_object_tree_get_nentries(tree);
+
+	for (i = 0; i < nentries; i++) {
+		struct got_tree_entry *te;
+		const char *modestr = "";
+		char *id = NULL, *url_html = NULL;
+
+		te = got_object_tree_get_entry(tree, i);
+
+		error = got_object_id_str(&id_str, got_tree_entry_get_id(te));
+		if (error)
+			goto done;
+
+		if ((asprintf(&id, "%s", id_str)) == -1) {
+			error = got_error_from_errno("asprintf");
+			free(id_str);
+			goto done;
+		}
+
+		mode_t mode = got_tree_entry_get_mode(te);
+
+		if (got_object_tree_entry_is_submodule(te))
+			modestr = "$";
+		else if (S_ISLNK(mode))
+			modestr = "@";
+		else if (S_ISDIR(mode))
+			modestr = "/";
+		else if (mode & S_IXUSR)
+			modestr = "*";
+
+		char *build_folder = NULL;
+		if (S_ISDIR(got_tree_entry_get_mode(te))) {
+			if (gw_trans->repo_folder != NULL) {
+				if ((asprintf(&build_folder, "%s/%s",
+				    gw_trans->repo_folder,
+				    got_tree_entry_get_name(te))) == -1) {
+					error =
+					    got_error_from_errno("asprintf");
+					goto done;
+				}
+			} else {
+				if (asprintf(&build_folder, "%s",
+				    got_tree_entry_get_name(te)) == -1)
+					goto done;
+			}
+
+			if ((asprintf(&url_html, folder_html,
+			    gw_trans->repo_name, gw_trans->action_name,
+			    gw_trans->commit, build_folder,
+			    got_tree_entry_get_name(te), modestr)) == -1) {
+				error = got_error_from_errno("asprintf");
+				goto done;
+			}
+		} else {
+			if (gw_trans->repo_folder != NULL) {
+				if ((asprintf(&build_folder, "%s",
+				    gw_trans->repo_folder)) == -1) {
+					error =
+					    got_error_from_errno("asprintf");
+					goto done;
+				}
+			} else
+				build_folder = strdup("");
+
+			if ((asprintf(&url_html, file_html, gw_trans->repo_name,
+			    "blame", gw_trans->commit,
+			    got_tree_entry_get_name(te), build_folder,
+			    got_tree_entry_get_name(te), modestr)) == -1) {
+				error = got_error_from_errno("asprintf");
+				goto done;
+			}
+		}
+		free(build_folder);
+
+		if (error)
+			goto done;
+
+		if ((asprintf(&tree_row, trees_row, "", url_html)) == -1) {
+			error = got_error_from_errno("asprintf");
+			goto done;
+		}
+		error = buf_puts(&newsize, diffbuf, tree_row);
+		if (error)
+			goto done;
+
+		free(id);
+		free(id_str);
+		free(url_html);
+		free(tree_row);
+	}
+
+	if (buf_len(diffbuf) > 0) {
+		error = buf_putc(diffbuf, '\0');
+		tree_html = strdup(buf_get(diffbuf));
+	}
+done:
+	if (tree)
+		got_object_tree_close(tree);
+	if (repo)
+		got_repo_close(repo);
+
+	free(in_repo_path);
+	free(tree_id);
+	free(diffbuf);
+	if (error)
+		return NULL;
+	else
+		return tree_html;
+}
+
+static char *
+gw_get_repo_heads(struct trans *gw_trans)
+{
+	const struct got_error *error = NULL;
+	struct got_repository *repo = NULL;
+	struct got_reflist_head refs;
+	struct got_reflist_entry *re;
+	char *heads, *head_row = NULL, *head_navs_disp = NULL, *age = NULL;
+	struct buf *diffbuf = NULL;
+	size_t newsize;
+
+	error = buf_alloc(&diffbuf, 0);
+	if (error)
+		return NULL;
+
+	error = got_repo_open(&repo, gw_trans->repo_path, NULL);
+	if (error)
+		goto done;
+
+	SIMPLEQ_INIT(&refs);
+	error = got_ref_list(&refs, repo, "refs/heads", got_ref_cmp_by_name,
+	    NULL);
+	if (error)
+		goto done;
+
+	SIMPLEQ_FOREACH(re, &refs, entry) {
+		char *refname;
+
+		refname = strdup(got_ref_get_name(re->ref));
+		if (refname == NULL) {
+			error = got_error_from_errno("got_ref_to_str");
+			goto done;
+		}
+
+		if (strncmp(refname, "refs/heads/", 11) != 0) {
+			free(refname);
+			continue;
+		}
+
+		age = gw_get_repo_age(gw_trans, gw_trans->gw_dir->path, refname,
+		    TM_DIFF);
+
+		if ((asprintf(&head_navs_disp, heads_navs, gw_trans->repo_name,
+		    refname, gw_trans->repo_name, refname,
+		    gw_trans->repo_name, refname, gw_trans->repo_name,
+		    refname)) == -1) {
+			error = got_error_from_errno("asprintf");
+			goto done;
+		}
+
+		if (strncmp(refname, "refs/heads/", 11) == 0)
+			refname += 11;
+
+		if ((asprintf(&head_row, heads_row, age, refname,
+		    head_navs_disp)) == -1) {
+			error = got_error_from_errno("asprintf");
+			goto done;
+		}
+
+		error = buf_puts(&newsize, diffbuf, head_row);
+
+		free(head_navs_disp);
+		free(head_row);
+	}
+
+	if (buf_len(diffbuf) > 0) {
+		error = buf_putc(diffbuf, '\0');
+		heads = strdup(buf_get(diffbuf));
+	}
+done:
+	buf_free(diffbuf);
+	got_ref_list_free(&refs);
+	if (repo)
+		got_repo_close(repo);
+	if (error)
+		return NULL;
+	else
+		return heads;
+}
+
+static char *
+gw_get_got_link(struct trans *gw_trans)
+{
+	char *link;
+
+	if ((asprintf(&link, got_link, gw_trans->gw_conf->got_logo_url,
+	    gw_trans->gw_conf->got_logo)) == -1)
+		return NULL;
+
+	return link;
+}
+
+static char *
+gw_get_site_link(struct trans *gw_trans)
+{
+	char *link, *repo = "", *action = "";
+
+	if (gw_trans->repo_name != NULL)
+		if ((asprintf(&repo, " / <a href='?path=%s&action=summary'>%s" \
+		    "</a>", gw_trans->repo_name, gw_trans->repo_name)) == -1)
+			return NULL;
+
+	if (gw_trans->action_name != NULL)
+		if ((asprintf(&action, " / %s", gw_trans->action_name)) == -1)
+			return NULL;
+
+	if ((asprintf(&link, site_link, GOTWEB,
+	    gw_trans->gw_conf->got_site_link, repo, action)) == -1)
+		return NULL;
+
+	return link;
+}
+
+static char *
+color_diff_line(char *buf)
+{
+	const struct got_error *error = NULL;
+	char *colorized_line = NULL, *div_diff_line_div = NULL, *color = NULL;
+	struct buf *diffbuf = NULL;
+	size_t newsize;
+
+	error = buf_alloc(&diffbuf, 0);
+	if (error)
+		return NULL;
+
+	if (strncmp(buf, "-", 1) == 0)
+		color = "diff_minus";
+	if (strncmp(buf, "+", 1) == 0)
+		color = "diff_plus";
+	if (strncmp(buf, "@@", 2) == 0)
+		color = "diff_chunk_header";
+	if (strncmp(buf, "@@", 2) == 0)
+		color = "diff_chunk_header";
+	if (strncmp(buf, "commit +", 8) == 0)
+		color = "diff_meta";
+	if (strncmp(buf, "commit -", 8) == 0)
+		color = "diff_meta";
+	if (strncmp(buf, "blob +", 6) == 0)
+		color = "diff_meta";
+	if (strncmp(buf, "blob -", 6) == 0)
+		color = "diff_meta";
+	if (strncmp(buf, "file +", 6) == 0)
+		color = "diff_meta";
+	if (strncmp(buf, "file -", 6) == 0)
+		color = "diff_meta";
+	if (strncmp(buf, "from:", 5) == 0)
+		color = "diff_author";
+	if (strncmp(buf, "via:", 4) == 0)
+		color = "diff_author";
+	if (strncmp(buf, "date:", 5) == 0)
+		color = "diff_date";
+
+	if ((asprintf(&div_diff_line_div, div_diff_line, color)) == -1)
+		return NULL;
+
+	error = buf_puts(&newsize, diffbuf, div_diff_line_div);
+	if (error)
+		return NULL;
+
+	error = buf_puts(&newsize, diffbuf, buf);
+	if (error)
+		return NULL;
+
+	if (buf_len(diffbuf) > 0) {
+		error = buf_putc(diffbuf, '\0');
+		colorized_line = strdup(buf_get(diffbuf));
+	}
+
+	free(diffbuf);
+	free(div_diff_line_div);
+	return colorized_line;
+}
+
+static char *
+gw_html_escape(const char *html)
+{
+	char *escaped_str = NULL, *buf;
+	char c[1];
+	size_t sz, i, buff_sz = 2048;
+
+	if ((buf = calloc(buff_sz, sizeof(char *))) == NULL)
+		return NULL;
+
+	if (html == NULL)
+		return NULL;
+	else
+		if ((sz = strlen(html)) == 0)
+			return NULL;
+
+	/* only work with buff_sz */
+	if (buff_sz < sz)
+		sz = buff_sz;
+
+	for (i = 0; i < sz; i++) {
+		c[0] = html[i];
+		switch (c[0]) {
+		case ('>'):
+			strcat(buf, "&gt;");
+			break;
+		case ('&'):
+			strcat(buf, "&amp;");
+			break;
+		case ('<'):
+			strcat(buf, "&lt;");
+			break;
+		case ('"'):
+			strcat(buf, "&quot;");
+			break;
+		case ('\''):
+			strcat(buf, "&apos;");
+			break;
+		case ('\n'):
+			strcat(buf, "<br />");
+		default:
+			strcat(buf, &c[0]);
+			break;
+		}
+	}
+	asprintf(&escaped_str, "%s", buf);
+	free(buf);
+	return escaped_str;
+}
+
+int
+main()
+{
+	const struct got_error *error = NULL;
+	struct trans *gw_trans;
+	struct gw_dir *dir = NULL, *tdir;
+	const char *page = "index";
+	int gw_malloc = 1;
+
+	if ((gw_trans = malloc(sizeof(struct trans))) == NULL)
+		errx(1, "malloc");
+
+	if ((gw_trans->gw_req = malloc(sizeof(struct kreq))) == NULL)
+		errx(1, "malloc");
+
+	if ((gw_trans->gw_html_req = malloc(sizeof(struct khtmlreq))) == NULL)
+		errx(1, "malloc");
+
+	if ((gw_trans->gw_tmpl = malloc(sizeof(struct ktemplate))) == NULL)
+		errx(1, "malloc");
+
+	if (KCGI_OK != khttp_parse(gw_trans->gw_req, gw_keys, KEY__ZMAX,
+	    &page, 1, 0))
+		errx(1, "khttp_parse");
+
+	if ((gw_trans->gw_conf =
+	    malloc(sizeof(struct gotweb_conf))) == NULL) {
+		gw_malloc = 0;
+		error = got_error_from_errno("malloc");
+		goto err;
+	}
+
+	if (pledge("stdio rpath wpath cpath proc exec sendfd unveil",
+	    NULL) == -1) {
+		error = got_error_from_errno("pledge");
+		goto err;
+	}
+
+	TAILQ_INIT(&gw_trans->gw_dirs);
+
+	gw_trans->page = 0;
+	gw_trans->repos_total = 0;
+	gw_trans->repo_path = NULL;
+	gw_trans->commit = NULL;
+	gw_trans->headref = strdup(GOT_REF_HEAD);
+	gw_trans->mime = KMIME_TEXT_HTML;
+	gw_trans->gw_tmpl->key = templs;
+	gw_trans->gw_tmpl->keysz = TEMPL__MAX;
+	gw_trans->gw_tmpl->arg = gw_trans;
+	gw_trans->gw_tmpl->cb = gw_template;
+	error = parse_conf(GOTWEB_CONF, gw_trans->gw_conf);
+
+err:
+	if (error) {
+		gw_trans->mime = KMIME_TEXT_PLAIN;
+		gw_trans->action = GW_ERR;
+		gw_display_index(gw_trans, error);
+		goto done;
+	}
+
+	error = gw_parse_querystring(gw_trans);
+	if (error)
+		goto err;
+
+	gw_display_index(gw_trans, error);
+
+done:
+	if (gw_malloc) {
+		free(gw_trans->gw_conf->got_repos_path);
+		free(gw_trans->gw_conf->got_www_path);
+		free(gw_trans->gw_conf->got_site_name);
+		free(gw_trans->gw_conf->got_site_owner);
+		free(gw_trans->gw_conf->got_site_link);
+		free(gw_trans->gw_conf->got_logo);
+		free(gw_trans->gw_conf->got_logo_url);
+		free(gw_trans->gw_conf);
+		free(gw_trans->commit);
+		free(gw_trans->repo_path);
+		free(gw_trans->repo_name);
+		free(gw_trans->repo_file);
+		free(gw_trans->action_name);
+		free(gw_trans->headref);
+
+		TAILQ_FOREACH_SAFE(dir, &gw_trans->gw_dirs, entry, tdir) {
+			free(dir->name);
+			free(dir->description);
+			free(dir->age);
+			free(dir->url);
+			free(dir->path);
+			free(dir);
+		}
+
+	}
+
+	khttp_free(gw_trans->gw_req);
+	return EXIT_SUCCESS;
+}
blob - /dev/null
blob + dc7cb15803cd952fb4cbdd56ba70b4d96445b7fc (mode 644)
--- /dev/null
+++ gotweb/gotweb.conf.5
@@ -0,0 +1,80 @@
+.\"
+.\" Copyright (c) 2020 Tracey Emery <tracey@traceyemery.net>
+.\"
+.\" 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.
+.\"
+.Dd $Mdocdate: January 15 2020 $
+.Dt GOTWEB.CONF 5
+.Os
+.Sh NAME
+.Nm gotweb.conf
+.Nd gotweb configuration file
+.Sh DESCRIPTION
+.Nm
+is the configuration file for gotweb.
+.Sh GLOBAL CONFIGURATION
+Here are the settings that can be set globally:
+.Bl -tag -width Ds
+.It Ic got_commits_display Ar number
+Set the maximum amount of log lines displayed.
+.It Ic got_logo Ar string
+Set the displayed logo.
+.It Ic got_logo_url Ar string
+Set the href link for the logo.
+.It Ic got_max_repos Ar number
+Set the maximum amount of repositories gotweb will work with.
+.It Ic got_max_repos_display Ar number
+Set the maximum amount of repositories displayed on the index screen.
+.It Ic got_repo_age Ar string
+Set whether to display the repository age, or not.
+.It Ic got_repo_cloneurl Ar string
+Set whether to display clone URLs for a repository.
+This requires the creation of a cloneurl file in the repository.
+.It Ic got_repo_description Ar string
+Set whether to display the repository description, or not.
+This requires the description file to be edited in the repository.
+.It Ic got_repos_path Ar string
+Set the default git repository path.
+.It Ic got_repo_owner Ar string
+Set whether to display the repository owner, or not.
+This requires the owner to be added to the config file in the repository.
+The gotweb code will parse either [gotweb] or [gitweb] owner information.
+For example:
+.Bd -literal -offset indent
+[gotweb]
+owner = "Your Name"
+.Ed
+.It Ic got_site_link Ar string
+Set the displayed site link name for the index page.
+.It Ic got_site_name Ar string
+Set the displayed site name title.
+.It Ic got_site_owner Ar string
+Set the displayed site owner.
+.It Ic got_show_site_owner Ar string
+Set whether to display the site owner, or not.
+.It Ic got_www_path Ar string
+Set the public gotweb httpd path.
+.El
+.Sh FILES
+.Bl -tag -width Ds -compact
+.It Pa /var/www/etc/gotweb.conf
+gotweb configuration file.
+.El
+.Sh SEE ALSO
+.Xr got 1 ,
+.Xr tog 1
+.Sh HISTORY
+The
+.Nm
+file format first appeared in
+.Ox 6.7 .
blob - /dev/null
blob + 664f2731e0a19f79ff966dd2bfcc7a1466a2c93b (mode 644)
--- /dev/null
+++ gotweb/gotweb.h
@@ -0,0 +1,74 @@
+/*
+ * Copyright (c) 2019, 2020 Tracey Emery <tracey@traceyemery.net>
+ * Copyright (c) 2018, 2019 Stefan Sperling <stsp@openbsd.org>
+ *
+ * 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.
+ */
+
+#ifndef GOTWEB_H
+#define GOTWEB_H
+
+#include <stdbool.h>
+
+#include <got_error.h>
+
+#define	GOTWEB_CONF	 "/etc/gotweb.conf"
+#define GOTWEB_TMPL_DIR	 "/cgi-bin/gw_tmpl"
+#define GOTWEB		 "/cgi-bin/gotweb/gotweb"
+
+#define GOTWEB_GOT_DIR	 ".got"
+#define GOTWEB_GIT_DIR	 ".git"
+
+#define D_GOTPATH	 "/got/public"
+#define D_GOTWWW	 "/gotweb"
+#define D_SITENAME	 "Gotweb"
+#define D_SITEOWNER	 "Got Owner"
+#define D_SITELINK	 "Repos"
+#define D_GOTLOGO	 "got.png"
+#define D_GOTURL	 "https://gameoftrees.org"
+
+#define	D_SHOWROWNER	 true
+#define	D_SHOWSOWNER	 true
+#define D_SHOWAGE	 true
+#define D_SHOWDESC	 true
+#define D_SHOWURL	 true
+#define	D_MAXREPO	 0
+#define D_MAXREPODISP	 25
+#define D_MAXSLCOMMDISP	 10
+#define D_MAXCOMMITDISP	 25
+
+#define BUFFER_SIZE	 2048
+
+struct gotweb_conf {
+	char		*got_repos_path;
+	char		*got_www_path;
+	char		*got_site_name;
+	char		*got_site_owner;
+	char		*got_site_link;
+	char		*got_logo;
+	char		*got_logo_url;
+
+	size_t		 got_max_repos;
+	size_t		 got_max_repos_display;
+	size_t		 got_max_commits_display;
+
+	bool		 got_show_site_owner;
+	bool		 got_show_repo_owner;
+	bool		 got_show_repo_age;
+	bool		 got_show_repo_description;
+	bool		 got_show_repo_cloneurl;
+};
+
+const struct got_error*	 parse_conf(const char *, struct gotweb_conf *);
+
+#endif /* GOTWEB_H */
blob - /dev/null
blob + 15583f4aab01f67ca646c83fe31fb9e51b055923 (mode 644)
--- /dev/null
+++ gotweb/gotweb_ui.h
@@ -0,0 +1,358 @@
+/*
+ * Copyright (c) 2019, 2020 Tracey Emery <tracey@traceyemery.net>
+ *
+ * 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.
+ */
+
+#ifndef GOTWEB_UI_H
+#define GOTWEB_UI_H
+
+/* general html */
+
+char *head =
+	"<meta name='viewport' content='initial-scale=1.0," \
+	    " user-scalable=no' />" \
+	"<meta charset='utf-8' />" \
+	"<meta name='msapplication-TileColor' content='#da532c' />" \
+	"<meta name='theme-color' content='#ffffff' />" \
+	"<link rel='apple-touch-icon' sizes='180x180'" \
+	    " href='/apple-touch-icon.png' />" \
+	"<link rel='icon' type='image/png' sizes='32x32'" \
+	    " href='/favicon-32x32.png' />" \
+	"<link rel='icon' type='image/png' sizes='16x16'" \
+	    " href='/favicon-16x16.png' />" \
+	"<link rel='manifest' href='/site.webmanifest' />" \
+	"<link rel='mask-icon' href='/safari-pinned-tab.svg'" \
+	    " color='#5bbad5' />" \
+	"<link rel='stylesheet' type='text/css' href='/gotweb.css' />";
+
+char *got_link =
+	"<div id='got_link'>" \
+	"<a href='%s' target='_sotd'><img src='/%s' alt='logo' /></a>" \
+	"</div>";
+
+char *site_link =
+	"<div id='site_link'>" \
+	"<a href='%s'>%s</a> %s %s" \
+	"</div>";
+
+char *site_owner =
+	"<div id='site_owner_wrapper'><div id='site_owner'>%s</div></div>";
+
+char *search =
+	"<!--/* <div id='search'>" \
+	"<form method='POST'>" \
+	"<input type='search' id='got-search' name='got-search' size='15'" \
+	    " maxlength='50' />" \
+	"<button>Search</button>" \
+	"</form>" \
+	"</div> */-->";
+
+char *np_wrapper_start =
+	"<div id='np_wrapper'>" \
+	"<div id='nav_prev'>";
+
+char *div_diff_line =
+	"<div id='diff_line' class='%s'>";
+
+char *div_end =
+	"</div>";
+
+char *nav_next =
+	"<div id='nav_next'>" \
+	"<a href='?page=%d'>Next<a/>" \
+	"</div>";
+
+char *nav_prev =
+	"<a href='?page=%d'>Previous<a/>";
+
+char *description =
+	"<div id='description_title'>Description: </div>" \
+	"<div id='description'>%s</div>";
+
+char *repo_owner =
+	"<div id='repo_owner_title'>Owner: </div>" \
+	"<div id='repo_owner'>%s</div>";
+
+char *last_change =
+	"<div id='last_change_title'>Last Change: </div>" \
+	"<div id='last_change'>%s</div>";
+
+char *cloneurl =
+	"<div id='cloneurl_title'>Clone URL: </div>" \
+	"<div id='cloneurl'>%s</div>";
+
+char *logbriefs_row =
+	"<div id='logbriefs_wrapper'>" \
+	"<div id='logbriefs_age'>%s</div>" \
+	"<div id='logbriefs_author'>%s</div>" \
+	"<div id='logbriefs_log'>%s</div>" \
+	"</div>" \
+	"<div id='navs_wrapper'>" \
+	"<div id='navs'>%s</div>" \
+	"</div>" \
+	"</div>" \
+	"<div id='dotted_line'></div>";
+
+char *logbriefs_navs =
+	/* "<a href='?path=%s&action=commit&commit=%s'>commit</a> | " \ */
+	"<a href='?path=%s&action=commitdiff&commit=%s'>diff</a> | " \
+	"<a href='?path=%s&action=tree&commit=%s'>tree</a><!--/* | " \
+	"<a href='?path=%s&action=snapshot&commit=%s'>snapshot</a> */-->";
+
+char *tags_row =
+	"<div id='tags_wrapper'>" \
+	"<div id='tags_age'>%s</div>" \
+	"<div id='tag'>tag %s</div>" \
+	"<div id='tag_name'>%s</div>" \
+	"</div>" \
+	"<div id='navs_wrapper'>" \
+	"<div id='navs'>%s</div>" \
+	"</div>" \
+	"</div>" \
+	"<div id='dotted_line'></div>";
+
+char *tags_navs =
+	"<a href='?path=%s&action=tag&commit=%s'>tag</a> | " \
+	/* "<a href='?path=%s&action=commit&commit=%s'>commit</a> | " \ */
+	"<a href='?path=%s&action=logbriefs&commit=%s'>log briefs</a> | " \
+	"<a href='?path=%s&action=log&commit=%s'>log</a>";
+
+char *trees_row =
+	"<div id='tree_wrapper'>" \
+	"<div id='tree_id'>%s</div>" \
+	"<div id='tree'>%s</div>" \
+	"</div>";
+
+char *heads_row =
+	"<div id='heads_wrapper'>" \
+	"<div id='heads_age'>%s</div>" \
+	"<div id='head'>%s</div>" \
+	"</div>" \
+	"<div id='navs_wrapper'>" \
+	"<div id='navs'>%s</div>" \
+	"</div>" \
+	"</div>" \
+	"<div id='dotted_line'></div>";
+
+char *heads_navs =
+	"<a href='?path=%s&action=summary&headref=%s'>summary</a> | " \
+	"<a href='?path=%s&action=logbriefs&headref=%s'>log briefs</a> | " \
+	"<a href='?path=%s&action=log&headref=%s'>log</a> | ";
+	/* "<a href='?path=%s&action=commit&headref=%s'>commit</a>"; */
+
+char *commit_diff_html =
+	"<div id='commit_diff_title'>Diff:</div>" \
+	"<div id='commit_diff'>%s %s</div>";
+
+char *commit_commit_html =
+	"<div id='commit_commit_title'>Commit:</div>" \
+	"<div id='commit_commit'>%s %s</div>";
+
+char *commit_author_html =
+	"<div id='commit_author_title'>Author:</div>" \
+	"<div id='commit_author'>%s</div>";
+
+char *commit_committer_html =
+	"<div id='commit_committer_title'>Committer:</div>" \
+	"<div id='commit_committer'>%s</div>";
+
+char *commit_age_html =
+	"<div id='commit_age_title'>Date:</div>" \
+	"<div id='commit_age'>%s</div>";
+
+char *commit_log_html =
+	"<div id='commit_log_title'>Log:</div>" \
+	"<div id='commit_log'>%s</div>";
+
+char *commit_tree_html =
+	"<div id='commit_log_title'>Tree:</div>" \
+	"<div id='commit_log'>%s</div>";
+
+char *folder_html =
+	"<a href='?path=%s&action=%s&commit=%s&folder=/%s' " \
+	    "class='diff_directory'>%s%s</a>";
+
+char *file_html =
+	"<a href='?path=%s&action=%s&commit=%s&file=%s&folder=/%s'>%s%s</a>";
+
+/* log.tmpl */
+
+char *logs =
+	"<div id='logs_title_wrapper'>" \
+	"<div id='logs_title'>Commits</div></div>" \
+	"<div id='logs_content'>%s</div>";
+
+char *logs_row =
+	"<div id='logs_row_wrapper'>%s%s%s%s</div>" \
+	"<div id='dotted_line'></div>" \
+	"<div id='log'>%s</div>" \
+	"<div id='navs_wrapper'>" \
+	"<div id='navs'>%s</div>" \
+	"</div>" \
+	"</div>" \
+	"<div id='solid_line'></div>";
+
+char *logs_navs =
+	/* "<a href='?path=%s&action=commit&commit=%s'>commit</a> | " \ */
+	"<a href='?path=%s&action=commitdiff&commit=%s'>diff</a> | " \
+	"<a href='?path=%s&action=tree&commit=%s'>tree</a><!--/* | " \
+	"<a href='?path=%s&action=snapshot&commit=%s'>snapshot</a> */-->";
+
+/* tag.tmpl */
+
+char *log_tag =
+	"<div id='log_tree_title_wrapper'>" \
+	"<div id='log_tree_title'>Tag</div></div>" \
+	"<div id='log_tree_content'>%s</div>";
+
+char *log_tag_row =
+	"<div id='log_tag_row_wrapper'>" \
+	"<div id='log_tag_commit'>%s</div>" \
+	"</div>" \
+	"<div id='dotted_line'></div>" \
+	"<div id='log_tag'>%s</div>" \
+	"</div>";
+
+/* blame.tmpl */
+
+char *log_blame =
+	"<div id='log_blame_title_wrapper'>" \
+	"<div id='log_blame_title'>Blame</div></div>" \
+	"<div id='log_blame_content'>%s</div>";
+
+char *log_blame_row =
+	"<div id='log_blame_row_wrapper'>" \
+	"<div id='log_blame_commit'>%s</div>" \
+	"</div>" \
+	"<div id='dotted_line'></div>" \
+	"<div id='log_blame'>%s</div>" \
+	"</div>";
+
+char *log_blame_navs =
+	/* "<a href='?path=%s&action=commit&commit=%s'>commit</a> | " \ */
+	"<a href='?path=%s&action=commitdiff&commit=%s'>diff</a> | " \
+	"<a href='?path=%s&action=blame&commit=%s'>blame</a><!--/* | " \
+	"<a href='?path=%s&action=snapshot&commit=%s'>snapshot</a> */-->";
+
+char *log_blame_line =
+	"<div id='blame_wrapper'>" \
+	"<div id='blame_number'>%.*d</div>" \
+	"<div id='blame_hash'>%.8s</div>" \
+	"<div id='blame_date'>%s</div>" \
+	"<div id='blame_author'>%-8s</div>" \
+	"<div id='blame_code'>%s</div>" \
+	"</div>";
+
+/* tree.tmpl */
+
+char *log_tree =
+	"<div id='log_tree_title_wrapper'>" \
+	"<div id='log_tree_title'>Tree</div></div>" \
+	"<div id='log_tree_content'>%s</div>";
+
+char *log_tree_row =
+	"<div id='log_tree_row_wrapper'>" \
+	"<div id='log_tree_commit'>%s</div>" \
+	"</div>" \
+	"<div id='dotted_line'></div>" \
+	"<div id='log_tree'>%s</div>" \
+	"</div>";
+
+char *log_tree_navs =
+	/* "<a href='?path=%s&action=commit&commit=%s'>commit</a> | " \ */
+	"<a href='?path=%s&action=commitdiff&commit=%s'>diff</a> | " \
+	"<a href='?path=%s&action=tree&commit=%s'>tree</a><!--/* | " \
+	"<a href='?path=%s&action=snapshot&commit=%s'>snapshot</a> */-->";
+
+/* commit.tmpl */
+
+char *log_commit =
+	"<div id='log_commit_title_wrapper'>" \
+	"<div id='log_commit_title'>Commit</div></div>" \
+	"<div id='log_commit_content'>%s</div>";
+
+char *log_commit_row =
+	"<div id='log_commit_row_wrapper'>" \
+	"<div id='log_commit_commit'>%s%s%s%s%s%s%s</div>" \
+	"</div>" \
+	"<div id='dotted_line'></div>" \
+	"<div id='log_commit'>%s</div>" \
+	"</div>";
+
+/* diff.tmpl */
+
+char *log_diff =
+	"<div id='log_diff_title_wrapper'>" \
+	"<div id='log_diff_title'>Commit Diff</div></div>" \
+	"<div id='log_diff_content'>%s</div>";
+
+char *log_diff_row =
+	"<div id='log_diff_row_wrapper'>" \
+	"<div id='log_commit_diff'>%s%s%s%s%s%s%s</div>" \
+	"</div>" \
+	"<div id='dotted_line'></div>" \
+	"<div id='log_diff'>%s</div>" \
+	"</div>";
+
+/* index.tmpl */
+
+char *index_projects_header =
+	"<div id='index_header'>" \
+	"<div id='index_header_project'>Project</div>" \
+	"<div id='index_header_description'>Description</div>" \
+	"<div id='index_header_owner'>Owner</div>" \
+	"<div id='index_header_age'>Last Change</div>" \
+	"</div>";
+
+char *index_projects =
+	"<div id='index_wrapper'>" \
+	"<div id='index_project'>" \
+	"<a href='?path=%s&action=summary'>%s</a>" \
+	"</div>" \
+	"<div id='index_project_description'>%s</div>" \
+	"<div id='index_project_owner'>%s</div>" \
+	"<div id='index_project_age'>%s</div>" \
+	"<div id='navs_wrapper'>" \
+	"<div id='navs'>%s</div>" \
+	"</div>" \
+	"</div>" \
+	"<div id='dotted_line'></div>";
+
+char *index_navs =
+	"<a href='?path=%s&action=summary'>summary</a> | " \
+	"<a href='?path=%s&action=logbriefs'>log briefs</a> | " \
+	"<a href='?path=%s&action=log'>log</a> | " \
+	"<a href='?path=%s&action=tree'>tree</a>";
+
+/* summary.tmpl */
+
+char *summary_wrapper =
+	"<div id='summary_wrapper'>";
+
+char *summary_logbriefs =
+	"<div id='summary_logbriefs_title_wrapper'>" \
+	"<div id='summary_logbriefs_title'>Log Briefs</div></div>" \
+	"<div id='summary_logbriefs_content'>%s</div>";
+
+char *summary_tags =
+	"<div id='summary_tags_title_wrapper'>" \
+	"<div id='summary_tags_title'>Tags</div></div>" \
+	"<div id='summary_tags_content'>%s</div>";
+
+char *summary_heads =
+	"<div id='summary_heads_title_wrapper'>" \
+	"<div id='summary_heads_title'>Heads</div></div>" \
+	"<div id='summary_heads_content'>%s</div>";
+
+#endif /* GOTWEB_UI_H */
blob - /dev/null
blob + f066d2df1f07bdd931a62cbdb7fb00a3b9bd4989 (mode 644)
--- /dev/null
+++ gotweb/parse.y
@@ -0,0 +1,525 @@
+/*
+ * Copyright (c) 2019 Tracey Emery <tracey@traceyemery.net>
+ * Copyright (c) 2002, 2003, 2004 Henning Brauer <henning@openbsd.org>
+ * Copyright (c) 2001 Markus Friedl.  All rights reserved.
+ * Copyright (c) 2001 Daniel Hartmeier.  All rights reserved.
+ * Copyright (c) 2001 Theo de Raadt.  All rights reserved.
+ *
+ * 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 <sys/types.h>
+#include <sys/queue.h>
+
+#include <ctype.h>
+#include <err.h>
+#include <limits.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "gotweb.h"
+
+TAILQ_HEAD(files, file)		 files = TAILQ_HEAD_INITIALIZER(files);
+static struct file {
+	TAILQ_ENTRY(file)	 entry;
+	FILE			*stream;
+	char			*name;
+	int			 lineno;
+	int			 errors;
+	const struct got_error*	 error;
+} *file, *topfile;
+struct file	*pushfile(const char *);
+int		 popfile(void);
+int		 yyparse(void);
+int		 yylex(void);
+int		 yyerror(const char *, ...)
+    __attribute__((__format__ (printf, 1, 2)))
+    __attribute__((__nonnull__ (1)));
+int		 kw_cmp(const void *, const void *);
+int		 lookup(char *);
+int		 lgetc(int);
+int		 lungetc(int);
+int		 findeol(void);
+
+static const struct got_error*	 gerror = NULL;
+char				*syn_err;
+
+struct gotweb_conf		*gw_conf;
+
+typedef struct {
+	union {
+		int64_t			 number;
+		char			*string;
+	} v;
+	int lineno;
+} YYSTYPE;
+
+%}
+
+%token	GOT_WWW_PATH GOT_MAX_REPOS GOT_SITE_NAME GOT_SITE_OWNER GOT_SITE_LINK
+%token	GOT_LOGO GOT_LOGO_URL GOT_SHOW_REPO_OWNER GOT_SHOW_REPO_AGE
+%token	GOT_SHOW_REPO_DESCRIPTION GOT_MAX_REPOS_DISPLAY GOT_REPOS_PATH
+%token	GOT_MAX_COMMITS_DISPLAY ON ERROR GOT_SHOW_SITE_OWNER
+%token	GOT_SHOW_REPO_CLONEURL
+%token	<v.string>		STRING
+%token	<v.number>		NUMBER
+%type	<v.number>		boolean
+%%
+
+grammar		: /* empty */
+		| grammar '\n'
+		| grammar main '\n'
+		| grammar error '\n'		{ file->errors++; }
+		;
+
+boolean		: STRING			{
+			if (strcasecmp($1, "true") == 0 ||
+			    strcasecmp($1, "yes") == 0)
+				$$ = 1;
+			else if (strcasecmp($1, "false") == 0 ||
+			    strcasecmp($1, "off") == 0 ||
+			    strcasecmp($1, "no") == 0)
+				$$ = 0;
+			else {
+				yyerror("invalid boolean value '%s'", $1);
+				free($1);
+				YYERROR;
+			}
+			free($1);
+		}
+		| ON				{ $$ = 1; }
+		;
+
+main		: GOT_REPOS_PATH STRING {
+      			if ((gw_conf->got_repos_path = strdup($2)) == NULL)
+				errx(1, "out of memory");
+      		}
+		| GOT_WWW_PATH STRING {
+      			if ((gw_conf->got_www_path = strdup($2)) == NULL)
+				errx(1, "out of memory");
+      		}
+		| GOT_MAX_REPOS NUMBER {
+			if ($2 > 0)
+				gw_conf->got_max_repos = $2;
+		}
+		| GOT_SITE_NAME STRING {
+      			if ((gw_conf->got_site_name = strdup($2)) == NULL)
+				errx(1, "out of memory");
+		}
+		| GOT_SITE_OWNER STRING {
+      			if ((gw_conf->got_site_owner = strdup($2)) == NULL)
+				errx(1, "out of memory");
+		}
+		| GOT_SITE_LINK STRING {
+      			if ((gw_conf->got_site_link = strdup($2)) == NULL)
+				errx(1, "out of memory");
+		}
+		| GOT_LOGO STRING {
+      			if ((gw_conf->got_logo = strdup($2)) == NULL)
+				errx(1, "out of memory");
+		}
+		| GOT_LOGO_URL STRING {
+      			if ((gw_conf->got_logo_url = strdup($2)) == NULL)
+				errx(1, "out of memory");
+		}
+		| GOT_SHOW_SITE_OWNER boolean {
+			gw_conf->got_show_site_owner = $2;
+		}
+		| GOT_SHOW_REPO_OWNER boolean {
+			gw_conf->got_show_repo_owner = $2;
+		}
+		| GOT_SHOW_REPO_AGE boolean { gw_conf->got_show_repo_age = $2; }
+		| GOT_SHOW_REPO_DESCRIPTION boolean {
+			gw_conf->got_show_repo_description =	$2;
+		}
+		| GOT_SHOW_REPO_CLONEURL boolean {
+			gw_conf->got_show_repo_cloneurl =	$2;
+		}
+		| GOT_MAX_REPOS_DISPLAY NUMBER {
+			if ($2 > 0)
+				gw_conf->got_max_repos_display = $2;
+		}
+		| GOT_MAX_COMMITS_DISPLAY NUMBER {
+			if ($2 > 0)
+				gw_conf->got_max_commits_display = $2;
+		}
+      		;
+
+%%
+
+struct keywords {
+	const char	*k_name;
+	int		 k_val;
+};
+
+int
+yyerror(const char *fmt, ...)
+{
+	va_list		 ap;
+	char		*msg = NULL;
+	static char	 err_msg[512];
+
+	file->errors++;
+	va_start(ap, fmt);
+	if (vasprintf(&msg, fmt, ap) == -1)
+		errx(1, "yyerror vasprintf");
+	va_end(ap);
+	snprintf(err_msg, sizeof(err_msg), "%s:%d: %s", file->name,
+	    yylval.lineno, msg);
+	gerror = got_error_from_errno2("parse_error", err_msg);
+
+	free(msg);
+	return (0);
+}
+
+int
+kw_cmp(const void *k, const void *e)
+{
+	return (strcmp(k, ((const struct keywords *)e)->k_name));
+}
+
+int
+lookup(char *s)
+{
+	/* this has to be sorted always */
+	static const struct keywords keywords[] = {
+		{ "got_logo",			GOT_LOGO },
+		{ "got_logo_url",		GOT_LOGO_URL },
+		{ "got_max_commits_display",	GOT_MAX_COMMITS_DISPLAY },
+		{ "got_max_repos",		GOT_MAX_REPOS },
+		{ "got_max_repos_display",	GOT_MAX_REPOS_DISPLAY },
+		{ "got_repos_path",		GOT_REPOS_PATH },
+		{ "got_show_repo_age",		GOT_SHOW_REPO_AGE },
+		{ "got_show_repo_cloneurl",	GOT_SHOW_REPO_CLONEURL },
+		{ "got_show_repo_description",	GOT_SHOW_REPO_DESCRIPTION },
+		{ "got_show_repo_owner",	GOT_SHOW_REPO_OWNER },
+		{ "got_show_site_owner",	GOT_SHOW_SITE_OWNER },
+		{ "got_site_link",		GOT_SITE_LINK },
+		{ "got_site_name",		GOT_SITE_NAME },
+		{ "got_site_owner",		GOT_SITE_OWNER },
+		{ "got_www_path",		GOT_WWW_PATH },
+	};
+	const struct keywords	*p;
+
+	p = bsearch(s, keywords, sizeof(keywords)/sizeof(keywords[0]),
+	    sizeof(keywords[0]), kw_cmp);
+
+	if (p)
+		return (p->k_val);
+	else
+		return (STRING);
+}
+
+#define MAXPUSHBACK	128
+
+u_char	*parsebuf;
+int	 parseindex;
+u_char	 pushback_buffer[MAXPUSHBACK];
+int	 pushback_index = 0;
+
+int
+lgetc(int quotec)
+{
+	int		c, next;
+
+	if (parsebuf) {
+		/* Read character from the parsebuffer instead of input. */
+		if (parseindex >= 0) {
+			c = parsebuf[parseindex++];
+			if (c != '\0')
+				return (c);
+			parsebuf = NULL;
+		} else
+			parseindex++;
+	}
+
+	if (pushback_index)
+		return (pushback_buffer[--pushback_index]);
+
+	if (quotec) {
+		if ((c = getc(file->stream)) == EOF) {
+			yyerror("reached end of file while parsing "
+			    "quoted string");
+			if (file == topfile || popfile() == EOF)
+				return (EOF);
+			return (quotec);
+		}
+		return (c);
+	}
+
+	while ((c = getc(file->stream)) == '\\') {
+		next = getc(file->stream);
+		if (next != '\n') {
+			c = next;
+			break;
+		}
+		yylval.lineno = file->lineno;
+		file->lineno++;
+	}
+
+	while (c == EOF) {
+		if (file == topfile || popfile() == EOF)
+			return (EOF);
+		c = getc(file->stream);
+	}
+	return (c);
+}
+
+int
+lungetc(int c)
+{
+	if (c == EOF)
+		return (EOF);
+	if (parsebuf) {
+		parseindex--;
+		if (parseindex >= 0)
+			return (c);
+	}
+	if (pushback_index < MAXPUSHBACK-1)
+		return (pushback_buffer[pushback_index++] = c);
+	else
+		return (EOF);
+}
+
+int
+findeol(void)
+{
+	int	c;
+
+	parsebuf = NULL;
+
+	/* skip to either EOF or the first real EOL */
+	while (1) {
+		if (pushback_index)
+			c = pushback_buffer[--pushback_index];
+		else
+			c = lgetc(0);
+		if (c == '\n') {
+			file->lineno++;
+			break;
+		}
+		if (c == EOF)
+			break;
+	}
+	return (ERROR);
+}
+
+int
+yylex(void)
+{
+	u_char	 buf[8096];
+	u_char	*p;
+	int	 quotec, next, c;
+	int	 token;
+
+	p = buf;
+	while ((c = lgetc(0)) == ' ' || c == '\t')
+		; /* nothing */
+
+	yylval.lineno = file->lineno;
+	if (c == '#')
+		while ((c = lgetc(0)) != '\n' && c != EOF)
+			; /* nothing */
+
+	switch (c) {
+	case '\'':
+	case '"':
+		quotec = c;
+		while (1) {
+			if ((c = lgetc(quotec)) == EOF)
+				return (0);
+			if (c == '\n') {
+				file->lineno++;
+				continue;
+			} else if (c == '\\') {
+				if ((next = lgetc(quotec)) == EOF)
+					return (0);
+				if (next == quotec || next == ' ' ||
+				    next == '\t')
+					c = next;
+				else if (next == '\n') {
+					file->lineno++;
+					continue;
+				} else
+					lungetc(next);
+			} else if (c == quotec) {
+				*p = '\0';
+				break;
+			} else if (c == '\0') {
+				yyerror("syntax error");
+				return (findeol());
+			}
+			if (p + 1 >= buf + sizeof(buf) - 1) {
+				yyerror("string too long");
+				return (findeol());
+			}
+			*p++ = c;
+		}
+		yylval.v.string = strdup(buf);
+		if (yylval.v.string == NULL)
+			errx(1, "yylex: strdup");
+		return (STRING);
+	}
+
+#define allowed_to_end_number(x) \
+	(isspace(x) || x == ')' || x ==',' || x == '/' || x == '}' || x == '=')
+
+	if (c == '-' || isdigit(c)) {
+		do {
+			*p++ = c;
+			if ((size_t)(p-buf) >= sizeof(buf)) {
+				yyerror("string too long");
+				return (findeol());
+			}
+		} while ((c = lgetc(0)) != EOF && isdigit(c));
+		lungetc(c);
+		if (p == buf + 1 && buf[0] == '-')
+			goto nodigits;
+		if (c == EOF || allowed_to_end_number(c)) {
+			const char *errstr = NULL;
+
+			*p = '\0';
+			yylval.v.number = strtonum(buf, LLONG_MIN,
+			    LLONG_MAX, &errstr);
+			if (errstr) {
+				yyerror("\"%s\" invalid number: %s",
+				    buf, errstr);
+				return (findeol());
+			}
+			return (NUMBER);
+		} else {
+nodigits:
+			while (p > buf + 1)
+				lungetc(*--p);
+			c = *--p;
+			if (c == '-')
+				return (c);
+		}
+	}
+
+#define allowed_in_string(x) \
+	(isalnum(x) || (ispunct(x) && x != '(' && x != ')' && \
+	x != '{' && x != '}' && x != '<' && x != '>' && \
+	x != '!' && x != '=' && x != '/' && x != '#' && \
+	x != ','))
+
+	if (isalnum(c) || c == ':' || c == '_' || c == '*') {
+		do {
+			*p++ = c;
+			if ((size_t)(p-buf) >= sizeof(buf)) {
+				yyerror("string too long");
+				return (findeol());
+			}
+		} while ((c = lgetc(0)) != EOF && (allowed_in_string(c)));
+		lungetc(c);
+		*p = '\0';
+		if ((token = lookup(buf)) == STRING)
+			if ((yylval.v.string = strdup(buf)) == NULL)
+				errx(1, "yylex: strdup");
+		return (token);
+	}
+	if (c == '\n') {
+		yylval.lineno = file->lineno;
+		file->lineno++;
+	}
+	if (c == EOF)
+		return (0);
+	return (c);
+}
+
+struct file *
+pushfile(const char *name)
+{
+	struct file	*nfile;
+
+	if ((nfile = calloc(1, sizeof(struct file))) == NULL) {
+		gerror = got_error(GOT_ERR_NO_SPACE);
+		return (NULL);
+	}
+	if ((nfile->name = strdup(name)) == NULL) {
+		gerror = got_error(GOT_ERR_NO_SPACE);
+		free(nfile);
+		return (NULL);
+	}
+	if ((nfile->stream = fopen(nfile->name, "r")) == NULL) {
+		gerror = got_error_from_errno2("parse_conf", nfile->name);
+		free(nfile->name);
+		free(nfile);
+		return (NULL);
+	}
+	nfile->lineno = 1;
+	TAILQ_INSERT_TAIL(&files, nfile, entry);
+	return (nfile);
+}
+
+int
+popfile(void)
+{
+	struct file	*prev;
+
+	if ((prev = TAILQ_PREV(file, files, entry)) != NULL)
+		prev->errors += file->errors;
+
+	TAILQ_REMOVE(&files, file, entry);
+	fclose(file->stream);
+	free(file->name);
+	free(file);
+	file = prev;
+	return (file ? 0 : EOF);
+}
+
+const struct got_error*
+parse_conf(const char *filename, struct gotweb_conf *gconf)
+{
+	static const struct got_error*	 error = NULL;
+
+	gw_conf = gconf;
+      	if ((gw_conf->got_repos_path = strdup(D_GOTPATH)) == NULL)
+		errx(1, "out of memory");
+      	if ((gw_conf->got_www_path = strdup(D_GOTWWW)) == NULL)
+		errx(1, "out of memory");
+      	if ((gw_conf->got_site_name = strdup(D_SITENAME)) == NULL)
+		errx(1, "out of memory");
+      	if ((gw_conf->got_site_owner = strdup(D_SITEOWNER)) == NULL)
+		errx(1, "out of memory");
+      	if ((gw_conf->got_site_link = strdup(D_SITELINK)) == NULL)
+		errx(1, "out of memory");
+      	if ((gw_conf->got_logo = strdup(D_GOTLOGO)) == NULL)
+		errx(1, "out of memory");
+      	if ((gw_conf->got_logo_url = strdup(D_GOTURL)) == NULL)
+		errx(1, "out of memory");
+	gw_conf->got_show_site_owner = D_SHOWSOWNER;
+	gw_conf->got_show_repo_owner = D_SHOWROWNER;
+	gw_conf->got_show_repo_age = D_SHOWAGE;
+	gw_conf->got_show_repo_description = D_SHOWDESC;
+	gw_conf->got_show_repo_cloneurl = D_SHOWURL;
+	gw_conf->got_max_repos = D_MAXREPO;
+	gw_conf->got_max_repos_display = D_MAXREPODISP;
+	gw_conf->got_max_commits_display = D_MAXCOMMITDISP;
+	if ((file = pushfile(filename)) == NULL) {
+		error = got_error_from_errno2("parse_conf", GOTWEB_CONF);
+		goto done;
+	}
+	topfile = file;
+
+	yyparse();
+	popfile();
+	if (gerror)
+		error = gerror;
+done:
+	return error;
+}
blob - da3924e2e27178c8c42cfa8411735cbb75eb814b
blob + 40fa9cc99ceca8d3d1fe298c1a381c45073a6736
--- libexec/Makefile.inc
+++ libexec/Makefile.inc
@@ -1,7 +1,15 @@
 .include "../Makefile.inc"
 
+.if ${MAKEWEB} == "Yes"
 realinstall:
+	if [ ! -d ${LIBEXEC_DIR}/. ]; then \
+		${INSTALL} -d -o root -g daemon -m 755 ${LIBEXEC_DIR}; \
+	fi
+	${INSTALL} ${INSTALL_COPY} -o root -g daemon -m 755 ${PROG} \
+	${LIBEXEC_DIR}/${PROG}
+.else
+realinstall:
 	${INSTALL} ${INSTALL_COPY} -o ${BINOWN} -g ${BINGRP} \
 	-m ${BINMODE} ${PROG} ${LIBEXECDIR}/${PROG}
-
+.endif
 NOMAN = Yes