fix(gitflow): every autopush reader fails closed and names an invalid value

Run D1 of manual-push mode (BDR-114). `git config --bool --default true
gitflow.autopush` only covered a MISSING key: an unparseable value made
git die with empty output, the `= false` test failed, and every push ran
again. A typo on a work machine silently re-enabled the pushes it was
meant to stop.

- lib/gitflow.sh: `_gitflow_push_off` reads the mode through the lib
  verb (`push-mode`); anything but `auto` is push-off, and the verb's
  stderr line names an invalid value during start/finish.
- Emitted post-commit/post-merge hooks (POSIX sh, standalone): push only
  when the key reads `true` or is unset; `false` exits quietly; any
  other result prints one stderr line ("NOT pushed, treated as manual
  push mode") and exits 0. Mirrors gitflow_push_mode.
- .githooks/ and githooks/ regenerated files-only through `emit-hook`
  (no config read or write; .git/config hash unchanged).
- hooks/unpushed-guard.sh: mode from the lib verb (absolute lib path
  resolved before any cd, no temp file); anything but auto is manual;
  the SessionStart line names an invalid or unreadable value.
- Tests: gitflow-test T18q block (invalid → start, hook and finish push
  nothing and say so; `true` → the hook pushes; emitted hook is
  POSIX-clean), unpushed-guard T14 rewritten.
This commit is contained in:
bchanot
2026-10-07 16:45:31 +02:00
parent e4bc6212ef
commit 472cccbc52
8 changed files with 98 additions and 26 deletions
+15 -7
View File
@@ -12,9 +12,13 @@
#
# Manual-push mode (git config gitflow.autopush false, human-set): unpushed
# work is expected, so Stop stays silent; SessionStart gives one info line
# counting every local branch, with the branches to push by hand.
# counting every local branch, with the branches to push by hand. An
# unparseable value is treated as manual too (fail closed, BDR-114); the mode
# comes from the lib verb, the one reader the hooks share.
set -u
# Resolved before any cd: the hook may be invoked by a relative path.
_lib="$(cd "$(dirname "${BASH_SOURCE[0]}")/../lib" 2>/dev/null && pwd)/gitflow.sh"
payload=$(cat 2>/dev/null)
field() { printf '%s' "$payload" | jq -r "$1 // empty" 2>/dev/null; }
event=$(field '.hook_event_name')
@@ -23,10 +27,10 @@ cd "$cwd" 2>/dev/null || exit 0
git rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0
br=$(git symbolic-ref --short -q HEAD 2>/dev/null) || exit 0
raw=$(git config gitflow.autopush 2>/dev/null)
manual=0; invalid=0
[ "$(git config --bool --default true gitflow.autopush 2>/dev/null)" = false ] && manual=1
[ -n "$raw" ] && ! git config --bool gitflow.autopush >/dev/null 2>&1 && invalid=1
# The verb writes its stderr line BEFORE its stdout word: the last line is the mode.
out=$(bash "$_lib" push-mode 2>&1); mode=${out##*$'\n'}
mode_err=${out%"$mode"}; mode_err=${mode_err%$'\n'}
manual=0; [ "$mode" = auto ] || manual=1 # fail closed: anything but auto
[ "$manual" = 1 ] && [ "$event" != SessionStart ] && exit 0 # BDR-087: info at start only
# Local branches holding commits no remote has, one per line.
@@ -73,8 +77,12 @@ if [ "$event" = "SessionStart" ]; then
dirty=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
[ "$dirty" -gt 0 ] && msg="${msg:+$msg; }$dirty uncommitted change(s) in $cwd"
fi
if [ "$invalid" = 1 ] && [ "$event" = "SessionStart" ]; then
msg="${msg:+$msg; }gitflow.autopush='$raw' is not a boolean, treated as auto (pushes run)"
if [ "$event" = "SessionStart" ]; then
case "$mode" in
invalid) msg="${msg:+$msg; }${mode_err#gitflow.sh push-mode: } — treated as manual push mode (nothing pushes); fix the value by hand" ;;
manual|auto) ;;
*) msg="${msg:+$msg; }push mode unreadable (lib verb printed '${mode:-nothing}') — treated as manual push mode" ;;
esac
fi
[ -n "$msg" ] || exit 0