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
95 changes: 95 additions & 0 deletions scripts/agent-worktree-prune.sh
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ fi
repo_root="$(git rev-parse --show-toplevel)"
current_pwd="$(pwd -P)"
worktree_root="${repo_root}/.omx/agent-worktrees"
repo_common_dir="$(
git -C "$repo_root" rev-parse --git-common-dir \
| awk -v root="$repo_root" '{ if ($0 ~ /^\//) { print $0 } else { print root "/" $0 } }'
)"
repo_common_dir="$(cd "$repo_common_dir" && pwd -P)"

resolve_base_branch() {
local configured=""
Expand Down Expand Up @@ -132,11 +137,98 @@ is_clean_worktree() {
&& [[ -z "$(git -C "$wt" ls-files --others --exclude-standard)" ]]
}

resolve_worktree_common_dir() {
local wt="$1"
local common_dir=""
common_dir="$(git -C "$wt" rev-parse --git-common-dir 2>/dev/null || true)"
if [[ -z "$common_dir" ]]; then
return 1
fi
if [[ "$common_dir" == /* ]]; then
common_dir="$(cd "$common_dir" 2>/dev/null && pwd -P || true)"
else
common_dir="$(cd "$wt/$common_dir" 2>/dev/null && pwd -P || true)"
fi
if [[ -z "$common_dir" ]]; then
return 1
fi
printf '%s' "$common_dir"
}

select_unique_worktree_path() {
local root="$1"
local name="$2"
local candidate="${root}/${name}"
local suffix=2
while [[ -e "$candidate" ]]; do
candidate="${root}/${name}-${suffix}"
suffix=$((suffix + 1))
done
printf '%s' "$candidate"
}

relocated_foreign=0
skipped_foreign=0

relocate_foreign_worktree_entries() {
[[ -d "$worktree_root" ]] || return 0

local entry=""
for entry in "${worktree_root}"/*; do
[[ -d "$entry" ]] || continue
if ! git -C "$entry" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
continue
fi

local entry_common_dir=""
entry_common_dir="$(resolve_worktree_common_dir "$entry" || true)"
[[ -n "$entry_common_dir" ]] || continue

if [[ "$entry_common_dir" == "$repo_common_dir" ]]; then
continue
fi

if [[ "$(basename "$entry_common_dir")" != ".git" ]]; then
skipped_foreign=$((skipped_foreign + 1))
echo "[agent-worktree-prune] Skipping foreign worktree with unsupported git common dir: ${entry}"
continue
fi

local owner_repo_root
owner_repo_root="$(dirname "$entry_common_dir")"
local owner_worktree_root="${owner_repo_root}/.omx/agent-worktrees"
local target_path
target_path="$(select_unique_worktree_path "$owner_worktree_root" "$(basename "$entry")")"

if [[ "$entry" == "$current_pwd" || "$current_pwd" == "${entry}"/* ]]; then
skipped_foreign=$((skipped_foreign + 1))
echo "[agent-worktree-prune] Skipping active foreign worktree: ${entry}"
continue
fi

echo "[agent-worktree-prune] Relocating foreign worktree to owning repo: ${entry} -> ${target_path}"
if [[ "$DRY_RUN" -eq 1 ]]; then
relocated_foreign=$((relocated_foreign + 1))
continue
fi

mkdir -p "$owner_worktree_root"
if git -C "$owner_repo_root" worktree move "$entry" "$target_path" >/dev/null 2>&1; then
relocated_foreign=$((relocated_foreign + 1))
else
skipped_foreign=$((skipped_foreign + 1))
echo "[agent-worktree-prune] Failed to relocate foreign worktree: ${entry}" >&2
fi
done
}

removed_worktrees=0
removed_branches=0
skipped_active=0
skipped_dirty=0

relocate_foreign_worktree_entries

process_entry() {
local wt="$1"
local branch_ref="$2"
Expand Down Expand Up @@ -265,6 +357,9 @@ fi
run_cmd git -C "$repo_root" worktree prune

echo "[agent-worktree-prune] Summary: base=${BASE_BRANCH}, removed_worktrees=${removed_worktrees}, removed_branches=${removed_branches}, skipped_active=${skipped_active}, skipped_dirty=${skipped_dirty}"
if [[ "$relocated_foreign" -gt 0 || "$skipped_foreign" -gt 0 ]]; then
echo "[agent-worktree-prune] Foreign routing: relocated=${relocated_foreign}, skipped=${skipped_foreign}"
fi
if [[ "$skipped_active" -gt 0 ]]; then
echo "[agent-worktree-prune] Tip: leave active agent worktree directories, then run this command again for full cleanup." >&2
fi
Expand Down
95 changes: 95 additions & 0 deletions templates/scripts/agent-worktree-prune.sh
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ fi
repo_root="$(git rev-parse --show-toplevel)"
current_pwd="$(pwd -P)"
worktree_root="${repo_root}/.omx/agent-worktrees"
repo_common_dir="$(
git -C "$repo_root" rev-parse --git-common-dir \
| awk -v root="$repo_root" '{ if ($0 ~ /^\//) { print $0 } else { print root "/" $0 } }'
)"
repo_common_dir="$(cd "$repo_common_dir" && pwd -P)"

resolve_base_branch() {
local configured=""
Expand Down Expand Up @@ -132,11 +137,98 @@ is_clean_worktree() {
&& [[ -z "$(git -C "$wt" ls-files --others --exclude-standard)" ]]
}

resolve_worktree_common_dir() {
local wt="$1"
local common_dir=""
common_dir="$(git -C "$wt" rev-parse --git-common-dir 2>/dev/null || true)"
if [[ -z "$common_dir" ]]; then
return 1
fi
if [[ "$common_dir" == /* ]]; then
common_dir="$(cd "$common_dir" 2>/dev/null && pwd -P || true)"
else
common_dir="$(cd "$wt/$common_dir" 2>/dev/null && pwd -P || true)"
fi
if [[ -z "$common_dir" ]]; then
return 1
fi
printf '%s' "$common_dir"
}

select_unique_worktree_path() {
local root="$1"
local name="$2"
local candidate="${root}/${name}"
local suffix=2
while [[ -e "$candidate" ]]; do
candidate="${root}/${name}-${suffix}"
suffix=$((suffix + 1))
done
printf '%s' "$candidate"
}

relocated_foreign=0
skipped_foreign=0

relocate_foreign_worktree_entries() {
[[ -d "$worktree_root" ]] || return 0

local entry=""
for entry in "${worktree_root}"/*; do
[[ -d "$entry" ]] || continue
if ! git -C "$entry" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
continue
fi

local entry_common_dir=""
entry_common_dir="$(resolve_worktree_common_dir "$entry" || true)"
[[ -n "$entry_common_dir" ]] || continue

if [[ "$entry_common_dir" == "$repo_common_dir" ]]; then
continue
fi

if [[ "$(basename "$entry_common_dir")" != ".git" ]]; then
skipped_foreign=$((skipped_foreign + 1))
echo "[agent-worktree-prune] Skipping foreign worktree with unsupported git common dir: ${entry}"
continue
fi

local owner_repo_root
owner_repo_root="$(dirname "$entry_common_dir")"
local owner_worktree_root="${owner_repo_root}/.omx/agent-worktrees"
local target_path
target_path="$(select_unique_worktree_path "$owner_worktree_root" "$(basename "$entry")")"

if [[ "$entry" == "$current_pwd" || "$current_pwd" == "${entry}"/* ]]; then
skipped_foreign=$((skipped_foreign + 1))
echo "[agent-worktree-prune] Skipping active foreign worktree: ${entry}"
continue
fi

echo "[agent-worktree-prune] Relocating foreign worktree to owning repo: ${entry} -> ${target_path}"
if [[ "$DRY_RUN" -eq 1 ]]; then
relocated_foreign=$((relocated_foreign + 1))
continue
fi

mkdir -p "$owner_worktree_root"
if git -C "$owner_repo_root" worktree move "$entry" "$target_path" >/dev/null 2>&1; then
relocated_foreign=$((relocated_foreign + 1))
else
skipped_foreign=$((skipped_foreign + 1))
echo "[agent-worktree-prune] Failed to relocate foreign worktree: ${entry}" >&2
fi
done
}

removed_worktrees=0
removed_branches=0
skipped_active=0
skipped_dirty=0

relocate_foreign_worktree_entries

process_entry() {
local wt="$1"
local branch_ref="$2"
Expand Down Expand Up @@ -265,6 +357,9 @@ fi
run_cmd git -C "$repo_root" worktree prune

echo "[agent-worktree-prune] Summary: base=${BASE_BRANCH}, removed_worktrees=${removed_worktrees}, removed_branches=${removed_branches}, skipped_active=${skipped_active}, skipped_dirty=${skipped_dirty}"
if [[ "$relocated_foreign" -gt 0 || "$skipped_foreign" -gt 0 ]]; then
echo "[agent-worktree-prune] Foreign routing: relocated=${relocated_foreign}, skipped=${skipped_foreign}"
fi
if [[ "$skipped_active" -gt 0 ]]; then
echo "[agent-worktree-prune] Tip: leave active agent worktree directories, then run this command again for full cleanup." >&2
fi
Expand Down
35 changes: 35 additions & 0 deletions test/install.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2322,6 +2322,41 @@ test('worktree prune --only-dirty-worktrees removes clean agent worktrees but ke
assert.equal(branchResult.status, 0, 'unmerged branch ref should remain');
});

test('worktree prune reroutes foreign worktrees to the owning repo .omx root', () => {
const repoDir = initRepo();
let result = runNode(['setup', '--target', repoDir, '--no-global-install'], repoDir);
assert.equal(result.status, 0, result.stderr || result.stdout);
seedCommit(repoDir);

const foreignRepoDir = initRepo();
seedCommit(foreignRepoDir);

const misplacedPath = path.join(repoDir, '.omx', 'agent-worktrees', 'agent__foreign-owned');
result = runCmd(
'git',
['-C', foreignRepoDir, 'worktree', 'add', '-b', 'agent/foreign-owned', misplacedPath, 'dev'],
repoDir,
);
assert.equal(result.status, 0, result.stderr || result.stdout);
assert.equal(fs.existsSync(misplacedPath), true, 'foreign worktree should start misplaced under current repo');

result = runCmd('bash', ['scripts/agent-worktree-prune.sh'], repoDir);
assert.equal(result.status, 0, result.stderr || result.stdout);
assert.match(result.stdout, /Relocating foreign worktree to owning repo/);
assert.equal(fs.existsSync(misplacedPath), false, 'misplaced foreign worktree should be moved out');

const foreignWorktreeRoot = path.join(foreignRepoDir, '.omx', 'agent-worktrees');
const relocatedCandidates = fs.existsSync(foreignWorktreeRoot)
? fs.readdirSync(foreignWorktreeRoot).filter((name) => name.startsWith('agent__foreign-owned'))
: [];
assert.equal(relocatedCandidates.length > 0, true, 'foreign repo should receive relocated worktree');

const relocatedPath = path.join(foreignWorktreeRoot, relocatedCandidates[0]);
const commonDirResult = runCmd('git', ['-C', relocatedPath, 'rev-parse', '--git-common-dir'], repoDir);
assert.equal(commonDirResult.status, 0, commonDirResult.stderr || commonDirResult.stdout);
assert.match(commonDirResult.stdout.trim(), new RegExp(`${escapeRegexLiteral(foreignRepoDir)}/\\.git$`));
});

test('cleanup command removes merged agent branch/worktree and remote ref', () => {
const repoDir = initRepo();
seedCommit(repoDir);
Expand Down
Loading