From 4ff4830df408f1b490e913a9f13cbb545de7067d Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Tue, 12 May 2026 10:03:47 +0200 Subject: [PATCH] feat(scripts): implement agent-worktree clean sub-command (ref #83) Also patches gh-mock to sanitize dashes in branch-derived env var names, and fixes --head two-arg parsing in the pr view mock handler. Co-Authored-By: Claude Opus 4.7 (1M context) --- scripts/agent-worktree.sh | 40 +++++++++++++++++++++++++++- scripts/tests/fixtures/gh-mock.sh | 9 +++++-- scripts/tests/test-agent-worktree.sh | 34 +++++++++++++++++++++++ 3 files changed, 80 insertions(+), 3 deletions(-) diff --git a/scripts/agent-worktree.sh b/scripts/agent-worktree.sh index 672e09ca..d0ba7122 100755 --- a/scripts/agent-worktree.sh +++ b/scripts/agent-worktree.sh @@ -210,7 +210,45 @@ cmd_finish() { --title "$branch" --body "Closes #$issue" \ || return 4 } -cmd_clean() { echo "clean: not implemented" >&2 ; return 1; } +cmd_clean() { + local force=0 + local issue="" + while (($#)); do + case "$1" in + --force) force=1; shift ;; + -*) echo "clean: unknown flag $1" >&2; return 1 ;; + *) issue="$1"; shift ;; + esac + done + if [[ -z "$issue" ]]; then + echo "clean: usage: clean " >&2; return 1 + fi + + local wt_path + wt_path=$(_resolve_worktree_path_by_issue "$issue") \ + || { echo "clean: no worktree for issue #$issue" >&2; return 2; } + + if ! "$GIT_BIN" -C "$wt_path" diff-index --quiet HEAD --; then + echo "clean: worktree dirty: $wt_path" >&2; return 3 + fi + + local branch + branch=$("$GIT_BIN" -C "$wt_path" rev-parse --abbrev-ref HEAD) + + if (( ! force )); then + local pr_json state + pr_json=$("$GH_BIN" pr view --head "$branch" --json state,mergedAt 2>/dev/null || true) + state=$(printf '%s' "$pr_json" | sed -E 's/.*"state":"([^"]+)".*/\1/') + if [[ "$state" != "MERGED" ]]; then + echo "clean: PR for $branch is '$state', refusing (use --force)" >&2; return 3 + fi + fi + + "$GIT_BIN" worktree remove "$wt_path" + "$GIT_BIN" branch -d "$branch" 2>/dev/null \ + || "$GIT_BIN" branch -D "$branch" + echo "removed: $wt_path branch=$branch" +} main() { local sub="${1:-}" diff --git a/scripts/tests/fixtures/gh-mock.sh b/scripts/tests/fixtures/gh-mock.sh index 19ef7d5b..c81cc872 100755 --- a/scripts/tests/fixtures/gh-mock.sh +++ b/scripts/tests/fixtures/gh-mock.sh @@ -49,12 +49,17 @@ case "$cmd" in view|list) # Look up by --head or by branch arg branch="" + _prev="" for arg in "$@"; do + if [[ "$_prev" == "--head" ]]; then branch="$arg"; fi case "$arg" in --head=*) branch="${arg#--head=}";; esac + _prev="$arg" done if [[ -z "$branch" ]]; then branch="${1:-}"; fi - var_state="GH_MOCK_PR_${branch//\//_}_STATE" - var_merged="GH_MOCK_PR_${branch//\//_}_MERGED" + sanitized="${branch//\//_}" + sanitized="${sanitized//-/_}" + var_state="GH_MOCK_PR_${sanitized}_STATE" + var_merged="GH_MOCK_PR_${sanitized}_MERGED" state="${!var_state:-MERGED}" merged="${!var_merged:-2026-05-12T10:00:00Z}" printf '{"state":"%s","mergedAt":"%s"}\n' "$state" "$merged" diff --git a/scripts/tests/test-agent-worktree.sh b/scripts/tests/test-agent-worktree.sh index 28ba650d..84aa555f 100755 --- a/scripts/tests/test-agent-worktree.sh +++ b/scripts/tests/test-agent-worktree.sh @@ -293,6 +293,40 @@ test_finish_dirty_refuses() { if [[ $rc -ne 3 ]]; then echo "expected rc=3 got $rc" >&2; return 1; fi } +test_clean_refuses_open_pr() { + local repo wt + repo=$(make_sandbox_repo); wt=$(mktemp -d) + trap "rm -rf $repo $wt" RETURN + export GH_BIN="$GH_MOCK"; export GH_MOCK_AUTH=ok + export GH_MOCK_ISSUE_11_TITLE="C"; export GH_MOCK_ISSUE_11_LABELS="" + export GH_MOCK_PR_feature_11_c_STATE="OPEN" + export GH_MOCK_PR_feature_11_c_MERGED="" + export WORKTREE_ROOT="$wt" + (cd "$repo" && bash "$SCRIPT" start --issue 11) >/dev/null + local rc + (cd "$repo" && bash "$SCRIPT" clean 11) >/dev/null 2>&1 ; rc=$? + if [[ $rc -ne 3 ]]; then echo "expected rc=3 got $rc" >&2; return 1; fi +} + +test_clean_merged_pr_removes() { + local repo wt + repo=$(make_sandbox_repo); wt=$(mktemp -d) + trap "rm -rf $repo $wt" RETURN + export GH_BIN="$GH_MOCK"; export GH_MOCK_AUTH=ok + export GH_MOCK_ISSUE_12_TITLE="C"; export GH_MOCK_ISSUE_12_LABELS="" + export GH_MOCK_PR_feature_12_c_STATE="MERGED" + export WORKTREE_ROOT="$wt" + (cd "$repo" && bash "$SCRIPT" start --issue 12) >/dev/null + # Make a real commit on the branch then fold into master so branch is "merged" + (cd "$wt/12-c" && echo x > x && git add x && git commit -q -m "x") + (cd "$repo" && git merge --no-ff -q feature/12-c) + (cd "$repo" && bash "$SCRIPT" clean 12) >/dev/null + test -d "$wt/12-c" && { echo "worktree still present" >&2; return 1; } + (cd "$repo" && git show-ref --verify --quiet refs/heads/feature/12-c) \ + && { echo "branch still present" >&2; return 1; } + return 0 +} + # Auto-discover and run mapfile -t tests < <(declare -F | awk '{print $3}' | grep '^test_') for t in "${tests[@]}"; do run_test "$t"; done