git.lucas.co / gitsite
the static git browser that builds this site
git clone https://git.lucas.co/gitsite.git

commitbd32c8ae8bb9cacd39e002b6d10711ba29b2c16c
parentd12cf5dda3
authorLucas Galante <[email protected]>
date2026-09-20 14:10
post-commit hook: a commit pushes itself to GitHub

post-commit-push pushes the committed branch to origin, never forcing,
and reports a rejected push rather than hiding it; it stays quiet on a
detached HEAD or mid-rebase. install-hooks.sh symlinks it into every repo
in repos.conf so there is one copy to edit, and the readme's publishing
model says that a plain commit is now the publishing step.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

 install-hooks.sh | 44 ++++++++++++++++++++++++++++++++++++++++++++
 post-commit-push | 27 +++++++++++++++++++++++++++
 readme.txt       | 15 ++++++++++++++-
 3 files changed, 85 insertions(+), 1 deletion(-)

diff --git a/install-hooks.sh b/install-hooks.sh
new file mode 100755
index 0000000..45f4d8d
--- /dev/null
+++ b/install-hooks.sh
@@ -0,0 +1,44 @@
+#!/bin/sh
+# Symlink post-commit-push into every repo listed in repos.conf, as its
+# post-commit hook. Safe to re-run: an existing symlink is refreshed, and a
+# hook that is a real file (someone's own) is left alone and reported.
+#
+# Work trees are found by NAME under $GIT_WORK_ROOTS, since repos.conf records
+# only where a repo is mirrored from. A name that resolves to nothing fails
+# the run rather than being skipped.
+set -eu
+
+BASE=$(dirname "$(readlink -f "$0")")
+HOOK="$BASE/post-commit-push"
+WORK_ROOTS="${GIT_WORK_ROOTS:-$HOME/projects/cce $HOME/projects}"
+FAILED=""
+
+names=$(grep -v '^#' "$BASE/repos.conf" | grep -v '^[[:space:]]*$' | cut -d'|' -f1)
+for name in $names; do
+    wt=""
+    for root in $WORK_ROOTS; do
+        if [ -d "$root/$name/.git" ]; then wt="$root/$name"; break; fi
+    done
+    if [ -z "$wt" ]; then
+        echo "!! $name: no work tree under $WORK_ROOTS"
+        FAILED="$FAILED $name"
+        continue
+    fi
+    hooks=$(git -C "$wt" rev-parse --path-format=absolute --git-path hooks)
+    dest="$hooks/post-commit"
+    if [ -e "$dest" ] && [ ! -L "$dest" ]; then
+        echo "!! $name: $dest exists and is not a symlink -- left alone"
+        FAILED="$FAILED $name"
+        continue
+    fi
+    mkdir -p "$hooks"
+    ln -sfn "$HOOK" "$dest"
+    echo "   $name"
+done
+
+echo
+if [ -n "$FAILED" ]; then
+    echo "done, with failures:$FAILED"
+    exit 1
+fi
+echo "done: post-commit hook installed in every repo"
diff --git a/post-commit-push b/post-commit-push
new file mode 100755
index 0000000..758db53
--- /dev/null
+++ b/post-commit-push
@@ -0,0 +1,27 @@
+#!/bin/sh
+# post-commit hook: push the branch just committed to origin (GitHub).
+#
+# Installed into every repo in repos.conf by install-hooks.sh, as a symlink to
+# this file, so there is one copy to edit. It makes a commit also the
+# publishing step: without it, committing is local only and nothing reaches
+# GitHub (or git.lucas.co, which mirrors GitHub hourly) until a manual push.
+#
+# It never forces. If the push is rejected -- the branch moved on GitHub, the
+# network is down -- the commit stands and the message below says so; fix it
+# with an ordinary git pull / git push. Nothing is pushed on a detached HEAD
+# or while a rebase is replaying commits.
+
+branch=$(git symbolic-ref --quiet --short HEAD) || exit 0
+git_dir=$(git rev-parse --git-dir)
+if [ -d "$git_dir/rebase-merge" ] || [ -d "$git_dir/rebase-apply" ]; then
+    exit 0
+fi
+git remote get-url origin >/dev/null 2>&1 || exit 0
+
+if out=$(git push --quiet --set-upstream origin "$branch" 2>&1); then
+    echo "pushed $branch to origin"
+else
+    echo "post-commit: push of $branch FAILED -- the commit is local only:" >&2
+    printf '%s\n' "$out" | sed 's/^/  /' >&2
+fi
+exit 0
diff --git a/readme.txt b/readme.txt
index be78746..b029ced 100644
--- a/readme.txt
+++ b/readme.txt
@@ -20,6 +20,11 @@ autodeploy.sh
 gitsite.service, gitsite.timer
              systemd user units that run autodeploy.sh hourly — see
              "Automatic publishing" below; nothing here installs them
+post-commit-push
+             git post-commit hook: pushes the committed branch to origin,
+             so committing is publishing -- see "Publishing model" below
+install-hooks.sh
+             symlinks post-commit-push into every repo in repos.conf
 style.css    matches lucas.co (black, white, blue links, Circe)
 
 Workflow
@@ -59,6 +64,13 @@ GitHub (git ls-remote) whether any HEAD moved. So:
     git push                    # the publishing step
                                 # gitsite.timer then deploys it
 
+Every repo in repos.conf has post-commit-push installed as its post-commit
+hook (install-hooks.sh does that; re-run it after cloning a repo fresh or
+adding one), so in practice the push happens on commit and a plain commit
+IS the publishing step. The hook never forces: a rejected push leaves the
+commit local and says so, and you resolve it with git pull / git push as
+usual. It does nothing on a detached HEAD or mid-rebase.
+
 The 27 crates pinned to https://git.lucas.co/<name>.git?rev=... keep
 working because the dumb-http clone dirs are built from the same mirrors;
 a rev exists here as long as it is reachable on GitHub.
@@ -97,4 +109,5 @@ Notes
   (Cloudflare rejects files over 25MB). Delete a mirror dir to force a
   fresh re-mirror.
 - To add a repo: create it on GitHub (gh repo create lsgalante/<name>
-  --public), push, add a line to repos.conf, run build.sh + deploy.sh.
+  --public), push, add a line to repos.conf, run install-hooks.sh, then
+  build.sh + deploy.sh.