commit 750a58c43fddfc1599c1f645e2261e64024ee33d
parent 1b3f36a712f701794e5e38e34240a0d922b2d998
Author: Chris Bracken <chris@bracken.jp>
Date: Mon, 31 Aug 2026 14:52:37 +0900
Add gout gemini hook, wiring, and update grm
Diffstat:
| M | grm | | | 96 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------- |
| A | post-receive.d/gout_gemini | | | 110 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
2 files changed, 180 insertions(+), 26 deletions(-)
diff --git a/grm b/grm
@@ -33,6 +33,12 @@ GRM_POSTRECV_HOOKS_DIR="/home/git/git_infra/post-receive.d"
# root directory of output gout web pages.
GOUT_WEB_ROOT="/var/www/git.bracken.jp"
+# root directory of output gout gemini pages.
+#
+# agate serves this as a virtual host, so it must be content/<hostname>/ and
+# agate must be started with more than one --hostname; see gout_gemini.
+GOUT_GEMINI_ROOT="/var/gemini/content/git.bracken.jp"
+
# #
# #
#--------------------------------------------#
@@ -43,50 +49,81 @@ export LC_CTYPE="en_US.UTF-8"
prog_name="${0##*/}"
repos_root="$GRM_REPOS_ROOT"
web_root="$GOUT_WEB_ROOT"
+gemini_root="$GOUT_GEMINI_ROOT"
+
+# agate reads the gemini pages as a different user in a different jail.
+umask 022
recompile_repo() {
- repo_dir="${repos_root}/${1}.git"
+ repo_name="$1"
+ repo_dir="${repos_root}/${repo_name}.git"
repo_web_dir="${web_root}/${repo_name}"
+ repo_gmi_dir="${gemini_root}/${repo_name}"
+ # One cache per output format: the cache holds log entries already rendered
+ # in that format, so html and gemini must not share one.
cachefile="${repo_dir}/.htmlcache"
+ gmicachefile="${repo_dir}/.gmicache"
[ -d "$repo_dir" ] || { echo "[$1] repo not found"; return 1; }
if [ -e "$repo_dir/git-daemon-export-ok" ]; then
- echo "[$1] recompiling gout pages..."
+ echo "[$1] recompiling gout html pages..."
mkdir -p "$repo_web_dir"
- cd "${repo_web_dir:?}" && \
- rm -f "$cachefile" && \
- rm -rf "commit" "file" && \
- gout -c "$cachefile" "$repo_dir" && \
- ln -sf log.html index.html && \
- ln -sf ../logo.png logo.png && \
- ln -sf ../style.css style.css && \
- ln -sf ../favicon.png favicon.png && \
+ cd "${repo_web_dir:?}" && \
+ rm -f "$cachefile" && \
+ rm -rf "commit" "file" && \
+ gout -H -c "$cachefile" "$repo_dir" && \
+ ln -sf log.html index.html && \
+ ln -sf ../logo.png logo.png && \
+ ln -sf ../style.css style.css && \
+ ln -sf ../favicon.png favicon.png || return 1
+
+ echo "[$1] recompiling gout gemini pages..."
+ mkdir -p "$repo_gmi_dir"
+ cd "${repo_gmi_dir:?}" && \
+ rm -f "$gmicachefile" && \
+ rm -rf "commit" "file" && \
+ gout -M -c "$gmicachefile" "$repo_dir" && \
+ ln -sf log.gmi index.gmi || return 1
+
echo "[$1] done!"
else
echo "[$1] Not recompiling gout pages: private repo"
fi
}
-rebuild_index() {
- echo "[index] rebuilding index..."
- mkdir -p "${web_root}" || return 1;
- # 1. find all directories in $repos_root ending with .git
- # 2. filter all the public repos (with git-daemon-export-ok)
- # 3. exclude any repo marked with gout-no-index
- # 4. sort the result
- # 5. hack for posix compatibility
- # 6. run gout_index on the result
- # 7. export result to index.html
+# Lists the repos that belong in the index, one shell-quoted path per line.
+#
+# 1. find all directories in $repos_root ending with .git
+# 2. filter all the public repos (with git-daemon-export-ok)
+# 3. exclude any repo marked with gout-no-index
+# 4. sort the result
+# 5. hack for posix compatibility
+indexable_repos() {
find "${repos_root}/." ! -name . -prune \
-type d -name "*.git" \
-exec test -e "{}/git-daemon-export-ok" \;\
-exec test ! -e "{}/gout-no-index" \; \
-print \
| sort -f \
- | sed -e 's/"/"\\""/g' -e 's/.*/"&"/' \
- | xargs gout_index -m "${ME_URL}" \
- > "${web_root}/index.html" && \
+ | sed -e 's/"/"\\""/g' -e 's/.*/"&"/'
+}
+
+rebuild_index() {
+ echo "[index] rebuilding html index..."
+ mkdir -p "${web_root}" || return 1;
+ indexable_repos \
+ | xargs gout_index -H -m "${ME_URL}" \
+ > "${web_root}/index.html" || return 1
+
+ # gout_index -M emits relative links (=> repo/log.gmi) and takes no -m: the
+ # Mastodon verification link is an HTML-only concept.
+ echo "[index] rebuilding gemini index..."
+ mkdir -p "${gemini_root}" || return 1;
+ indexable_repos \
+ | xargs gout_index -M \
+ > "${gemini_root}/index.gmi" || return 1
+
echo "[index] done!"
}
@@ -150,11 +187,17 @@ ${BLUE}enter index [default: ${GREEN}1${BLUE}]${RESET}"
[ "$exported" = "1" ] && : >> "$repo_path/git-daemon-export-ok"
[ "$hidden" = "1" ] && : >> "$repo_path/gout-no-index"
- echo "installing gout post-receive hook..."
+ # Link the page-generation hooks ONLY. post-receive.d also holds
+ # publish_www and publish_gemini, which check out a work tree over the blog
+ # and the capsule; a new repo linked to those would overwrite them on its
+ # first push. Never replace this with a loop over the directory.
+ echo "installing gout post-receive hooks..."
ln -sf "$GRM_POSTRECV_HOOK" "$repo_path/hooks/post-receive"
mkdir -p "$repo_path/hooks/post-receive.d"
ln -sf "$GRM_POSTRECV_HOOKS_DIR/gout_html" \
"$repo_path/hooks/post-receive.d/gout_html"
+ ln -sf "$GRM_POSTRECV_HOOKS_DIR/gout_gemini" \
+ "$repo_path/hooks/post-receive.d/gout_gemini"
echo "done!"
}
@@ -169,10 +212,11 @@ grm_remove() {
if echo "$resp" | grep -iq "^y$"; then
rm -rf "${repos_root:?}/${repo:?}.git" || continue;
rm -rf "${web_root:?}/${repo:?}" || continue;
+ rm -rf "${gemini_root:?}/${repo:?}" || continue;
fi
done
# only rebuild index if gout exists
- command -v gout_index -m "${ME_URL}" >/dev/null && rebuild_index &
+ command -v gout_index >/dev/null && rebuild_index &
wait
}
@@ -268,7 +312,7 @@ commands:
ls private list private repos
ls unlisted list unlisted (hidden) repos
rm repo1 [repo2..] remove repos
- rc recompile gout index
+ rc recompile gout index (html and gemini)
rc repo1 [repo2..] recompile gout pages for repos,
and recompile index
rca recompile all public repos
diff --git a/post-receive.d/gout_gemini b/post-receive.d/gout_gemini
@@ -0,0 +1,110 @@
+#!/bin/sh -e
+#
+# post-receive.d hook: generate Gemini pages for a repo, plus the repo index.
+#
+# The twin of gout_html. The post-receive dispatcher feeds the same stdin to
+# every hook in post-receive.d, so the two run independently and neither needs
+# to know about the other.
+#
+# Output is served by agate as a virtual host, which is why the root below is
+# content/<hostname>/ and why agate must be started with BOTH hostnames:
+# agate_flags="--hostname chris.bracken.jp --hostname git.bracken.jp ..."
+# agate_content="/var/gemini/content"
+# agate only resolves the per-hostname subdirectory when given more than one
+# --hostname.
+#
+# Install:
+# /home/git/git_infra/post-receive.d/gout_gemini (this file, chmod +x)
+# /home/git/<repo>.git/hooks/post-receive.d/gout_gemini -> symlink
+
+export LC_CTYPE="en_US.UTF-8"
+
+# agate reads these as a different user (1965) in a different jail, so the
+# world bits have to be set.
+umask 022
+
+# Root directory into which gout will write pages.
+gmi_root="/var/gemini/content/git.bracken.jp"
+
+# Hooks are called from the repo directory.
+repo_dir=$(pwd)
+
+# The gout cache file.
+# This is an optimisation to avoid regenerating all pages on each push.
+#
+# It must NOT be shared with gout_html: the cache holds log entries already
+# rendered in the output format, so one cache per format.
+cachefile="${repo_dir}/.gmicache"
+
+# The default branch of the repository.
+default_branch=$(git symbolic-ref HEAD 2>/dev/null || echo "refs/heads/main")
+
+# The directory under which all repos are located.
+# Repos under this directory will be added to the repo index page.
+repos_root=$(dirname "$repo_dir")
+
+# The user-friendly name of the repository.
+repo_name=$(basename "$repo_dir" '.git')
+
+# Directory into which repo pages will be written.
+repo_gmi_dir="${gmi_root}/${repo_name}"
+
+# Detect --force pushes.
+cd "${repo_dir}" || exit 1
+force=0
+while read -r old new ref; do
+ # Ignore force pushes to non-default branches.
+ [ "${ref}" = "${default_branch}" ] || continue
+
+ [ "${old}" = "0000000000000000000000000000000000000000" ] && continue
+ [ "${new}" = "0000000000000000000000000000000000000000" ] && continue
+
+ hasrevs=$(git rev-list "$old" "^$new" | sed 1q)
+ if [ -n "$hasrevs" ]; then
+ echo "[gout] Force push detected on ${ref}: cleaning gemini cache"
+ force=1
+ break
+ fi
+done
+
+# If there was a --force push, delete all existing pages. The lack of a cache
+# file will trigger a full rebuild below.
+if [ "$force" = "1" ]; then
+ rm -f "$cachefile"
+ rm -rf "${repo_gmi_dir}/commit" "${repo_gmi_dir}/file"
+fi
+
+# Build the pages. If $cachefile is not present, a full rebuild is performed.
+if [ -e "${repo_dir}/git-daemon-export-ok" ]; then
+ # Change to the directory where we will output pages.
+ mkdir -p "$repo_gmi_dir"
+ cd "$repo_gmi_dir" || exit 1
+
+ # Generate the index page.
+ # Any repo containing a gout-no-index file will not be indexed.
+ #
+ # gout_index -M emits relative links (=> repo/log.gmi), and takes no -m: the
+ # Mastodon verification link is an HTML-only concept.
+ echo "[gout] Building gemini repo index"
+ find "${repos_root}/." -maxdepth 1 \
+ -type d \
+ -name "*.git" \
+ -exec test -e "{}/git-daemon-export-ok" \; \
+ -exec test ! -e "{}/gout-no-index" \; \
+ -print \
+ | sort -f \
+ | sed -e 's/"/"\\""/g' -e 's/.*/"&"/' \
+ | xargs gout_index -M \
+ > "${gmi_root}/index.gmi"
+
+ # Rebuild the pages for the repo (including for unlisted repos).
+ echo "[gout] Building gemini pages"
+ gout -M -c "$cachefile" "$repo_dir"
+
+ # use log as index page: agate looks for index.gmi when a directory is
+ # requested, and answers "Directory index disabled" if there is none.
+ echo "[gout] Linking gemini index"
+ ln -sf log.gmi index.gmi
+else
+ echo "[gout] Not building gemini pages: private repo"
+fi