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
6 changes: 3 additions & 3 deletions cli/__tests__/ship-branch.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -1429,7 +1429,7 @@ describe('ship-branch.sh — untracked/gitignored gate configs are linked into t
expect(r.stderr).not.toMatch(/\.search-code\/index\.db .*commit it/);
});

it('uses main-worktree gate inputs when shipping from a linked worktree', () => {
it('uses main-worktree gate inputs and classifies a symlinked cache without a fatal pathspec', () => {
const hookBody = [
'[ -e guard.config.json ] && echo CONFIG_SEEN || echo CONFIG_MISSING',
'[ -e .search-code/index.db ] && echo INDEX_SEEN || echo INDEX_MISSING',
Expand All @@ -1455,12 +1455,11 @@ describe('ship-branch.sh — untracked/gitignored gate configs are linked into t
dirs.push(linkedParent);
const linked = join(linkedParent, 'checkout');
git(['worktree', 'add', '-q', '-b', 'linked-task', linked], { stdio: 'ignore' });
mkdirSync(join(linked, '.search-code')); // interrupted/empty local index directory
execFileSync('ln', ['-s', join(dir, '.search-code'), join(linked, '.search-code')]);
mkdirSync(join(linked, '.decisions')); // empty local projection must not hide populated main data
mkdirSync(join(linked, '.fallow'));
writeFileSync(join(linked, '.fallow/source'), 'linked'); // populated local override still wins
writeFileSync(join(linked, 'note.txt'), 'hi\n');

const r = spawnSync('/bin/bash', [scriptPath, 'feat/linked-gate-inputs', 't', 'note.txt'], {
cwd: linked,
input: 'b\n',
Expand All @@ -1470,6 +1469,7 @@ describe('ship-branch.sh — untracked/gitignored gate configs are linked into t
dropWorktree(git, r.stderr);
expect(r.status, r.stderr).toBe(0);
expect(r.stderr).toMatch(/\.search-code\/index\.db .*gitignored cache/);
expect(r.stderr).not.toMatch(/fatal: pathspec/);
const log = readFileSync(
join(linked, '.devkit/last-ship-gates-feat-linked-gate-inputs.log'),
'utf8',
Expand Down
38 changes: 36 additions & 2 deletions cli/lib/ship/link-gate-configs.sh
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,42 @@ is_review_projection_purpose() {
[ "$1" = review ] || [ "$1" = review-baseline ]
}

# gate_projection_source_is_ignored <consumer-root> <resolved-source> <repo-relative-path>
#
# `git check-ignore <repo-relative-path>` refuses to traverse a symlinked directory inside a
# worktree. Gate inputs deliberately use that shape to share ignored caches from the main worktree,
# so resolve the source's parent physically and ask the worktree that owns those bytes instead.
gate_projection_source_is_ignored() {
local root=$1 source=$2 rel=$3 physical_parent physical_source owner='' candidate owner_rel
if ! physical_parent=$(cd -P "$(dirname "$source")" 2>/dev/null && pwd); then
git -C "$root" check-ignore -q -- "$rel"
return
fi
physical_source="$physical_parent/$(basename "$source")"
while IFS= read -r candidate; do
case "$physical_source" in
"$candidate"/*)
[ "${#candidate}" -gt "${#owner}" ] && owner=$candidate
;;
esac
done < <(
git -C "$root" worktree list --porcelain 2>/dev/null |
awk '/^worktree /{print substr($0, 10)}'
)
if [ -n "$owner" ]; then
owner_rel=${physical_source#"$owner"/}
git -C "$owner" check-ignore -q -- "$owner_rel"
else
git -C "$root" check-ignore -q -- "$rel"
fi
}

# link_untracked_gate_configs <worktree> <root> [purpose]
link_untracked_gate_configs() {
local wt=$1 root=$2 purpose=${3:-ship} emitter resolved rel line index_rel='' candidate_manifest=''
local main_root='' candidate_root=$root source=''
local projection_manifest=${DEVKIT_REVIEW_PROJECTION_MANIFEST:-} projection_tool=''
local linked=() candidates=()
local linked=() linked_sources=() candidates=()
case "$purpose" in
ship | review | review-baseline) ;;
*)
Expand Down Expand Up @@ -138,6 +168,7 @@ link_untracked_gate_configs() {
for rel in "${candidates[@]}"; do
[ -e "$root/$rel" ] && [ ! -e "$wt/$rel" ] && [ ! -L "$wt/$rel" ] || continue
linked+=("$rel")
linked_sources+=("$root/$rel")
done
{
if [ "${#linked[@]}" -gt 0 ]; then
Expand All @@ -154,6 +185,7 @@ link_untracked_gate_configs() {
mkdir -p "$wt/$(dirname "$rel")"
ln -s "$source" "$wt/$rel"
linked+=("$rel")
linked_sources+=("$source")
done
fi

Expand All @@ -167,13 +199,15 @@ link_untracked_gate_configs() {
else
echo " linked into the gate worktree so gates match a normal commit (not defaults):"
fi
local linked_index=0
for rel in "${linked[@]}"; do
# `check-ignore -q` inside the `if` → its exit-1 "not ignored" is errexit-safe.
if git -C "$root" check-ignore -q "$rel"; then
if gate_projection_source_is_ignored "$root" "${linked_sources[$linked_index]}" "$rel"; then
echo " - $rel (gitignored cache — normal)"
else
echo " - $rel (untracked — commit it so gates are consistent for everyone)"
fi
linked_index=$((linked_index + 1))
done
} >&2
return 0
Expand Down
Loading