Diff options
authorBryce Kwon <bryce@brycekwon.com>
committerBryce Kwon <bryce@brycekwon.com>
commit
parent
tree
download
Drop only the cache a push makes stale
The hook cleared every slot on a push to any repository. cgit writes the request a slot answers at its head, so the hook now reads it back and drops only the pushed repository's pages and the index. It finds the repository's url from scan-path or hooks.cgiturl, and with neither it clears everything as before.
Diffstat (limited to '')
-rwxr-xr-xcustom/hooks/post-receive.cgit-cache115
-rwxr-xr-xtests/t0507-hooks.sh108
2 files changed, 195 insertions, 28 deletions
diff --git a/custom/hooks/post-receive.cgit-cache b/custom/hooks/post-receive.cgit-cache
index b1d5c13..f1a4b40 100755
--- a/custom/hooks/post-receive.cgit-cache
+++ b/custom/hooks/post-receive.cgit-cache
@@ -1,14 +1,20 @@
#!/bin/sh
-# Drops cgit's output cache after a push, so new commits show up right away
+# Drops the cached pages a push makes stale, so new commits show up right away
# instead of once the cache-*-ttl minutes have passed. It only matters when
# cache-size in cgitrc is above 0, which turns the cache on.
#
-# cgit names each cache slot after a hash of the request url, so there is no
-# way to expire one repository's pages on their own and this clears the lot.
-# cgit refills a slot the next time something asks for it. The cached
-# repository scan goes too, which is what makes a brand new repository appear
-# without waiting out cache-scan-ttl.
+# cgit writes the request each slot answers at the head of the slot file, so
+# the hook reads it back and drops only this repository's pages and the
+# repository index, whose idle column the push changes. Other repositories
+# keep their cached pages. The cached repository scan goes too, which is what
+# makes a brand new repository appear without waiting out cache-scan-ttl.
+#
+# The hook needs the url cgit serves this repository at. A repository that
+# scan-path finds is served at its path below scan_path, as set below, so
+# match that and remove_suffix to cgitrc. One that cgitrc names with repo.url
+# gives its url with "git config hooks.cgiturl <url>". With no url to go on,
+# the whole cache goes, as the safe choice.
#
# The path below is the default cache-root, so a cgitrc that changes the
# setting needs the same change here. The web server owns the cache root, so
@@ -23,11 +29,98 @@
set -eu
cache_root=/var/cache/cgit
+scan_path=/var/lib/git
+remove_suffix=0
test -d "$cache_root" || exit 0
-# Everything directly under the cache root is a cache file, so the only thing
-# worth keeping is the lock files, which belong to writers still working.
-# The "! -name . -prune" pair is how POSIX find stays out of subdirectories.
-find "$cache_root"/. ! -name . -prune ! -name '*.lock' -type f \
- -exec rm -f {} + 2>/dev/null || exit 0
+url=$(git config --get hooks.cgiturl || :)
+scan_root=$(cd "$scan_path" 2>/dev/null && pwd -P) || scan_root=
+if test -z "$url" && test -n "$scan_root"; then
+ git_dir=$(cd "$(git rev-parse --git-dir)" && pwd -P)
+ case $git_dir in
+ "$scan_root"/?*)
+ url=${git_dir#"$scan_root"/}
+ url=${url%/.git}
+ if test "$remove_suffix" = 1; then
+ url=${url%.git}
+ url=${url%/}
+ fi
+ ;;
+ esac
+fi
+
+# Takes one part of a slot's key off the front of $rest into $part. cgit
+# writes each part behind its length and a bar, so no value can be read as
+# another.
+take() {
+ len=${rest%%"|"*}
+ case $len in
+ "" | *[!0-9]*)
+ return 1
+ ;;
+ esac
+ rest=${rest#*"|"}
+ test "${#rest}" -ge "$len" || return 1
+ pattern=
+ while test "${#pattern}" -lt "$len"; do
+ pattern="$pattern?"
+ done
+ part=${rest%"${rest#$pattern}"}
+ rest=${rest#$pattern}
+}
+
+# Whether the page a slot holds goes stale with this push: one of this
+# repository's, by path or by the url and r parameters, or the index. A key
+# that does not read as cgit's counts as stale, since dropping a page costs
+# only a refill.
+stale() {
+ rest=$1
+ take && take && take || return 0
+ path=$part
+ take || return 0
+ case $path in
+ "/$url" | "/$url/"*)
+ return 0
+ ;;
+ "" | "/")
+ ;;
+ *)
+ return 1
+ ;;
+ esac
+ case "&$part&" in
+ *"&url=$url&"* | *"&url=$url/"* | *"&r=$url&"*)
+ return 0
+ ;;
+ *"&url=&"* | *"&url=/&"*)
+ ;;
+ *"&url="* | *"&r="*)
+ return 1
+ ;;
+ esac
+ return 0
+}
+
+# The lock files belong to writers still working, so they stay.
+for slot in "$cache_root"/*; do
+ case $slot in
+ *.lock)
+ continue
+ ;;
+ esac
+ test -f "$slot" || continue
+ case $slot in
+ */rc-*)
+ rm -f "$slot" 2>/dev/null || :
+ continue
+ ;;
+ esac
+ if test -z "$url" ||
+ dd if="$slot" bs=4096 count=1 2>/dev/null | tr '\0' '\n' |
+ { IFS= read -r key || :; stale "$key"; }
+ then
+ rm -f "$slot" 2>/dev/null || :
+ fi
+done
+exit 0
diff --git a/tests/t0507-hooks.sh b/tests/t0507-hooks.sh
index 977931b..8802ccc 100755
--- a/tests/t0507-hooks.sh
+++ b/tests/t0507-hooks.sh
@@ -1,9 +1,9 @@
#!/bin/sh
# Checks the post-receive hooks under custom/hooks. The cache hook runs from a
-# copy pointed at a cache in the trash directory, since the shipped one names
-# the system's cache root, and the age hook runs inside bare repositories the
-# way git runs it after a push.
+# copy pointed at a cache and a scan path in the trash directory, since the
+# shipped one names the system's own, and both hooks run inside bare
+# repositories the way git runs them after a push.
test_description='Check the shipped post-receive hooks'
CGIT_TEST_NO_CREATE_REPOS=YesPlease
@@ -11,23 +11,88 @@ CGIT_TEST_NO_CREATE_REPOS=YesPlease
HOOK_DIRECTORY="$TEST_OUTPUT_DIRECTORY/../custom/hooks"
-test_expect_success 'the cache hook clears the slots and keeps the locks' '
- mkdir cache &&
- echo page >cache/0123abcd &&
- echo writer >cache/0123abcd.lock &&
- sed "s|^cache_root=.*|cache_root='\''$PWD/cache'\''|" \
- "$HOOK_DIRECTORY/post-receive.cgit-cache" >cache-hook &&
- sh cache-hook &&
- test_path_is_missing cache/0123abcd &&
- test_path_is_file cache/0123abcd.lock
+# A cache that cgit itself filled, for two scanned repositories and the
+# index, so the hook reads the keys cgit really writes.
+test_expect_success 'set up a cache of two repositories and the index' '
+ test_create_repo pusher &&
+ test_commit -C pusher cached &&
+ mkdir scan slots &&
+ git init -q --bare scan/a.git &&
+ git init -q --bare scan/b.git &&
+ git -C pusher push -q ../scan/a.git HEAD:refs/heads/main &&
+ git -C pusher push -q ../scan/b.git HEAD:refs/heads/main &&
+ {
+ echo "cache-root=$PWD/slots" &&
+ echo "cache-size=1000" &&
+ echo "virtual-root=/" &&
+ echo "scan-path=$PWD/scan"
+ } >cachedrc &&
+ for page in / /a.git/ /a.git/log/ /b.git/ /b.git/log/
+ do
+ CGIT_CONFIG="$PWD/cachedrc" PATH_INFO="$page" QUERY_STRING= cgit >/dev/null ||
+ return 1
+ done &&
+ CGIT_CONFIG="$PWD/cachedrc" QUERY_STRING="url=a.git/refs/" cgit >/dev/null &&
+ CGIT_CONFIG="$PWD/cachedrc" QUERY_STRING="url=b.git/refs/" cgit >/dev/null &&
+ echo writer >slots/0123abcd.lock &&
+ cp -R slots pristine &&
+ sed -e "s|^cache_root=.*|cache_root='\''$PWD/slots'\''|" \
+ -e "s|^scan_path=.*|scan_path='\''$PWD/scan'\''|" \
+ "$HOOK_DIRECTORY/post-receive.cgit-cache" >cache-hook
+'
+
+# The request each slot answers, read off its head the way the hook reads it.
+cached_requests() {
+ for slot in slots/*
+ do
+ dd if="$slot" bs=4096 count=1 2>/dev/null | tr "\0" "\n" | sed -n 1p
+ done
+}
+
+# The hooks are POSIX sh, so each runs under every such shell that is here.
+shells=
+for shell in sh dash ksh bash
+do
+ command -v $shell >/dev/null && shells="$shells $shell"
+done
+
+for shell in $shells
+do
+ test_expect_success "a push drops its own pages and the index under $shell" "
+ rm -rf slots && cp -R pristine slots &&
+ (cd scan/a.git && $shell ../../cache-hook) &&
+ cached_requests >left &&
+ grep '|/b.git/log/' left &&
+ grep 'url=b.git/refs/' left &&
+ ! grep 'a.git' left &&
+ ! grep '1|/0|' left &&
+ test_path_is_file slots/0123abcd.lock &&
+ ! ls slots | grep '^rc-'
+ "
+done
+
+test_expect_success 'a repository named in cgitrc gives the hook its url' '
+ rm -rf slots && cp -R pristine slots &&
+ git -C scan/b.git config hooks.cgiturl b.git &&
+ sed "s|^scan_path=.*|scan_path=/nonexistent|" cache-hook >named-hook &&
+ (cd scan/b.git && sh ../../named-hook) &&
+ cached_requests >left &&
+ grep "|/a.git/log/" left &&
+ ! grep "b.git" left
+'
+
+test_expect_success 'with no url to go on the whole cache goes' '
+ rm -rf slots && cp -R pristine slots &&
+ (cd scan/a.git && sh ../../named-hook) &&
+ test "$(ls slots)" = 0123abcd.lock
'
test_expect_success SANITY 'the cache hook shows a pusher no error' '
- echo page >cache/89abcdef &&
- test_when_finished "chmod 755 cache" &&
- chmod 555 cache &&
- sh cache-hook 2>err &&
- test_path_is_file cache/89abcdef &&
+ rm -rf slots && cp -R pristine slots &&
+ test_when_finished "chmod 755 slots" &&
+ chmod 555 slots &&
+ (cd scan/a.git && sh ../../cache-hook 2>../../err) &&
+ cached_requests | grep "a.git" &&
test_must_be_empty err
'
@@ -41,6 +106,15 @@ test_expect_success 'the age hook writes the newest branch date' '
test_cmp expect plain.git/info/web/last-modified
'
+for shell in $shells
+do
+ test_expect_success "the age hook writes the same date under $shell" "
+ rm -f plain.git/info/web/last-modified &&
+ (cd plain.git && $shell \"\$HOOK_DIRECTORY/post-receive.cgit-age\") &&
+ test_cmp expect plain.git/info/web/last-modified
+ "
+done
+
# git keeps a shared repository group writable whatever the pusher's umask,
# and the next member to push has to be able to replace the file.
test_expect_success SANITY 'the age hook keeps a shared directory group writable' '