greenhouse

greenhouse

minimal software forge

git clone git@git.lostduk.com:greenhouse.git

refactor(*): drop env overrides, and add man pages

commit117bc277b6bf294d849d75217fed1969794385e0
parent43b6fb5aa340705bc135211e7cd71adc8e156cf2
authorlostduk <hi@lostduk.com>
date2026-10-17 23:43 +0700
hedge/Makefile6++
hedge/config.mk3+
hedge/hedge.189+++++++++++++++++++++++++++
hedge/hedge.c21+------
herbarium/Makefile6++
herbarium/config.def.h1-
herbarium/config.mk3+
herbarium/herbarium.1135++++++++++++++++++++++++++++++++++++++++
herbarium/herbarium.c24--------
petal/Makefile6++
petal/config.def.h1-
petal/config.mk3+
petal/petal.1136++++++++++++++++++++++++++++++++++++++++
petal/petal.c11----
pollen/pollen.11+
pollen/pollen.c5+-

16 files changed, -60/+391

hedge/Makefile

@@ -23,8 +23,12 @@ install: all 	mkdir -p ${DESTDIR}${PREFIX}/bin 	cp -f hedge ${DESTDIR}${PREFIX}/bin 	chmod 755 ${DESTDIR}${PREFIX}/bin/hedge+	mkdir -p ${DESTDIR}${MANPREFIX}/man1+	cp -f hedge.1 ${DESTDIR}${MANPREFIX}/man1+	chmod 644 ${DESTDIR}${MANPREFIX}/man1/hedge.1  uninstall:-	rm -f ${DESTDIR}${PREFIX}/bin/hedge+	rm -f ${DESTDIR}${PREFIX}/bin/hedge\+		${DESTDIR}${MANPREFIX}/man1/hedge.1  .PHONY: all clean install uninstall

hedge/config.mk

@@ -1,4 +1,5 @@-PREFIX = /usr/local+PREFIX    = /usr/local+MANPREFIX = ${PREFIX}/share/man  CPPFLAGS = -D_XOPEN_SOURCE=700 CFLAGS   = -std=c99 -pedantic -Wall -Wextra -Os ${CPPFLAGS}

hedge/hedge.1

@@ -0,0 +1,89 @@+.TH HEDGE 1 hedge+.SH NAME+hedge \- git access control for ssh+.SH SYNOPSIS+.B hedge+.I id+.SH DESCRIPTION+.B hedge+is run as the forced command of an ssh key and lets the identity+.I id+fetch from or push to the repositories the+.B acl+file grants it.+It reads the git command from+.BR SSH_ORIGINAL_COMMAND ,+checks it and hands it to+.B gitshell+as+.IR verb " '" root / repo .git'.+Any other command, including an interactive login, is refused.+.P+.B git-upload-pack+and+.B git-upload-archive+need read access,+.B git-receive-pack+needs write access.+Write access implies read access.+.P+The repository path may be quoted, may start with a slash and may end+with a slash or+.BR .git .+What remains must consist of+.BR / -separated+segments matching+.BR ^[A-Za-z0-9_][A-Za-z0-9._-]*$ ,+so nested repositories such as+.B group/repo+are allowed.+.SH FILES+.TP+.B acl+One grant per line:+.IP+.nf+.IR repo " " read | write " " id+.fi+.IP+Fields are separated by a single space and+.I repo+is written without+.BR .git .+There are no comments or wildcards.+.TP+.IR root / repo .git+The bare repositories.+.SH EXIT STATUS+.TP+.B 1+Access was denied or+.B gitshell+could not be run.+.P+Otherwise the exit status is that of+.BR git-shell .+.SH EXAMPLES+.B acl:+.P+.nf+greenhouse write lostduk+greenhouse read alice+.fi+.P+.BR authorized_keys :+.P+.nf+command="/usr/local/bin/hedge lostduk",restrict ssh-ed25519 AAAA...+.fi+.P+Clone with+.IR "git clone git@host:greenhouse" .+.SH CUSTOMIZATION+.B hedge+is configured by editing+.I config.h+and recompiling.+.SH SEE ALSO+.BR git-shell (1),+.BR sshd (8)

hedge/hedge.c

@@ -108,7 +108,7 @@ trim(char *path) void usage(void) {-	fputs("usage: hedge [-a acl] [-r root] id\n", stderr);+	fputs("usage: hedge id\n", stderr); 	exit(1); } @@ -145,23 +145,10 @@ main(int argc, char *argv[]) 	const char *s; 	char *path; 	size_t i;-	int c;--	while ((c = getopt(argc, argv, "a:r:")) != -1) {-		switch (c) {-		case 'a':-			acl = optarg;-			break;-		case 'r':-			root = optarg;-			break;-		default:-			usage();-		}-	}-	if (argc - optind != 1)++	if (argc != 2) 		usage();-	id = argv[optind];+	id = argv[1];  	if (!(s = getenv("SSH_ORIGINAL_COMMAND"))) 		s = "";

