Commit Diff


commit - d1f166363b86f21241be730505c1182508dc9d2c
commit + 2c251c14c84a96ddb3a968cd30fd889ba363f805
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"
@@ -25,4 +25,14 @@ dist: clean
 	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 + 8f9b2cf6f568f007f0e1d1cce187828cc667f290
--- Makefile.inc
+++ Makefile.inc
@@ -27,3 +27,18 @@ 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
+PROG_DIR =	${HTTPD_DIR}/${PROG}
+CGI_DIR =	${CHROOT_DIR}${GOTWEB_DIR}
+TMPL_DIR =	${CGI_DIR}/gw_tmpl
+.endif
blob - /dev/null
blob + 4ea3fdf6856195211f469ede4bc0b6c70d0ad0e1 (mode 644)
--- /dev/null
+++ gotweb/Makefile
@@ -0,0 +1,49 @@
+.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}.8 ${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 ${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 + 46af06afd32c72f4d45ab5cb8e6d8124afee0876 (mode 644)
--- /dev/null
+++ gotweb/README
@@ -0,0 +1,2 @@
+***THIS IS NOT FINISHED CODE***
+gotweb README
blob - /dev/null
blob + c1ca944f5834b9a6f65edf7f890a8968edafdf78 (mode 644)
--- /dev/null
+++ gotweb/TODO
@@ -0,0 +1,7 @@
+Complete templates.
+Complete stylesheets.
+Complete gw_funcs
+----
+Remember items
+	description
+	cloneurl
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 + 9b5a4fc668ccf2e333d79adfff2a4d9501126311 (mode 644)
--- /dev/null
+++ gotweb/files/cgi-bin/gw_tmpl/summary.tmpl
@@ -0,0 +1,30 @@
+<!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>
+			@@summary_shortlog@@
+			@@summary_tags@@
+			@@summary_heads@@
+		</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)
Binary files /dev/null and gotweb/files/htdocs/gotweb/android-chrome-192x192.png differ
blob - /dev/null
blob + 653a1510ce933f7fe9fbab2fcd171f04fa0b24cc (mode 644)
Binary files /dev/null and gotweb/files/htdocs/gotweb/android-chrome-384x384.png differ
blob - /dev/null
blob + 460aa1299f8e9f37773618bcab2619794416fb49 (mode 644)
Binary files /dev/null and gotweb/files/htdocs/gotweb/apple-touch-icon.png differ
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)
Binary files /dev/null and gotweb/files/htdocs/gotweb/favicon-16x16.png differ
blob - /dev/null
blob + 0ceea8c0eabe73e8d12cf106d73c34abb1999cb2 (mode 644)
Binary files /dev/null and gotweb/files/htdocs/gotweb/favicon-32x32.png differ
blob - /dev/null
blob + ee414573031ea5b310539196d2530a1e52d49b64 (mode 644)
Binary files /dev/null and gotweb/files/htdocs/gotweb/favicon.ico differ
blob - /dev/null
blob + 33933f80ee46217039804bc96672ede12b352b93 (mode 644)
Binary files /dev/null and gotweb/files/htdocs/gotweb/got.png differ
blob - /dev/null
blob + 97ace786464b193baf1cd51e54016aea3016e62f (mode 644)
Binary files /dev/null and gotweb/files/htdocs/gotweb/got_large.png differ
blob - /dev/null
blob + 133211bd0a02daf443d1c7c66ab17778f60f23e7 (mode 644)
--- /dev/null
+++ gotweb/files/htdocs/gotweb/gotweb.css
@@ -0,0 +1,357 @@
+/*
+ * Copyright (c) 2019 Jerome Kasper <neon.king.fr@gmail.com>
+ * Copyright (c) 2019 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;
+}
+#dotted_line {
+	clear: left;
+	float: left;
+	width: 100%;
+	border-top: 1px dotted #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;
+}
+#np_navs {
+	padding-left: 10px;
+	padding-top: 2px;
+	padding-bottom: 2px;
+}
+#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;
+}
+
+/* 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;
+}
+
+/* summary.tmpl */
+
+#summary_wrapper {
+	clear: left;
+	float: left;
+	width: 100%;
+	background-color: Khaki;
+}
+#summary_description_title {
+	clear: left;
+	float: left;
+	width: 6.5em;
+	padding-left: 10px;
+	padding-top: 5px;
+	padding-bottom: 5px;
+}
+#summary_description {
+	float: left;
+	width: 72%;
+	padding-top: 5px;
+	padding-bottom: 5px;
+}
+#summary_repo_owner_title {
+	clear: left;
+	float: left;
+	width: 6.5em;
+	padding-left: 10px;
+	padding-top: 5px;
+	padding-bottom: 5px;
+}
+#summary_repo_owner {
+	float: left;
+	width: 72%;
+	padding-top: 5px;
+	padding-bottom: 5px;
+}
+#summary_last_change_title {
+	clear: left;
+	float: left;
+	width: 6.5em;
+	padding-left: 10px;
+	padding-top: 5px;
+	padding-bottom: 5px;
+}
+#summary_last_change {
+	float: left;
+	width: 72%;
+	padding-top: 5px;
+	padding-bottom: 5px;
+}
+#summary_cloneurl_title {
+	clear: left;
+	float: left;
+	width: 6.5em;
+	padding-left: 10px;
+	padding-top: 5px;
+	padding-bottom: 5px;
+}
+#summary_cloneurl {
+	float: left;
+	width: 72%;
+	padding-top: 5px;
+	padding-bottom: 5px;
+	overflow: auto;
+}
+#summary_shortlog_title_wrapper {
+	clear: left;
+	float: left;
+	width: 100%;
+	background-color: LightSlateGray;
+	color: #ffffff;
+}
+#summary_shortlog_title {
+	padding-left: 10px;
+	padding-top: 5px;
+	padding-bottom: 5px;
+}
+#summary_shortlog_content_wrapper {
+	clear: left;
+	float: left;
+	width: 100%;
+}
+#summary_shortlog_content {
+	padding-left: 10px;
+	padding-top: 5px;
+	padding-bottom: 5px;
+}
+#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_wrapper {
+	clear: left;
+	float: left;
+	width: 100%;
+}
+#summary_tags_content {
+	padding-left: 10px;
+	padding-top: 5px;
+	padding-bottom: 5px;
+}
+#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_wrapper {
+	clear: left;
+	float: left;
+	width: 100%;
+}
+#summary_heads_content {
+	padding-left: 10px;
+	padding-top: 5px;
+	padding-bottom: 5px;
+}
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)
Binary files /dev/null and gotweb/files/htdocs/gotweb/mstile-150x150.png differ
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 + e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 (mode 644)
blob - /dev/null
blob + 3eac38fa7a0a786b60a6c550fe90389aa8e71e6f (mode 644)
--- /dev/null
+++ gotweb/gotweb.c
@@ -0,0 +1,1175 @@
+/*
+ * Copyright (c) 2019 Tracey Emery <tracey@traceyemery.net>
+ * Copyright (c) 2018, 2019 Stefan Sperling <stsp@openbsd.org>
+ * Copyright (c) 2014, 2015, 2017 Kristaps Dzonsons <kristaps@bsd.lv>
+ *
+ * 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 <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 "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 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			*action_name;
+	unsigned int		 action;
+	unsigned int		 page;
+	unsigned int		 repos_total;
+	enum kmime               mime;
+};
+
+enum gw_key {
+	KEY_PATH,
+	KEY_ACTION,
+	KEY_COMMIT_ID,
+	KEY_FILE,
+	KEY_PAGE,
+	KEY__MAX
+};
+
+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_DESCRIPTION,
+	TEMPL_CONTENT,
+	TEMPL_REPO_OWNER,
+	TEMPL_REPO_AGE,
+	TEMPL_CLONEURL,
+	TEMPL_SUMMARY_SHORTLOG,
+	TEMPL_SUMMARY_TAGS,
+	TEMPL_SUMMARY_HEADS,
+	TEMPL__MAX
+};
+
+static const char *const templs[TEMPL__MAX] = {
+	"head",
+	"header",
+	"sitepath",
+	"siteowner",
+	"title",
+	"search",
+	"description",
+	"content",
+	"repo_owner",
+	"repo_age",
+	"cloneurl",
+	"summary_shortlog",
+	"summary_tags",
+	"summary_heads",
+};
+
+static const struct kvalid gw_keys[KEY__MAX] = {
+	{ kvalid_stringne,	"path" },
+	{ kvalid_stringne,	"action" },
+	{ kvalid_stringne,	"commit" },
+	{ kvalid_stringne,	"file" },
+	{ kvalid_int,		"page" },
+};
+
+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_repo_age(struct trans *,
+				    char *, char *);
+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 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*	 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*	 gw_blame(struct trans *);
+static const struct got_error*	 gw_blob(struct trans *);
+static const struct got_error*	 gw_blob_diff(struct trans *);
+static const struct got_error*	 gw_commit(struct trans *);
+static const struct got_error*	 gw_commit_diff(struct trans *);
+static const struct got_error*	 gw_history(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_shortlog(struct trans *);
+static const struct got_error*	 gw_snapshot(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_BLOB,
+	GW_BLOBDIFF,
+	GW_COMMIT,
+	GW_COMMITDIFF,
+	GW_ERR,
+	GW_HISTORY,
+	GW_INDEX,
+	GW_LOG,
+	GW_RAW,
+	GW_SHORTLOG,
+	GW_SNAPSHOT,
+	GW_SUMMARY,
+	GW_TREE
+};
+
+static struct gw_query_action gw_query_funcs[] = {
+	{ GW_BLAME,	 "blame",	gw_blame,	"gw_tmpl/index.tmpl" },
+	{ GW_BLOB,	 "blob",	gw_blob,	"gw_tmpl/index.tmpl" },
+	{ GW_BLOBDIFF,	 "blobdiff",	gw_blob_diff,	"gw_tmpl/index.tmpl" },
+	{ GW_COMMIT,	 "commit",	gw_commit,	"gw_tmpl/index.tmpl" },
+	{ GW_COMMITDIFF, "commit_diff",	gw_commit_diff,	"gw_tmpl/index.tmpl" },
+	{ GW_ERR,	 NULL,		NULL,		"gw_tmpl/err.tmpl" },
+	{ GW_HISTORY,	 "history",	gw_history,	"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_SHORTLOG,	 "shortlog",	gw_shortlog,	"gw_tmpl/index.tmpl" },
+	{ GW_SNAPSHOT,	 "snapshot",	gw_snapshot,	"gw_tmpl/index.tmpl" },
+	{ GW_SUMMARY,	 "summary",	NULL,		"gw_tmpl/summary.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 *
+gw_blame(struct trans *gw_trans)
+{
+	const struct got_error *error = NULL;
+
+	return error;
+}
+
+static const struct got_error *
+gw_blob(struct trans *gw_trans)
+{
+	const struct got_error *error = NULL;
+
+	return error;
+}
+
+static const struct got_error *
+gw_blob_diff(struct trans *gw_trans)
+{
+	const struct got_error *error = NULL;
+
+	return error;
+}
+
+static const struct got_error *
+gw_commit(struct trans *gw_trans)
+{
+	const struct got_error *error = NULL;
+
+	return error;
+}
+
+static const struct got_error *
+gw_commit_diff(struct trans *gw_trans)
+{
+	const struct got_error *error = NULL;
+
+	return error;
+}
+
+static const struct got_error *
+gw_history(struct trans *gw_trans)
+{
+	const struct got_error *error = NULL;
+
+	return error;
+}
+
+static const struct got_error *
+gw_index(struct trans *gw_trans)
+{
+	const struct got_error *error = NULL;
+	struct gw_dir *dir = NULL;
+	char *html, *navs, *next, *prev;
+	unsigned int  prev_disp = 0, next_disp = 1, dir_c = 0;
+
+	error = gw_load_got_paths(gw_trans);
+	if (error && error->code != GOT_ERR_OK)
+		return error;
+
+	khttp_puts(gw_trans->gw_req, index_projects_header);
+
+	TAILQ_FOREACH(dir, &gw_trans->gw_dirs, entry)
+		dir_c++;
+
+	TAILQ_FOREACH(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, dir->name, dir->name, dir->name,
+		    dir->name)) == -1)
+			return got_error_from_errno("asprintf");
+
+		if ((asprintf(&html, index_projects, dir->name, dir->name,
+		    dir->description, dir->owner, 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;
+
+	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_shortlog(struct trans *gw_trans)
+{
+	const struct got_error *error = NULL;
+
+	return error;
+}
+
+static const struct got_error *
+gw_snapshot(struct trans *gw_trans)
+{
+	const struct got_error *error = NULL;
+
+	return error;
+}
+
+static const struct got_error *
+gw_tree(struct trans *gw_trans)
+{
+	const struct got_error *error = NULL;
+
+	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;
+	bool opened = false;
+
+	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 = true;
+		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 = true;
+		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, NULL);
+	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;
+
+	if (pledge("stdio rpath proc exec sendfd unveil", NULL) == -1) {
+		error = got_error_from_errno("pledge");
+		return error;
+	}
+
+	error = apply_unveil(gw_trans->gw_conf->got_repos_path, NULL);
+	if (error)
+		return error;
+
+	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");
+
+ 		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");
+
+		/* 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_FILE]))
+			if ((asprintf(&gw_trans->repo_file, "%s",
+			    p->parsed.s)) == -1)
+				return got_error_from_errno("asprintf");
+
+		if (action == NULL) {
+			error = got_error_from_errno("invalid action");
+			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 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;
+	char *description, *description_h;
+	char *repo_owner, *repo_owner_h;
+	char *repo_age, *repo_age_h;
+	char *cloneurl, *cloneurl_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_DESCRIPTION):
+		if (gw_trans->gw_conf->got_show_repo_description) {
+			description = gw_html_escape(
+			    gw_get_repo_description(gw_trans,
+			    gw_trans->repo_path));
+			if (description != NULL &&
+			    (strcmp(description, "") != 0)) {
+				if ((asprintf(&description_h,
+				    summary_description, description)) == -1)
+					return 0;
+
+				khttp_puts(gw_trans->gw_req, description_h);
+				free(description);
+				free(description_h);
+			}
+		}
+		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;
+	case(TEMPL_REPO_OWNER):
+		if (gw_trans->gw_conf->got_show_repo_owner) {
+			repo_owner = gw_html_escape(gw_get_repo_owner(gw_trans,
+			    gw_trans->repo_path));
+			if ((asprintf(&repo_owner_h, summary_repo_owner,
+			    repo_owner)) == -1)
+				return 0;
+
+			if (repo_owner != NULL &&
+			    (strcmp(repo_owner, "") != 0)) {
+				khttp_puts(gw_trans->gw_req, repo_owner_h);
+			}
+
+			free(repo_owner_h);
+		}
+		break;
+	case(TEMPL_REPO_AGE):
+		if (gw_trans->gw_conf->got_show_repo_age) {
+			repo_age = gw_get_repo_age(gw_trans,
+			    gw_trans->repo_path, NULL);
+			if (repo_age != NULL) {
+				if ((asprintf(&repo_age_h, summary_last_change,
+			    	    repo_age)) == -1)
+				return 0;
+				khttp_puts(gw_trans->gw_req, repo_age_h);
+				free(repo_age);
+				free(repo_age_h);
+			}
+		}
+		break;
+	case(TEMPL_CLONEURL):
+		if (gw_trans->gw_conf->got_show_repo_cloneurl) {
+			cloneurl = gw_html_escape(gw_get_clone_url(gw_trans,
+			    gw_trans->repo_path));
+			if (cloneurl != NULL) {
+				if ((asprintf(&cloneurl_h,
+				    summary_cloneurl, cloneurl)) == -1)
+					return 0;
+
+				khttp_puts(gw_trans->gw_req, cloneurl_h);
+				free(cloneurl);
+				free(cloneurl_h);
+			}
+
+		}
+		break;
+	case(TEMPL_SUMMARY_SHORTLOG):
+		khttp_puts(gw_trans->gw_req, summary_shortlog);
+		break;
+	case(TEMPL_SUMMARY_TAGS):
+		khttp_puts(gw_trans->gw_req, summary_tags);
+		break;
+	case(TEMPL_SUMMARY_HEADS):
+		khttp_puts(gw_trans->gw_req, summary_heads);
+		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_repo_age(struct trans *gw_trans, char *dir, char *repo_ref)
+{
+	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;
+	time_t committer_time = 0, cmp_time = 0, diff_time;
+	char *repo_age = NULL, *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";
+
+	SIMPLEQ_INIT(&refs);
+	if (gw_trans->gw_conf->got_show_repo_age == false) {
+		asprintf(&repo_age, "");
+		return repo_age;
+	}
+	error = got_repo_open(&repo, dir, NULL);
+	if (error != NULL)
+		goto err;
+
+	error = got_ref_list(&refs, repo, "refs/heads", got_ref_cmp_by_name,
+	    NULL);
+	if (error != NULL)
+		goto err;
+
+	const char *refname;
+	SIMPLEQ_FOREACH(re, &refs, entry) {
+		refname = got_ref_get_name(re->ref);
+		error = got_ref_open(&head_ref, repo, refname, 0);
+		if (error != NULL)
+			goto err;
+
+		error = got_ref_resolve(&id, repo, head_ref);
+		got_ref_close(head_ref);
+		if (error != NULL)
+			goto err;
+
+		error = got_object_open_as_commit(&commit, repo, id);
+		if (error != NULL)
+			goto err;
+
+		committer_time =
+		    got_object_commit_get_committer_time(commit);
+		if (repo_ref != NULL && (strcmp(refname, repo_ref) == 0)) {
+			cmp_time = 0;
+			break;
+		}
+
+		if (committer_time > cmp_time)
+			cmp_time = committer_time;
+	}
+
+	if (repo_ref != NULL && (strcmp(refname, repo_ref) != 0)) {
+		asprintf(&repo_age, "");
+		goto noref;
+	}
+
+	if (cmp_time != 0)
+		committer_time = cmp_time;
+
+	diff_time = time(NULL) - committer_time;
+	if (diff_time > 60 * 60 * 24 * 365 * 2)
+		asprintf(&repo_age, "%lld %s", (diff_time / 60 / 60 / 24 / 365),
+		    years);
+	else if (diff_time > 60 * 60 * 24 * (365 / 12) * 2)
+		asprintf(&repo_age, "%lld %s", (diff_time / 60 / 60 / 24 /
+		    (365 / 12)), months);
+	else if (diff_time > 60 * 60 * 24 * 7 * 2)
+		asprintf(&repo_age, "%lld %s", (diff_time / 60 / 60 / 24 / 7),
+		    weeks);
+	else if (diff_time > 60 * 60 * 24 * 2)
+		asprintf(&repo_age, "%lld %s", (diff_time / 60 / 60 / 24),
+		    days);
+	else if (diff_time > 60 * 60 * 2)
+		asprintf(&repo_age, "%lld %s", (diff_time / 60 / 60), hours);
+	else if (diff_time > 60 * 2)
+		asprintf(&repo_age, "%lld %s", (diff_time / 60), minutes);
+	else if (diff_time > 2)
+		asprintf(&repo_age, "%lld %s", diff_time, seconds);
+	else
+		asprintf(&repo_age, "%s", now);
+
+noref:
+	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_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(BUFFER_SIZE, sizeof(char *))) == NULL)
+		goto err;
+
+	while ((fgets(buf, BUFFER_SIZE, f)) != NULL) {
+		if ((pos = strstr(buf, gotweb)) != NULL)
+			break;
+
+		if ((pos = strstr(buf, gitweb)) != NULL)
+			break;
+	}
+
+	if (pos == NULL)
+		goto err;
+
+	do {
+		fgets(buf, BUFFER_SIZE, 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_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 *
+gw_html_escape(const char *html)
+{
+	char *escaped_str = NULL, *buf;
+	char c[1];
+	size_t sz, i;
+
+	if ((buf = calloc(BUFFER_SIZE, sizeof(char *))) == NULL)
+		return NULL;
+
+	if (html == NULL)
+		return NULL;
+	else
+		if ((sz = strlen(html)) == 0)
+			return NULL;
+
+	/* only work with BUFFER_SIZE */
+	if (BUFFER_SIZE < sz)
+		sz = BUFFER_SIZE;
+
+	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";
+	bool gw_malloc = true;
+
+	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__MAX,
+	    &page, 1, 0))
+		errx(1, "khttp_parse");
+
+	if ((gw_trans->gw_conf =
+	    malloc(sizeof(struct gotweb_conf))) == NULL) {
+		gw_malloc = false;
+		error =  got_error_from_errno("malloc");
+		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->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);
+
+		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 + e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 (mode 644)
blob - /dev/null
blob + 3ed34580241af652fe0f01829f678efa82cfd317 (mode 644)
--- /dev/null
+++ gotweb/gotweb.h
@@ -0,0 +1,73 @@
+/*
+ * Copyright (c) 2019 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_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 + a058b737cb68fb43826c8186cc91f652fbd6e046 (mode 644)
--- /dev/null
+++ gotweb/gotweb_ui.h
@@ -0,0 +1,170 @@
+/*
+ * Copyright (c) 2019 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.
+ */
+
+/*
+ * header nav
+ *
+ * ***index
+ * search
+ * Projects
+ * 	Project|Description|Owner|Last Commit
+ * 	DIV (summary|shortlog|log|tree)
+ * ***summary
+ * repo navs | search
+ * repo description
+ * 	description
+ * 	owner
+ * 	last commit
+ * 	URL
+ * shortlog
+ * 	Date|committer|commit description (commit|commitdiff|tree|snapshot)
+ * heads
+ * 	create date | head (shortlog|log|tree)
+ *
+ *
+ *
+ * footer
+ */
+
+#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_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/>";
+
+/* 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='np_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=shortlog'>shortlog</a> | " \
+	"<a href='?path=%s&action=log'>log</a> | " \
+	"<a href='?path=%s&action=tree'>tree</a>";
+
+/* summary.tmpl */
+
+char *summary_description =
+	"<div id='summary_description_title'>Description: </div>" \
+	"<div id='summary_description'>%s</div>";
+
+char *summary_repo_owner =
+	"<div id='summary_repo_owner_title'>Owner: </div>" \
+	"<div id='summary_repo_owner'>%s</div>";
+
+char *summary_last_change =
+	"<div id='summary_last_change_title'>Last Change: </div>" \
+	"<div id='summary_last_change'>%s</div>";
+
+char *summary_cloneurl =
+	"<div id='summary_cloneurl_title'>Clone URL: </div>" \
+	"<div id='summary_cloneurl'>%s</div>";
+
+char *summary_shortlog =
+	"<div id='summary_shortlog_title_wrapper'>" \
+	"<div id='summary_shortlog_title'>Shortlog</div></div>" \
+	"<div id='summary_shortlog_content_wrapper'>" \
+	"<div id='summary_shortlog_content'>%s</div>" \
+	"</div>";
+
+char *summary_tags =
+	"<div id='summary_tags_title_wrapper'>" \
+	"<div id='summary_tags_title'>Tags</div></div>" \
+	"<div id='summary_tags_content_wrapper'>" \
+	"<div id='summary_tags_content'>%s</div>" \
+	"</div>";
+
+char *summary_heads =
+	"<div id='summary_heads_title_wrapper'>" \
+	"<div id='summary_heads_title'>Heads</div></div>" \
+	"<div id='summary_heads_content_wrapper'>" \
+	"<div id='summary_heads_content'>%s</div>" \
+	"</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