From 27e67f3272f0278d68908a4f35d2ee2e0a1fdc26 Mon Sep 17 00:00:00 2001 From: Simone <88100810+orsissimo@users.noreply.github.com> Date: Mon, 2 Nov 2026 21:04:04 +0100 Subject: [PATCH] Clone the bare repo and mirrors without hardlinks --- CHANGELOG.md | 29 +++++++++++++ USAGE.md | 7 ++- launch.sh | 10 ++++- tests/test_launch.sh | 100 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 143 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f7d5e7..81f8959 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -103,6 +103,35 @@ SIGPIPE and the pipeline failed, so a check that should pass failed. On a loaded Ubuntu host the pattern failed 22 times in 20,000 runs. The tests now pass the text with a here-string. +- **Fix: the bare repo and mirrors copy the checkout's objects.** A + local `git clone --bare` hardlinks object files, so the + `chmod -R a+rwX` that lets the container's agent write to the bare + repo also made the checkout's own object files writable for every + user, and the mirrors did the same to the submodules' objects under + `.git/modules`. Both clones now copy the objects + (`--no-hardlinks --dissociate`), including any the checkout borrows + through alternates. Where earlier clones shared objects through + hardlinks or alternates, the bare repo and mirrors now need their + own space for them. Earlier local clones used hardlinks where they + could and copied otherwise, for example across filesystems, so a + bare repo or mirror that an earlier release made may still share + objects, and interactive and post-process sessions reuse it: stop + its containers, harvest, and remove the bare repo and mirrors as + "Cleanup" in USAGE.md shows, so that the next start copies the + objects. Then, in the checkout, restore the read-only modes and + check the objects: + + ```bash + find "$(git rev-parse --git-common-dir)" -path '*/objects/*' \ + -type f -perm -0002 -exec chmod a-w {} + + git fsck --full + git submodule foreach --recursive 'git fsck --full' + ``` + + "Cleanup" removes both the bare repo and mirrors from a container, + as `launch.sh` does, so that files an agent wrote with another UID + do not stop it. The runtime directory and state file are removed + only after that succeeds. ## 0.22.0 — 2026-06-08 diff --git a/USAGE.md b/USAGE.md index 94ebb55..af396b0 100644 --- a/USAGE.md +++ b/USAGE.md @@ -975,10 +975,15 @@ those files, so the bare repo goes through Docker first, the way ```bash docker rm -f $(docker ps -aq \ --filter "label=swarm-core.project=") 2>/dev/null -docker run --rm -v ":/runtime" alpine rm -rf /runtime/upstream.git +docker run --rm -v ":/runtime" alpine \ + rm -rf /runtime/upstream.git /runtime/mirrors && rm -rf "" ``` +The bare repo and mirrors are removed before the runtime directory +and its state file. This needs a running Docker daemon and the +`alpine` image, which Docker pulls if it is missing. + ## Verify image ```bash diff --git a/launch.sh b/launch.sh index 106415b..d282955 100755 --- a/launch.sh +++ b/launch.sh @@ -271,7 +271,11 @@ create_bare_repo() { local label="${1:-bare repo}" echo "--- Creating ${label} ---" rm_docker_dir "$BARE_REPO" - git clone --bare "$REPO_ROOT" "$BARE_REPO" + # A local clone hardlinks object files by default, and the chmod + # below would then make the checkout's own objects writable for + # every user. Copy them instead, with any objects the checkout + # borrows through alternates. + git clone --bare --no-hardlinks --dissociate "$REPO_ROOT" "$BARE_REPO" git -C "$BARE_REPO" branch agent-work HEAD 2>/dev/null || true git -C "$BARE_REPO" symbolic-ref HEAD refs/heads/agent-work # The container's agent user can have a different UID from the host @@ -326,7 +330,9 @@ mirror_submodules() { mirror="${MIRROR_DIR}/${safe_name}.git" rm_docker_dir "$mirror" echo "--- Mirroring submodule: ${name} ---" - git clone --bare "$gitdir" "$mirror" + # Copy objects, as create_bare_repo does, so the chmod below + # cannot reach the checkout's .git/modules through hardlinks. + git clone --bare --no-hardlinks --dissociate "$gitdir" "$mirror" chmod -R a+rwX "$mirror" done } diff --git a/tests/test_launch.sh b/tests/test_launch.sh index 2af0da6..c2fe727 100755 --- a/tests/test_launch.sh +++ b/tests/test_launch.sh @@ -1308,6 +1308,106 @@ assert_eq "bare repo objects/pack/ is world-writable" "1" "$_wr_pack_w" rm -rf "$_wr_repo" "$_wr_bare" +# ============================================================ +echo "" +echo "=== 35b. Bare repo and mirrors copy the checkout's objects ===" + +# A local clone hardlinks object files unless told otherwise, and the +# chmod -R a+rwX in create_bare_repo and mirror_submodules would then +# make the checkout's own object files writable for every user. Run +# the live functions from launch.sh on a checkout with packed and loose +# objects, a submodule, and objects borrowed through alternates, and +# check that nothing is shared, changed or still borrowed. +_hl_links() { + stat -c '%h' "$1" 2>/dev/null || stat -f '%l' "$1" 2>/dev/null +} +_hl_mode() { + stat -c '%a' "$1" 2>/dev/null || stat -f '%Lp' "$1" 2>/dev/null +} +_hl_launch="$TESTS_DIR/../launch.sh" +_hl_dir="$TMPDIR/hl" +_hl_git() { + git -c user.name=test -c user.email=test@test \ + -c commit.gpgsign=false -c protocol.file.allow=always "$@" +} +mkdir -p "$_hl_dir" +_hl_git -C "$_hl_dir" init -q base +_hl_git -C "$_hl_dir/base" commit --allow-empty -m base -q +_hl_base_head=$(git -C "$_hl_dir/base" rev-parse HEAD) +_hl_git -C "$_hl_dir" init -q sub +_hl_git -C "$_hl_dir/sub" commit --allow-empty -m sub -q +# src borrows base's objects through alternates. +_hl_git -C "$_hl_dir" clone -q --shared base src +_hl_git -C "$_hl_dir/src" submodule add -q "$_hl_dir/sub" sub 2>/dev/null +_hl_git -C "$_hl_dir/src" commit -m src -q +git -C "$_hl_dir/src" gc -q +git -C "$_hl_dir/src/.git/modules/sub" gc -q +_hl_git -C "$_hl_dir/src" commit --allow-empty -m loose -q +_hl_loose=$(git -C "$_hl_dir/src" rev-parse HEAD) +_hl_loose="$_hl_dir/src/.git/objects/${_hl_loose:0:2}/${_hl_loose:2}" +_hl_loose_mode=$(_hl_mode "$_hl_loose") +_hl_packs=("$_hl_dir"/src/.git/objects/pack/*.pack) +_hl_src_pack=${_hl_packs[0]} +_hl_packs=("$_hl_dir"/src/.git/modules/sub/objects/pack/*.pack) +_hl_sub_pack=${_hl_packs[0]} +_hl_src_mode=$(_hl_mode "$_hl_src_pack") +_hl_sub_mode=$(_hl_mode "$_hl_sub_pack") + +for _hl_fn in create_bare_repo begin_engagement mirror_submodules; do + awk -v fn="$_hl_fn" '$0 ~ "^" fn "\\(\\)[[:space:]]*\\{" { p = 1 } + p { print } + p && /^\}[[:space:]]*$/ { exit }' "$_hl_launch" +done > "$TMPDIR/hl_funcs.sh" +_hl_runtime="$_hl_dir/runtime" +mkdir -m 700 "$_hl_runtime" +_hl_mirror="$_hl_runtime/mirrors/sub.git" +( + # shellcheck source=/dev/null + source "$TMPDIR/hl_funcs.sh" + # The extracted functions call this stub. + # shellcheck disable=SC2329 + rm_docker_dir() { rm -rf "$1"; } + REPO_ROOT="$_hl_dir/src" + BARE_REPO="$_hl_dir/bare.git" + MIRROR_DIR="$_hl_runtime/mirrors" + STATE_FILE="$_hl_runtime/state.json" + create_bare_repo + mirror_submodules +) >/dev/null 2>&1 || true + +assert_eq "bare repo has the checkout's HEAD on agent-work" \ + "$(git -C "$_hl_dir/src" rev-parse HEAD)" \ + "$(git -C "$_hl_dir/bare.git" rev-parse agent-work 2>/dev/null)" +assert_eq "mirror has the submodule's commit" \ + "$(git -C "$_hl_dir/sub" rev-parse HEAD)" \ + "$(git -C "$_hl_mirror" rev-parse HEAD 2>/dev/null)" +assert_eq "checkout pack is not shared with the bare repo" "1" \ + "$(_hl_links "$_hl_src_pack")" +assert_eq "checkout pack keeps its mode" "$_hl_src_mode" \ + "$(_hl_mode "$_hl_src_pack")" +assert_eq "submodule pack is not shared with its mirror" "1" \ + "$(_hl_links "$_hl_sub_pack")" +assert_eq "submodule pack keeps its mode" "$_hl_sub_mode" \ + "$(_hl_mode "$_hl_sub_pack")" +assert_eq "checkout loose object is not shared with the bare repo" "1" \ + "$(_hl_links "$_hl_loose")" +assert_eq "checkout loose object keeps its mode" "$_hl_loose_mode" \ + "$(_hl_mode "$_hl_loose")" +_hl_alt=no +[ -e "$_hl_dir/bare.git/objects/info/alternates" ] && _hl_alt=yes +assert_eq "bare repo does not borrow objects" "no" "$_hl_alt" +mv "$_hl_dir/base" "$_hl_dir/base.moved" +_hl_kept=no +git -C "$_hl_dir/bare.git" cat-file -e "${_hl_base_head}^{commit}" \ + 2>/dev/null && _hl_kept=yes +assert_eq "bare repo keeps the borrowed objects" "yes" "$_hl_kept" +assert_eq "every bare clone in launch.sh copies objects" \ + "$(grep -c 'git clone --bare ' "$_hl_launch")" \ + "$(grep -c 'git clone --bare --no-hardlinks --dissociate ' \ + "$_hl_launch")" + +rm -rf "$_hl_dir" + # ============================================================ echo "" echo "=== 36. signing_key resolution ==="