Diff options
| -rwxr-xr-x | custom/hooks/post-receive.cgit-cache | 115 | |
| -rwxr-xr-x | tests/t0507-hooks.sh | 108 |
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' ' |
