From d9fdd4cbdf6d355d6037c00482cbaf50f2bd8d83 Mon Sep 17 00:00:00 2001 From: Bastien Chanot Date: Fri, 3 Jul 2026 13:57:59 +0200 Subject: [PATCH] fix(gitflow): gitflow_finish validates its named branch against HEAD MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gitflow_finish ignored its args and always merged the checked-out branch — `finish bugfix audit-bugs` run from feature/audit-tokens merged the wrong branch (audit UX trap, 2026-07-02). Args are now an optional safety ASSERTION: if present and != current branch, refuse loudly (rc 2) instead of merging the wrong thing. No args = unchanged (the only real caller, SKILL.md:36, passes none). +7 T12 regression assertions. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01XpphkdTosUzokBDNG7PToS --- lib/gitflow-test.sh | 16 ++++++++++++++++ lib/gitflow.sh | 19 ++++++++++++++++--- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/lib/gitflow-test.sh b/lib/gitflow-test.sh index 9b46394..b3f37e8 100644 --- a/lib/gitflow-test.sh +++ b/lib/gitflow-test.sh @@ -156,6 +156,22 @@ if bash "$HERE/gitflow.sh" protected-base main; then ok "cli protected-bas if bash "$HERE/gitflow.sh" protected-base feature/x; then no "cli protected-base feature (rc0?)"; else ok "cli protected-base feature → rc1"; fi chk "cli base-for hotfix=main" '[ "$(bash "$HERE/gitflow.sh" base-for hotfix)" = main ]' +echo "T12 — finish arg-guard (named branch must equal current, else refuse)" +newrepo finargs; echo a>a; hookon; gitflow_init >/dev/null 2>&1 +gitflow_start feature standon >/dev/null 2>&1; echo w>w.txt; git add w.txt; git commit -q -m w +# mismatch: standing on feature/standon but asking to finish bugfix/other → refuse +# shellcheck disable=SC2034 # mism_out/mism_rc are used in the deferred chk eval strings +mism_out="$(gitflow_finish bugfix other 2>&1)"; mism_rc=$? +chk "arg-mismatch → nonzero rc" "[ $mism_rc -ne 0 ]" +chk "arg-mismatch → HEAD untouched" '[ "$(git symbolic-ref --short HEAD)" = feature/standon ]' +chk "arg-mismatch → branch kept" 'git rev-parse --verify -q refs/heads/feature/standon >/dev/null' +chk "arg-mismatch → develop NOT merged" '! git log develop --oneline | grep -q "Merge feature/standon into develop"' +chk "arg-mismatch → message names both" 'printf "%s" "$mism_out" | grep -q "current branch" && printf "%s" "$mism_out" | grep -q "bugfix/other"' +# match: naming the current branch explicitly finishes exactly like the no-arg path +gitflow_finish feature standon >/dev/null 2>&1 +chk "arg-match → merged into develop" 'git log develop --oneline | grep -q "Merge feature/standon into develop"' +chk "arg-match → branch deleted" '! git rev-parse --verify -q refs/heads/feature/standon >/dev/null' + echo echo "==== RESULT: $PASS passed, $FAIL failed ====" [ "$FAIL" -eq 0 ] diff --git a/lib/gitflow.sh b/lib/gitflow.sh index 54feec7..31f8ed1 100644 --- a/lib/gitflow.sh +++ b/lib/gitflow.sh @@ -97,11 +97,24 @@ _gitflow_delete() { # git branch -q -d "$br" || { echo "gitflow: '$br' not fully merged — branch kept" >&2; return 5; } } -# gitflow_finish → directed merge of the CURRENT branch per its type, then delete. -# WHEN to call this is the human gate (SKILL.md). This only performs the merge. +# gitflow_finish [ ] → directed merge of the CURRENT branch per its +# type, then delete. WHEN to call this is the human gate (SKILL.md). +# +# The merge source is ALWAYS the checked-out branch (HEAD) — that is the contract. +# The optional is a SAFETY ASSERTION, not a target selector: if you +# name a branch it MUST equal the current one, else finish refuses loudly instead +# of silently merging whatever you happen to be standing on. (Guards the audit UX +# trap: `finish bugfix audit-bugs` run from feature/audit-tokens merged the wrong +# branch — args were silently ignored. See BLK-015 / LRN-089.) No args = unchanged. gitflow_finish() { - local br type + local br type req_type="${1:-}" req_name="${2:-}" br="$(git symbolic-ref --short -q HEAD)" || { echo "gitflow_finish: detached HEAD" >&2; return 3; } + if [ -n "$req_type" ] || [ -n "$req_name" ]; then + [ "$req_type/$req_name" = "$br" ] || { + echo "gitflow_finish: operates on the current branch '$br', but you asked '$req_type/$req_name' — checkout '$req_type/$req_name' first (or run finish with no args)." >&2 + return 2 + } + fi type="$(gitflow_branch_type "$br")" case "$type" in feature|bugfix|chore)