Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
7 changes: 6 additions & 1 deletion USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=<project>") 2>/dev/null
docker run --rm -v "<runtime dir>:/runtime" alpine rm -rf /runtime/upstream.git
docker run --rm -v "<runtime dir>:/runtime" alpine \
rm -rf /runtime/upstream.git /runtime/mirrors &&
rm -rf "<runtime dir>"
```

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
Expand Down
10 changes: 8 additions & 2 deletions launch.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down
100 changes: 100 additions & 0 deletions tests/test_launch.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 ==="
Expand Down
Loading