feat(gitflow): manual-push mode honoured by the lib, quiet unpushed-guard
`gitflow.autopush false` (human-set git config) now means "nothing is pushed" end to end, not only in the post-commit/post-merge hooks: - lib/gitflow.sh: `_gitflow_push_off` is the single reader of GITFLOW_NO_PUSH / gitflow.autopush for the lib's push sites; `start` and `finish` stop pushing in manual mode. `gitflow_delete` checks out the base that contains the branch and drops a lagging upstream before `git branch -d` (LRN-161: `-d` judges against the upstream when set). Skipped remote deletes say `left in place`; `_gitflow_sync_base` replaces the silent `pull --ff-only || true` and warns when a base is behind origin and cannot fast-forward. - hooks/unpushed-guard.sh: manual mode is silent at Stop and gives one `ℹ manual push mode:` line at SessionStart counting every local branch; an unparseable value is named and treated as auto. - CLAUDE.global.md: manual-push mode doctrine, "ahead = defect" scoped to auto mode. - Tests: gitflow-test T18m block (T18m0, T18i-T18o, 7 cases), unpushed-guard T10-T16. Follow-ups (TODO.md): run B push-guard hook + settings deny widening + banner; run C skills that push on their own (/close STEP 5C, …). Do not enable manual mode on the work machine before B and C land.
This commit is contained in:
+8
-4
@@ -183,9 +183,12 @@ auto-pushed upstream). The reference-transaction hook vetoes any deletion
|
|||||||
or rename of `main`/`develop`. The four hooks run in every repo: `make
|
or rename of `main`/`develop`. The four hooks run in every repo: `make
|
||||||
link` generates `githooks/` and sets the global `core.hooksPath`; a repo
|
link` generates `githooks/` and sets the global `core.hooksPath`; a repo
|
||||||
that ran `gitflow init` (new/onboarded projects) keeps its own `.githooks/`,
|
that ran `gitflow init` (new/onboarded projects) keeps its own `.githooks/`,
|
||||||
refreshed at session start. Foreign clone: `git config gitflow.protect
|
refreshed at session start. Human-set opt-outs: `git config
|
||||||
false` / `gitflow.autopush false`; `GITFLOW_NO_PUSH=1` only for throwaway
|
gitflow.protect false` (foreign clone) and `gitflow.autopush false` =
|
||||||
test repos. A branch ahead of its upstream is a defect, not a state.
|
manual-push mode (work machine): branches, commits and local merges run as
|
||||||
|
usual, nothing is pushed, Claude never pushes (`/close` included) unless
|
||||||
|
the user asks; `GITFLOW_NO_PUSH=1` only for throwaway test repos. Outside
|
||||||
|
manual mode a branch ahead of its upstream is a defect, not a state.
|
||||||
|
|
||||||
## Security — non-negotiable defaults
|
## Security — non-negotiable defaults
|
||||||
Apply at every step: design, scaffolding, implementation, review.
|
Apply at every step: design, scaffolding, implementation, review.
|
||||||
@@ -227,7 +230,8 @@ days of work never pushed.
|
|||||||
- A brief, plan step or test recipe never authorizes a sub-agent to do any
|
- A brief, plan step or test recipe never authorizes a sub-agent to do any
|
||||||
of this; a reviewer reads the script it reviews, it does not run it.
|
of this; a reviewer reads the script it reviews, it does not run it.
|
||||||
- Everything is pushed as it lands (gitflow hooks): unpushed work is a
|
- Everything is pushed as it lands (gitflow hooks): unpushed work is a
|
||||||
defect to fix now, not a state to keep.
|
defect to fix now, not a state to keep. Manual-push mode (above) is the
|
||||||
|
one exception.
|
||||||
|
|
||||||
# Communication mode: radical honesty
|
# Communication mode: radical honesty
|
||||||
- TRUTH OVER COMFORT: point out flaws immediately, no sugarcoating, no "not
|
- TRUTH OVER COMFORT: point out flaws immediately, no sugarcoating, no "not
|
||||||
|
|||||||
+36
-1
@@ -9,6 +9,10 @@
|
|||||||
# SessionStart also reports uncommitted changes (a dead session leaves some
|
# SessionStart also reports uncommitted changes (a dead session leaves some
|
||||||
# behind); Stop reports unpushed commits only, since a dirty tree mid-work is
|
# behind); Stop reports unpushed commits only, since a dirty tree mid-work is
|
||||||
# the normal state at a turn end.
|
# the normal state at a turn end.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
set -u
|
set -u
|
||||||
|
|
||||||
payload=$(cat 2>/dev/null)
|
payload=$(cat 2>/dev/null)
|
||||||
@@ -19,9 +23,37 @@ cd "$cwd" 2>/dev/null || exit 0
|
|||||||
git rev-parse --is-inside-work-tree >/dev/null 2>&1 || 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
|
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
|
||||||
|
[ "$manual" = 1 ] && [ "$event" != SessionStart ] && exit 0 # BDR-087: info at start only
|
||||||
|
|
||||||
|
# Local branches holding commits no remote has, one per line.
|
||||||
|
ahead_branches() {
|
||||||
|
local b
|
||||||
|
while IFS= read -r b; do
|
||||||
|
[ "$(git rev-list --count "$b" --not --remotes 2>/dev/null)" -gt 0 ] && echo "$b"
|
||||||
|
done < <(git for-each-ref --format='%(refname:short)' refs/heads)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Manual mode: commits on every local branch that no remote holds.
|
||||||
|
manual_clause() {
|
||||||
|
local n list first
|
||||||
|
n=$(git rev-list --count --branches --not --remotes 2>/dev/null || echo 0)
|
||||||
|
[ "$n" -gt 0 ] || return 0
|
||||||
|
if ! git remote get-url origin >/dev/null 2>&1; then
|
||||||
|
echo "no 'origin' remote, $n commit(s) on this disk only"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
list=$(ahead_branches); first=$(printf '%s\n' "$list" | head -n 1)
|
||||||
|
echo "$n commit(s) not on origin ($(printf '%s' "$list" | paste -sd, - | sed 's/,/, /g')), push by hand: git push -u origin $first"
|
||||||
|
}
|
||||||
|
|
||||||
# Commits that no remote holds, as one clause; empty when everything is pushed.
|
# Commits that no remote holds, as one clause; empty when everything is pushed.
|
||||||
unpushed_clause() {
|
unpushed_clause() {
|
||||||
local up n
|
local up n
|
||||||
|
[ "$manual" = 1 ] && { manual_clause; return; }
|
||||||
if ! git remote get-url origin >/dev/null 2>&1; then
|
if ! git remote get-url origin >/dev/null 2>&1; then
|
||||||
echo "no 'origin' remote, every commit lives on this disk only"
|
echo "no 'origin' remote, every commit lives on this disk only"
|
||||||
return
|
return
|
||||||
@@ -41,9 +73,12 @@ if [ "$event" = "SessionStart" ]; then
|
|||||||
dirty=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
|
dirty=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
|
||||||
[ "$dirty" -gt 0 ] && msg="${msg:+$msg; }$dirty uncommitted change(s) in $cwd"
|
[ "$dirty" -gt 0 ] && msg="${msg:+$msg; }$dirty uncommitted change(s) in $cwd"
|
||||||
fi
|
fi
|
||||||
|
if [ "$invalid" = 1 ] && [ "$event" = "SessionStart" ]; then
|
||||||
|
msg="${msg:+$msg; }gitflow.autopush='$raw' is not a boolean, treated as auto (pushes run)"
|
||||||
|
fi
|
||||||
[ -n "$msg" ] || exit 0
|
[ -n "$msg" ] || exit 0
|
||||||
|
|
||||||
msg="⚠ unpushed work: $msg"
|
if [ "$manual" = 1 ]; then msg="ℹ manual push mode: $msg"; else msg="⚠ unpushed work: $msg"; fi
|
||||||
if [ "$event" = "SessionStart" ]; then
|
if [ "$event" = "SessionStart" ]; then
|
||||||
jq -cn --arg m "$msg" \
|
jq -cn --arg m "$msg" \
|
||||||
'{systemMessage: $m, hookSpecificOutput: {hookEventName: "SessionStart", additionalContext: $m}}'
|
'{systemMessage: $m, hookSpecificOutput: {hookEventName: "SessionStart", additionalContext: $m}}'
|
||||||
|
|||||||
@@ -354,6 +354,41 @@ gitflow_start feature nr >/dev/null 2>&1; echo w>w; git add w
|
|||||||
nr_out="$(git commit -q -m w 2>&1)"; nr_rc=$?
|
nr_out="$(git commit -q -m w 2>&1)"; nr_rc=$?
|
||||||
chk "T18g no origin → silent, commit ok" "[ $nr_rc -eq 0 ] && ! printf '%s' \"\$nr_out\" | grep -q FAILED"
|
chk "T18g no origin → silent, commit ok" "[ $nr_rc -eq 0 ] && ! printf '%s' \"\$nr_out\" | grep -q FAILED"
|
||||||
|
|
||||||
|
echo "T18m — manual-push mode: gitflow.autopush=false (human-set) → nothing pushed, finish still deletes"
|
||||||
|
newrepo manual; echo a>a; hookon; gitflow_init >/dev/null 2>&1
|
||||||
|
bare="$WORK/manual.git"; git init -q --bare "$bare"; git remote add origin "$bare"
|
||||||
|
git push -q -u origin main develop 2>/dev/null
|
||||||
|
chk "T18m0 develop tracks origin/develop" "git rev-parse -q --verify 'develop@{u}' >/dev/null"
|
||||||
|
git config gitflow.autopush false
|
||||||
|
gitflow_start feature manual >/dev/null 2>&1
|
||||||
|
chk "T18i start → branch local, no copy on origin" 'git rev-parse --verify -q refs/heads/feature/manual >/dev/null && ! git ls-remote --exit-code --heads origin feature/manual >/dev/null 2>&1'
|
||||||
|
echo m>m.txt; git add m.txt; git commit -q -m m
|
||||||
|
dev_remote_before=$(git -C "$bare" rev-parse develop)
|
||||||
|
gitflow_finish >/dev/null 2>&1; fin_rc=$?
|
||||||
|
chk "T18j finish → merged locally, origin develop unchanged, branch deleted" "[ $fin_rc -eq 0 ] && grep -q 'Merge feature/manual into develop' < <(git log develop --format=%s) && [ \"\$(git -C \"$bare\" rev-parse develop)\" = \"$dev_remote_before\" ] && ! git rev-parse --verify -q refs/heads/feature/manual >/dev/null"
|
||||||
|
git config gitflow.autopush true; gitflow_start feature lag >/dev/null 2>&1
|
||||||
|
git config gitflow.autopush false
|
||||||
|
echo l>l.txt; git add l.txt; git commit -q -m l
|
||||||
|
gitflow_finish >"$WORK/lag.out" 2>&1; lag_rc=$?
|
||||||
|
chk "T18k lagging upstream → finish deletes, remote copy left in place" "[ $lag_rc -eq 0 ] && ! git rev-parse --verify -q refs/heads/feature/lag >/dev/null && [ \"\$(git -C \"$bare\" rev-parse develop)\" = \"$dev_remote_before\" ] && grep -q 'left in place' \"$WORK/lag.out\" && git ls-remote --exit-code --heads origin feature/lag >/dev/null 2>&1"
|
||||||
|
git config gitflow.autopush true; gitflow_start feature np >/dev/null 2>&1
|
||||||
|
git config gitflow.autopush false
|
||||||
|
echo n>n.txt; git add n.txt; git commit -q -m n
|
||||||
|
GITFLOW_NO_PUSH=1 gitflow_finish >"$WORK/np.out" 2>&1; np_rc=$?
|
||||||
|
chk "T18o NO_PUSH → silent on the remote copy" "[ $np_rc -eq 0 ] && ! git rev-parse --verify -q refs/heads/feature/np >/dev/null && ! grep -q 'left in place' \"$WORK/np.out\" && git ls-remote --exit-code --heads origin feature/np >/dev/null 2>&1"
|
||||||
|
git remote set-url origin /nonexistent/x.git
|
||||||
|
gitflow_start feature off2 >"$WORK/off2.out" 2>&1
|
||||||
|
chk "T18n offline, nothing recorded → silent, branch created" "! grep -q behind \"$WORK/off2.out\" && git rev-parse --verify -q refs/heads/feature/off2 >/dev/null"
|
||||||
|
git remote set-url origin "$bare"; git checkout -q develop
|
||||||
|
other="$WORK/manual-other"; git clone -q "$bare" "$other" 2>/dev/null
|
||||||
|
( cd "$other" && git config user.email t@t && git config user.name t \
|
||||||
|
&& git config core.hooksPath /dev/null && git checkout -q develop \
|
||||||
|
&& echo o>o.txt && git add o.txt && git commit -q -m o \
|
||||||
|
&& git push -q origin develop ) >/dev/null 2>&1
|
||||||
|
div_err="$WORK/div.err"
|
||||||
|
div_out=$(gitflow_start feature div 2>"$div_err")
|
||||||
|
chk "T18l diverged base → warns on stderr, stdout stays the branch name" "[ \"$div_out\" = feature/div ] && grep -q 'behind origin/develop' \"$div_err\" && git rev-parse --verify -q refs/heads/feature/div >/dev/null"
|
||||||
|
|
||||||
echo "T19 — installed hooks == emitted hooks in the config repo (LRN-114 drift gate)"
|
echo "T19 — installed hooks == emitted hooks in the config repo (LRN-114 drift gate)"
|
||||||
if [ -d "$HERE/../.githooks" ]; then
|
if [ -d "$HERE/../.githooks" ]; then
|
||||||
chk "T19a pre-commit installed == emitted" 'diff -q <(_gitflow_emit_pre_commit) "$HERE/../.githooks/pre-commit" >/dev/null'
|
chk "T19a pre-commit installed == emitted" 'diff -q <(_gitflow_emit_pre_commit) "$HERE/../.githooks/pre-commit" >/dev/null'
|
||||||
|
|||||||
+55
-6
@@ -70,15 +70,23 @@ gitflow_release_open() {
|
|||||||
|
|
||||||
# ── start ────────────────────────────────────────────────────────────────────
|
# ── start ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# rc 0 when pushing is off: GITFLOW_NO_PUSH=1 (throwaway test repos) or
|
||||||
|
# gitflow.autopush=false (manual-push mode, human-set: work machine, foreign
|
||||||
|
# clone). The single reader of both flags for the lib's own push sites.
|
||||||
|
_gitflow_push_off() {
|
||||||
|
[ "${GITFLOW_NO_PUSH:-0}" = 1 ] && return 0
|
||||||
|
[ "$(git config --bool --default true gitflow.autopush)" = false ]
|
||||||
|
}
|
||||||
|
|
||||||
# gitflow_start <type> <name> → checkout -b <type>/<name> from the correct base.
|
# gitflow_start <type> <name> → checkout -b <type>/<name> from the correct base.
|
||||||
# _gitflow_push_branch <br> → push + set upstream on origin (BDR-095: a remote
|
# _gitflow_push_branch <br> → push + set upstream on origin (BDR-095: a remote
|
||||||
# only backs up what it holds, so a branch is pushed the moment it exists).
|
# only backs up what it holds, so a branch is pushed the moment it exists).
|
||||||
# Best effort BY CONTRACT: no origin, offline, or refused → loud warning, rc 0.
|
# Best effort BY CONTRACT: no origin, offline, or refused → loud warning, rc 0.
|
||||||
# A failed push must never block the work, only make the gap visible.
|
# A failed push must never block the work, only make the gap visible.
|
||||||
# GITFLOW_NO_PUSH=1 opts out (throwaway test repos).
|
# Opt-outs: see _gitflow_push_off.
|
||||||
_gitflow_push_branch() {
|
_gitflow_push_branch() {
|
||||||
local br="$1"
|
local br="$1"
|
||||||
[ "${GITFLOW_NO_PUSH:-0}" = 1 ] && return 0
|
_gitflow_push_off && return 0
|
||||||
git remote get-url origin >/dev/null 2>&1 || return 0
|
git remote get-url origin >/dev/null 2>&1 || return 0
|
||||||
if _gitflow_timeout git push -q -u --follow-tags origin "$br" >/dev/null 2>&1; then
|
if _gitflow_timeout git push -q -u --follow-tags origin "$br" >/dev/null 2>&1; then
|
||||||
return 0
|
return 0
|
||||||
@@ -96,6 +104,20 @@ _gitflow_timeout() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# _gitflow_sync_base → fast-forward the checked-out base from its upstream.
|
||||||
|
# Never blocks. A base that cannot fast-forward while the remote is ahead (a
|
||||||
|
# recorded divergence) is warned about: auto-push used to be the only thing
|
||||||
|
# that surfaced it. No upstream, or offline with nothing recorded → silent.
|
||||||
|
_gitflow_sync_base() {
|
||||||
|
local behind
|
||||||
|
_gitflow_timeout git pull --ff-only -q >/dev/null 2>&1 && return 0
|
||||||
|
git rev-parse -q --verify '@{u}' >/dev/null 2>&1 || return 0
|
||||||
|
behind=$(git rev-list --count 'HEAD..@{u}' 2>/dev/null || echo 0)
|
||||||
|
[ "$behind" -gt 0 ] || return 0
|
||||||
|
echo "gitflow: $(git symbolic-ref --short -q HEAD) is behind origin/$(git symbolic-ref --short -q HEAD) by $behind and cannot fast-forward — reconcile by hand (git pull, then push)" >&2
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
gitflow_start() {
|
gitflow_start() {
|
||||||
local type="${1:-}" name="${2:-}" base
|
local type="${1:-}" name="${2:-}" base
|
||||||
base="$(gitflow_base_for "$type")" || return 2
|
base="$(gitflow_base_for "$type")" || return 2
|
||||||
@@ -103,7 +125,7 @@ gitflow_start() {
|
|||||||
git rev-parse --verify -q "$base" >/dev/null \
|
git rev-parse --verify -q "$base" >/dev/null \
|
||||||
|| { echo "gitflow_start: base '$base' missing — run 'gitflow init' first" >&2; return 3; }
|
|| { echo "gitflow_start: base '$base' missing — run 'gitflow init' first" >&2; return 3; }
|
||||||
git checkout -q "$base" || return 1
|
git checkout -q "$base" || return 1
|
||||||
git pull --ff-only -q 2>/dev/null || true # best-effort sync; offline / no-upstream ok
|
_gitflow_sync_base # best-effort sync; warns on divergence, never blocks
|
||||||
git checkout -q -b "$type/$name" || return 1
|
git checkout -q -b "$type/$name" || return 1
|
||||||
_gitflow_push_branch "$type/$name"
|
_gitflow_push_branch "$type/$name"
|
||||||
echo "$type/$name"
|
echo "$type/$name"
|
||||||
@@ -114,7 +136,7 @@ gitflow_start() {
|
|||||||
_gitflow_merge_into() { # _gitflow_merge_into <target> <source>
|
_gitflow_merge_into() { # _gitflow_merge_into <target> <source>
|
||||||
local target="$1" source="$2"
|
local target="$1" source="$2"
|
||||||
git checkout -q "$target" || return 1
|
git checkout -q "$target" || return 1
|
||||||
git pull --ff-only -q 2>/dev/null || true
|
_gitflow_sync_base
|
||||||
git merge --no-ff -q -m "Merge $source into $target" "$source" \
|
git merge --no-ff -q -m "Merge $source into $target" "$source" \
|
||||||
|| { echo "gitflow: conflict merging $source → $target — resolve, commit, re-run finish" >&2; return 4; }
|
|| { echo "gitflow: conflict merging $source → $target — resolve, commit, re-run finish" >&2; return 4; }
|
||||||
_gitflow_push_branch "$target" # git merge fires post-merge, not post-commit; push here too
|
_gitflow_push_branch "$target" # git merge fires post-merge, not post-commit; push here too
|
||||||
@@ -143,6 +165,15 @@ gitflow_merged_into_base() {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# _gitflow_note_remote_left <br> → manual mode never deletes origin/<br>; say
|
||||||
|
# so when a remote-tracking ref shows a copy exists (no network call).
|
||||||
|
_gitflow_note_remote_left() {
|
||||||
|
local br="$1"
|
||||||
|
gitflow_protected_base "$br" && return 0
|
||||||
|
git rev-parse -q --verify "refs/remotes/origin/$br" >/dev/null || return 0
|
||||||
|
echo "gitflow: origin/$br left in place (manual push mode) — by hand: git push origin --delete $br" >&2
|
||||||
|
}
|
||||||
|
|
||||||
# _gitflow_delete_remote <br> → remove origin/<br> once the LOCAL copy is gone.
|
# _gitflow_delete_remote <br> → remove origin/<br> once the LOCAL copy is gone.
|
||||||
# Same contract as the pushes (BDR-095): best effort, warn never fail; skipped
|
# Same contract as the pushes (BDR-095): best effort, warn never fail; skipped
|
||||||
# under GITFLOW_NO_PUSH=1, gitflow.autopush=false or no origin. The REMOTE tip
|
# under GITFLOW_NO_PUSH=1, gitflow.autopush=false or no origin. The REMOTE tip
|
||||||
@@ -153,8 +184,11 @@ gitflow_merged_into_base() {
|
|||||||
_gitflow_delete_remote() {
|
_gitflow_delete_remote() {
|
||||||
local br="$1" out rc tip
|
local br="$1" out rc tip
|
||||||
[ "${GITFLOW_NO_PUSH:-0}" = 1 ] && return 0
|
[ "${GITFLOW_NO_PUSH:-0}" = 1 ] && return 0
|
||||||
[ "$(git config --bool --default true gitflow.autopush)" = false ] && return 0
|
|
||||||
git remote get-url origin >/dev/null 2>&1 || return 0
|
git remote get-url origin >/dev/null 2>&1 || return 0
|
||||||
|
if _gitflow_push_off; then
|
||||||
|
_gitflow_note_remote_left "$br"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
gitflow_protected_base "$br" && return 0
|
gitflow_protected_base "$br" && return 0
|
||||||
out="$(_gitflow_timeout git ls-remote --exit-code --heads origin "refs/heads/$br" 2>/dev/null)"; rc=$?
|
out="$(_gitflow_timeout git ls-remote --exit-code --heads origin "refs/heads/$br" 2>/dev/null)"; rc=$?
|
||||||
[ "$rc" -eq 2 ] && return 0 # no remote copy — nothing to remove
|
[ "$rc" -eq 2 ] && return 0 # no remote copy — nothing to remove
|
||||||
@@ -175,6 +209,17 @@ _gitflow_delete_remote() {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# _gitflow_checkout_containing_base <br> → leave <br>, landing on the base that
|
||||||
|
# contains it (develop first, main for a branch merged into main only).
|
||||||
|
_gitflow_checkout_containing_base() {
|
||||||
|
local br="$1"
|
||||||
|
if git merge-base --is-ancestor "$br" "$GITFLOW_DEVELOP" 2>/dev/null; then
|
||||||
|
git checkout -q "$GITFLOW_DEVELOP"
|
||||||
|
else
|
||||||
|
git checkout -q "$GITFLOW_MAIN"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
# gitflow_delete <branch> → the one sanctioned way to delete a branch, local
|
# gitflow_delete <branch> → the one sanctioned way to delete a branch, local
|
||||||
# copy then origin copy. finish calls it after its merges; the CLI exposes it
|
# copy then origin copy. finish calls it after its merges; the CLI exposes it
|
||||||
# for a branch merged elsewhere (a Gitea PR, a hand merge). Refuses, branch
|
# for a branch merged elsewhere (a Gitea PR, a hand merge). Refuses, branch
|
||||||
@@ -192,7 +237,11 @@ gitflow_delete() {
|
|||||||
echo "gitflow: REFUSED — '$br' is not merged into $GITFLOW_DEVELOP or $GITFLOW_MAIN — branch kept" >&2
|
echo "gitflow: REFUSED — '$br' is not merged into $GITFLOW_DEVELOP or $GITFLOW_MAIN — branch kept" >&2
|
||||||
return 5
|
return 5
|
||||||
fi
|
fi
|
||||||
git checkout -q "$GITFLOW_DEVELOP" 2>/dev/null || git checkout -q "$GITFLOW_MAIN" 2>/dev/null
|
_gitflow_checkout_containing_base "$br"
|
||||||
|
# LRN-161: `-d` judges against the upstream when one is set, against HEAD
|
||||||
|
# otherwise. The ancestor check above is the real gate, so HEAD must be the
|
||||||
|
# base that contains <br> and a lagging upstream (manual mode) must go.
|
||||||
|
git branch -q --unset-upstream "$br" 2>/dev/null || true
|
||||||
git branch -q -d "$br" || { echo "gitflow: git refused to delete '$br' — branch kept" >&2; return 5; }
|
git branch -q -d "$br" || { echo "gitflow: git refused to delete '$br' — branch kept" >&2; return 5; }
|
||||||
_gitflow_delete_remote "$br"
|
_gitflow_delete_remote "$br"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,4 +38,42 @@ check T8-dirty-start-reported "$(has "$(fire SessionStart "$PWD")" "uncommitted"
|
|||||||
out=$(jq -n --arg d "$PWD" '{hook_event_name:"SessionStart", cwd:$d}' | bash "$H" 2>/dev/null)
|
out=$(jq -n --arg d "$PWD" '{hook_event_name:"SessionStart", cwd:$d}' | bash "$H" 2>/dev/null)
|
||||||
check T9-start-adds-context "$(printf '%s' "$out" | jq -r '.hookSpecificOutput.hookEventName')" SessionStart
|
check T9-start-adds-context "$(printf '%s' "$out" | jq -r '.hookSpecificOutput.hookEventName')" SessionStart
|
||||||
|
|
||||||
|
# ── manual-push mode (gitflow.autopush=false) ──
|
||||||
|
git checkout -q -- a
|
||||||
|
git config gitflow.autopush false
|
||||||
|
check T10-manual-clean-start "$(fire SessionStart "$PWD")" silent
|
||||||
|
check T10-manual-clean-stop "$(fire Stop "$PWD")" silent
|
||||||
|
echo m>m; git add m; git commit -q -m m
|
||||||
|
git branch side HEAD; git checkout -q side; echo s>s; git add s; git commit -q -m s
|
||||||
|
git checkout -q -
|
||||||
|
check T11-manual-stop-silent "$(fire Stop "$PWD")" silent
|
||||||
|
out=$(fire SessionStart "$PWD")
|
||||||
|
check T11-manual-info "$(has "$out" "manual push mode")" yes
|
||||||
|
check T11-manual-count "$(has "$out" "2 commit(s)")" yes
|
||||||
|
check T11-manual-lists-branch "$(has "$out" "side")" yes
|
||||||
|
check T11-manual-no-warning "$(has "$out" "unpushed work")" no
|
||||||
|
git checkout -q -b fresh
|
||||||
|
check T12-fresh-branch-repo-wide "$(has "$(fire SessionStart "$PWD")" "2 commit(s)")" yes
|
||||||
|
git checkout -q -
|
||||||
|
git push -q origin HEAD side 2>/dev/null; echo d>>a
|
||||||
|
out=$(fire SessionStart "$PWD")
|
||||||
|
check T13-dirty-info "$(has "$out" "manual push mode")" yes
|
||||||
|
check T13-dirty-uncommitted "$(has "$out" "uncommitted")" yes
|
||||||
|
check T13-dirty-no-commit-clause "$(has "$out" "commit(s) not on origin")" no
|
||||||
|
check T13-dirty-stop-silent "$(fire Stop "$PWD")" silent
|
||||||
|
git checkout -q -- a
|
||||||
|
git config gitflow.autopush flase
|
||||||
|
echo i>i; git add i; git commit -q -m i
|
||||||
|
out=$(fire SessionStart "$PWD")
|
||||||
|
check T14-invalid-named "$(has "$out" "not a boolean")" yes
|
||||||
|
check T14-invalid-treated-auto "$(has "$out" "unpushed work")" yes
|
||||||
|
check T14-invalid-stop-auto "$(has "$(fire Stop "$PWD")" "1 commit(s)")" yes
|
||||||
|
git config --unset gitflow.autopush
|
||||||
|
check T15-unset-auto-intact "$(has "$(fire Stop "$PWD")" "1 commit(s)")" yes
|
||||||
|
git config gitflow.autopush false; git remote remove origin
|
||||||
|
out=$(fire SessionStart "$PWD")
|
||||||
|
check T16-no-origin-manual "$(has "$out" "manual push mode")" yes
|
||||||
|
check T16-no-origin-clause "$(has "$out" "no 'origin' remote")" yes
|
||||||
|
check T16-no-origin-stop-silent "$(fire Stop "$PWD")" silent
|
||||||
|
|
||||||
printf 'PASS=%s FAIL=%s\n' "$pass" "$fail"; [ "$fail" -eq 0 ]
|
printf 'PASS=%s FAIL=%s\n' "$pass" "$fail"; [ "$fail" -eq 0 ]
|
||||||
|
|||||||
Reference in New Issue
Block a user