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) <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-05-12 10:03:47 +02:00
parent 4d604018e8
commit 4ff4830df4
3 changed files with 80 additions and 3 deletions

View File

@ -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 <issue#>" >&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:-}"

View File

@ -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"

View File

@ -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