herbarium/Makefile

@@ -27,10 +27,14 @@ install: all 	cp -f style.css icon.svg ${DESTDIR}${PREFIX}/share/herbarium 	chmod 644 ${DESTDIR}${PREFIX}/share/herbarium/style.css\ 		${DESTDIR}${PREFIX}/share/herbarium/icon.svg+	mkdir -p ${DESTDIR}${MANPREFIX}/man1+	cp -f herbarium.1 ${DESTDIR}${MANPREFIX}/man1+	chmod 644 ${DESTDIR}${MANPREFIX}/man1/herbarium.1  uninstall: 	rm -f ${DESTDIR}${PREFIX}/bin/herbarium\ 		${DESTDIR}${PREFIX}/share/herbarium/style.css\-		${DESTDIR}${PREFIX}/share/herbarium/icon.svg+		${DESTDIR}${PREFIX}/share/herbarium/icon.svg\+		${DESTDIR}${MANPREFIX}/man1/herbarium.1  .PHONY: all clean install uninstall

herbarium/config.def.h

@@ -9,7 +9,6 @@ static const int maxmsgs   = 20000; static const int maxhdr    = 64 * 1024; static const int maxbody   = 512 * 1024; static const int maxdepth  = 8;-static const int maxcpu    = 10;  static const Service services[] = { 	{ "git",   "https://git.lostduk.com/" },

herbarium/config.mk

@@ -1,4 +1,5 @@-PREFIX = /usr/local+PREFIX    = /usr/local+MANPREFIX = ${PREFIX}/share/man  CPPFLAGS = -D_XOPEN_SOURCE=700 CFLAGS   = -std=c99 -pedantic -Wall -Wextra -Os ${CPPFLAGS}

herbarium/herbarium.1

@@ -0,0 +1,135 @@+.TH HERBARIUM 1 herbarium+.SH NAME+herbarium \- mailing list archive web interface+.SH SYNOPSIS+.B herbarium+.SH DESCRIPTION+.B herbarium+is a CGI program that shows the archives+.BR pollen (1)+keeps in+.IR root / list /archive.+Only lists whose name matches+.B ^[a-z0-9][a-z0-9-]*$+and that contain a+.B public+file are shown.+.P+Each request reads the headers of the newest+.B maxmsgs+messages, by file modification time, from the+.B new+and+.B cur+directories of the archive.+Messages are threaded by+.B In-Reply-To+and then+.BR References .+If several messages share a+.BR Message-ID ,+the oldest one is kept.+A missing or implausible+.B Date+is replaced by the file modification time.+.P+Pages are selected by+.BR PATH_INFO ,+where+.I id+is a+.B Message-ID+without angle brackets:+.TP+.B /+Index of all public lists with description, address, number of threads+and age of the last message.+.TP+.IB /list /+Threads, most recently active first,+.B perpage+per page; older pages are reached with+.BI ?ofs= n .+.TP+.IB /list / id /+The thread started by+.IR id .+Any other message redirects to its thread.+Plain text and patch parts are shown, decoded to UTF-8, up to+.B maxbody+bytes per message; HTML parts and attachments are only named.+Multipart nesting is followed up to+.B maxdepth+levels.+.TP+.IB /list / id /raw+The message as archived, served as+.BR text/plain .+.TP+.IB /list / id /t.mbox+The whole thread as an mboxrd file.+.P+Every response carries a Content-Security-Policy that allows only the+stylesheet and icon from the same origin.+.SH ENVIRONMENT+.TP+.B PATH_INFO+The page.+.TP+.B SCRIPT_NAME+URL prefix for links.+.TP+.B QUERY_STRING+.BI ofs= n+for the thread list.+.SH FILES+.TP+.IR root / list /public+Present if the archive is public.+.TP+.IR root / list /description+One line describing the list.+.TP+.IR root / list /archive/+Maildir written by+.BR pollen (1).+.B herbarium+only needs read access, for example through pollen's group.+.TP+.IR PREFIX /share/herbarium/style.css ", " icon.svg+Stylesheet and icon, to be served at+.B css+and+.BR icon .+.SH EXAMPLES+.B herbarium+sets no resource limits itself.+Run it through a wrapper:+.P+.nf+#!/bin/sh+ulimit -t 10+exec /usr/local/bin/herbarium+.fi+.P+.BR nginx :+.P+.nf+location = /style.css { alias /usr/local/share/herbarium/style.css; }+location = /icon.svg { alias /usr/local/share/herbarium/icon.svg; }+location / {+	include fastcgi_params;+	fastcgi_param SCRIPT_FILENAME /usr/local/bin/herbarium-cgi;+	fastcgi_param SCRIPT_NAME "";+	fastcgi_param PATH_INFO $uri;+	fastcgi_pass unix:/run/fcgiwrap.sock;+}+.fi+.SH CUSTOMIZATION+.B herbarium+is configured by editing+.I config.h+and recompiling.+.SH SEE ALSO+.BR pollen (1),+.BR petal (1)

herbarium/herbarium.c

@@ -12,7 +12,6 @@ #include <strings.h> #include <time.h> #include <unistd.h>-#include <sys/resource.h> #include <sys/stat.h>  /* macros */@@ -1850,7 +1849,6 @@ tou8(const char *cs, const char *s, size_t n, Buf *out) 	char name[64], tmp[1024], *in, *o; 	size_t i, il, ol, r; 	iconv_t cd;-	unsigned char c;  	for (i = 0; cs[i] && cs[i] != '*'; i++) { 		if (i + 1 >= sizeof(name) || !(isalnum((unsigned char)cs[i])@@ -1864,18 +1862,6 @@ tou8(const char *cs, const char *s, size_t n, Buf *out) 		bufadd(out, s, n); 		return 0; 	}-	if (!strcasecmp(name, "iso-8859-1") || !strcasecmp(name, "latin1")) {-		for (i = 0; i < n; i++) {-			c = (unsigned char)s[i];-			if (c < 0x80) {-				bufc(out, c);-			} else {-				bufc(out, 0xc0 | c >> 6);-				bufc(out, 0x80 | (c & 0x3f));-			}-		}-		return 0;-	} 	if ((cd = iconv_open("UTF-8", name)) == (iconv_t)-1) 		return -1; 	in = (char *)s;@@ -1979,15 +1965,7 @@ main(void) { 	char path[2048]; 	const char *q;-	struct rlimit rl;--	rl.rlim_cur = rl.rlim_max = maxcpu;-	setrlimit(RLIMIT_CPU, &rl);-	root = envor("HERBARIUM_ROOT", root);-	domain = envor("HERBARIUM_DOMAIN", domain);-	css = envor("HERBARIUM_CSS", css);-	icon = envor("HERBARIUM_ICON", icon);-	title = envor("HERBARIUM_TITLE", title);+ 	base = envor("SCRIPT_NAME", ""); 	q = envor("QUERY_STRING", ""); 	if (!strncmp(q, "ofs=", 4) && (ofs = strtol(q + 4, NULL, 10)) < 0)

petal/Makefile

@@ -27,10 +27,14 @@ install: all 	cp -f style.css icon.svg ${DESTDIR}${PREFIX}/share/petal 	chmod 644 ${DESTDIR}${PREFIX}/share/petal/style.css\ 		${DESTDIR}${PREFIX}/share/petal/icon.svg+	mkdir -p ${DESTDIR}${MANPREFIX}/man1+	cp -f petal.1 ${DESTDIR}${MANPREFIX}/man1+	chmod 644 ${DESTDIR}${MANPREFIX}/man1/petal.1  uninstall: 	rm -f ${DESTDIR}${PREFIX}/bin/petal\ 		${DESTDIR}${PREFIX}/share/petal/style.css\-		${DESTDIR}${PREFIX}/share/petal/icon.svg+		${DESTDIR}${PREFIX}/share/petal/icon.svg\+		${DESTDIR}${MANPREFIX}/man1/petal.1  .PHONY: all clean install uninstall

petal/config.def.h

@@ -9,7 +9,6 @@ static const int maxdepth  = 8; static const int barwidth  = 40; static const int maxblob   = 512 * 1024; static const int maxdiff   = 10000;-static const int maxcpu    = 10;  static const Service services[] = { 	{ "git",   "https://git.lostduk.com/" },

petal/config.mk

@@ -1,4 +1,5 @@-PREFIX = /usr/local+PREFIX    = /usr/local+MANPREFIX = ${PREFIX}/share/man  GIT2INC = /usr/local/include GIT2LIB = /usr/local/lib

petal/petal.1

@@ -0,0 +1,136 @@+.TH PETAL 1 petal+.SH NAME+petal \- git web interface+.SH SYNOPSIS+.B petal+.SH DESCRIPTION+.B petal+is a CGI program that shows the bare git repositories in+.IR root .+A directory+.IR root / name .git+is a repository named+.IR name ;+names may be nested, such as+.BR group/repo ,+up to+.B maxdepth+levels.+Only repositories containing+.B git-daemon-export-ok+are shown.+.P+Pages are selected by+.BR PATH_INFO .+The longest prefix that names a repository is the repository, the rest+is the page.+A+.I ref+is a branch, a tag or a commit id of 4 to 40 hex digits; the longest+prefix of the rest that resolves to one is taken.+Without a+.IR ref ,+.B HEAD+is used.+.TP+.B /+Index of all repositories with description, owner and age of the last+commit.+.TP+.IR /repo ", " /repo /log[/ ref ]+Log,+.B loglimit+commits per page; older pages are reached with+.BI ?ofs= n .+.TP+.IR /repo /tree[/ ref [/ path ]]+Directory listing or file view with line anchors.+Files larger than+.B maxblob+bytes are not shown.+.TP+.IR /repo /raw/ ref / path+The file as is, served as+.B text/plain+or, if binary,+.BR application/octet-stream .+.TP+.IR /repo /commit/ id+Commit metadata, diffstat and diff, cut after+.B maxdiff+lines.+.I id+must be a commit id of 4 to 40 hex digits.+.TP+.IR /repo /refs+Branches and tags.+.P+Every response carries a Content-Security-Policy that allows only the+stylesheet and icon from the same origin.+.SH ENVIRONMENT+.TP+.B PATH_INFO+The page.+.TP+.B SCRIPT_NAME+URL prefix for links.+.TP+.B QUERY_STRING+.BI ofs= n+for the log.+.TP+.B HOME+libgit2 refuses repositories owned by another user unless they are+listed as+.B safe.directory+in+.IR $HOME /.gitconfig.+.SH FILES+.TP+.IR root / repo .git/description+One line describing the repository.+.TP+.IR root / repo .git/owner+One line naming the owner.+.TP+.IR root / repo .git/git-daemon-export-ok+Present if the repository is public.+.TP+.IR PREFIX /share/petal/style.css ", " icon.svg+Stylesheet and icon, to be served at+.B css+and+.BR icon .+.SH EXAMPLES+.B petal+sets no resource limits itself.+Run it through a wrapper:+.P+.nf+#!/bin/sh+ulimit -t 10+exec /usr/local/bin/petal+.fi+.P+.BR nginx :+.P+.nf+location = /style.css { alias /usr/local/share/petal/style.css; }+location = /icon.svg { alias /usr/local/share/petal/icon.svg; }+location / {+	include fastcgi_params;+	fastcgi_param SCRIPT_FILENAME /usr/local/bin/petal-cgi;+	fastcgi_param SCRIPT_NAME "";+	fastcgi_param PATH_INFO $uri;+	fastcgi_pass unix:/run/fcgiwrap.sock;+}+.fi+.SH CUSTOMIZATION+.B petal+is configured by editing+.I config.h+and recompiling.+.SH SEE ALSO+.BR herbarium (1),+.BR hedge (1),+.BR git-http-backend (1)

petal/petal.c

@@ -7,7 +7,6 @@ #include <stdlib.h> #include <string.h> #include <time.h>-#include <sys/resource.h> #include <sys/stat.h> #include <git2.h> @@ -1150,15 +1149,7 @@ main(void) { 	char path[1024]; 	const char *q;-	struct rlimit rl;--	rl.rlim_cur = rl.rlim_max = maxcpu;-	setrlimit(RLIMIT_CPU, &rl);-	root = envor("PETAL_ROOT", root);-	css = envor("PETAL_CSS", css);-	icon = envor("PETAL_ICON", icon);-	clone = envor("PETAL_CLONE", clone);-	title = envor("PETAL_TITLE", title);+ 	base = envor("SCRIPT_NAME", ""); 	q = envor("QUERY_STRING", ""); 	if (!strncmp(q, "ofs=", 4) && (ofs = strtol(q + 4, NULL, 10)) < 0)

pollen/pollen.1

@@ -192,5 +192,6 @@ is configured by editing .I config.h and recompiling. .SH SEE ALSO+.BR herbarium (1), .BR pipe (8), .BR sendmail (1)

pollen/pollen.c

@@ -305,7 +305,8 @@ int dropped(size_t pos) { 	return (hdrlen - pos >= 5 && !strncasecmp(msg + pos, "List-", 5))-	    || isfield(pos, "Return-Path") || isfield(pos, "Delivered-To");+	    || isfield(pos, "Return-Path") || isfield(pos, "Delivered-To")+	    || isfield(pos, "Precedence"); }  size_t@@ -876,6 +877,7 @@ setsub(const char *addr, int add) 	} 	while (in && getline(&line, &cap, in) > 0) { 		line[strcspn(line, "\r\n")] = '\0';+		lower(line); 		if (!*line) 			continue; 		if (!strcmp(line, addr)) {@@ -933,7 +935,6 @@ validtoken(const char *s) 	return strlen(s) == TOKLEN && strspn(s, "0123456789abcdef") == TOKLEN; } - /* main */ int main(int argc, char *argv[])