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)