git_infra

Git infra scripts for git.bracken.jp
git clone https://git.bracken.jp/git_infra.git
Log | Files | Refs | LICENSE

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:
Mgrm | 96++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Apost-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