From 9cb0b27694cad481a5b726880e22d7828ae30c78 Mon Sep 17 00:00:00 2001 From: garrytan Date: Mon, 2 Nov 2026 05:20:58 +0000 Subject: [PATCH] fix(ubicloud): pack and sync a subdirectory of a git checkout (GST-935) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- scripts/ubicloud/ubi-runner.sh | 19 +++++++++++++++++-- test/ubicloud-runner.test.ts | 18 ++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/scripts/ubicloud/ubi-runner.sh b/scripts/ubicloud/ubi-runner.sh index 03a698e4b..5943a729c 100755 --- a/scripts/ubicloud/ubi-runner.sh +++ b/scripts/ubicloud/ubi-runner.sh @@ -1112,11 +1112,26 @@ cmd_down() { # options at the first operand and resolves -T names after a -C, so no single # tar command line works on both it and GNU tar (#3680). COPYFILE_DISABLE=1 # keeps Apple's tar from adding ._* AppleDouble entries for files with -# extended attributes, which the VM would see as untracked files. +# extended attributes, which the VM would see as untracked files. A +# subdirectory of a work tree ships the same file set without a .git: git's +# local transport fetches only from a repository's root, and the whole +# repository's .git would show the VM every file outside the subdirectory as +# deleted (GST-935). cmd_pack() { local src tmp gitdir branch head entry src=$(cd "${1:-.}" && pwd) - if git -C "$src" rev-parse --is-inside-work-tree >/dev/null 2>&1; then + if git -C "$src" rev-parse --is-inside-work-tree >/dev/null 2>&1 && [ -n "$(git -C "$src" rev-parse --show-prefix)" ]; then + ( + cd "$src" + export COPYFILE_DISABLE=1 + git ls-files -z -co --exclude-standard \ + | while IFS= read -r -d '' f; do + if [ -e "$f" ] || [ -L "$f" ]; then printf '%s\0' "$f"; fi + done \ + | tar --null -T - -cf - \ + | gzip -c + ) + elif git -C "$src" rev-parse --is-inside-work-tree >/dev/null 2>&1; then head=$(git -C "$src" rev-parse --verify -q HEAD) || die "pack: $src has no commits; the VM needs a HEAD to fingerprint" branch=$(git -C "$src" symbolic-ref -q --short HEAD || true) tmp=$(mktemp -d) diff --git a/test/ubicloud-runner.test.ts b/test/ubicloud-runner.test.ts index 66f6605bf..98fb5027a 100644 --- a/test/ubicloud-runner.test.ts +++ b/test/ubicloud-runner.test.ts @@ -380,6 +380,24 @@ test('pack ships a linked worktree with a self-contained .git on the checked-out } finally { rmSync(d, { recursive: true, force: true }); } }, 60_000); + // Value: protects=sync and pack of a subdirectory of a git checkout (gstack-evals' launch.sh syncs quick/laneb/runner) ship that subdirectory's working files; fails_when=pack fetches refs from a path below the work tree's root, which git's local transport refuses, or ships the whole repository's .git so the VM sees every file outside the subdirectory as deleted; why_new=every pack test packs a work tree's root or a plain directory; seam=none + test('pack of a subdirectory of a checkout or a linked worktree ships its tracked, edited and untracked files without a .git', () => { + const d = mkdtempSync(join(tmpdir(), 'ubi-pack-subdir-')); + try { + const sh = (cmd: string, cwd: string) => { const r = Bun.spawnSync(['bash', '-c', cmd], { cwd, timeout: 30_000, env: { ...process.env, GIT_AUTHOR_NAME: 't', GIT_AUTHOR_EMAIL: 't@t', GIT_COMMITTER_NAME: 't', GIT_COMMITTER_EMAIL: 't@t' } }); expect(r.exitCode, `${cmd}\n${r.stderr.toString()}`).toBe(0); return r.stdout.toString(); }; + sh('git init -q repo && cd repo && echo top > top && mkdir -p sub/nested && echo a > sub/a && echo b > sub/nested/b && echo g > sub/gone && echo "*.log" > sub/.gitignore && git add -A && git commit -qm one && git worktree add -q ../wt', d); + const tree = (dir: string) => sh('find . -type f | sort', dir).split('\n').filter(Boolean); + for (const checkout of ['repo', 'wt']) { + const sub = join(d, checkout, 'sub'); + sh('echo edited > a && rm gone && echo n > new && echo x > x.log', sub); + const vm = join(d, `vm-${checkout}`); + sh(`mkdir ${JSON.stringify(vm)} && bash ${JSON.stringify(join(DIR, 'ubi-runner.sh'))} pack sub | tar -xzf - -C ${JSON.stringify(vm)}`, join(d, checkout)); + expect(tree(vm)).toEqual(['./.gitignore', './a', './nested/b', './new']); + expect(readFileSync(join(vm, 'a'), 'utf8')).toBe('edited\n'); + } + } finally { rmSync(d, { recursive: true, force: true }); } + }, 60_000); + test('pack snapshots a plain checkout without reading its live .git: remotes resolve, a detached HEAD gets a branch, and git running beside it cannot fail the tar', () => { const d = mkdtempSync(join(tmpdir(), 'ubi-pack-live-')); try {