fix(upgrade): fast-forward first; reset --hard only behind a proved-safe gate (#2517)

/gstack-upgrade went straight to stash + reset --hard origin/main. Now it
tries git pull --ff-only --autostash first (the same policy session-update's
auto-upgrade uses). The destructive fallback runs unprompted ONLY when both
git status --porcelain AND git rev-list origin/main..HEAD are empty — a
clean tree with unpushed local commits is NOT safe, reset destroys them.
Anything else requires an explicit one-way-door confirmation that lists every
dirty file and unpushed commit being discarded.

Fixes #2517.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-08-17 10:53:36 -07:00
co-authored by Claude Fable 5
parent c856472034
commit 6ad9db51fb
+32 -10
View File
@@ -123,20 +123,42 @@ OLD_VERSION=$(cat "$INSTALL_DIR/VERSION" 2>/dev/null || echo "unknown")
Use the install type and directory detected in Step 2:
**For git installs** (global-git, local-git):
Fast-forward first (#2517) — the same policy the session-update auto-upgrade
uses. `--autostash` carries local edits over the pull; render-footprint dirt
is discarded first because it is regenerable and poisons stashes (#2569):
```bash
cd "$INSTALL_DIR"
# Discard render-footprint dirt BEFORE stashing (#2569): pre-v1.67
# gbrain-enabled installs ran gen:skill-docs:user IN PLACE, leaving
# generated SKILL.md / sections/*.md files permanently modified. Stashing
# that dirt poisons the stash: the post-upgrade `git stash pop` would
# restore STALE generated markdown over the fresh checkout permanently.
# These files are regenerable (setup re-renders brain-aware variants to
# ~/.gstack/render), so discarding is lossless; anything else the user
# changed still reaches the stash untouched. Same file classification as
# migrations/v1.67.0.0.sh, which remains for manual git-pull flows.
# Discard render-footprint dirt (#2569): pre-v1.67 gbrain-enabled installs
# ran gen:skill-docs:user IN PLACE, leaving generated SKILL.md / sections
# files permanently modified. They are regenerable (setup re-renders to
# ~/.gstack/render), so discarding is lossless.
git checkout -- 'SKILL.md' '*/SKILL.md' '*/sections/*.md' 2>/dev/null || true
STASH_OUTPUT=$(git stash 2>&1)
git fetch origin
git pull --ff-only --autostash origin main && ./setup && echo "FF_OK"
```
If the output ends with `FF_OK`, the upgrade is done — skip the fallback
below entirely.
**Fallback (ff-only refused — local commits or divergence).** `git reset
--hard` DESTROYS things: a clean tree with unpushed local commits still loses
those commits. Gate it (#2517):
1. Run `git status --porcelain` and `git rev-list origin/main..HEAD --oneline`
in `$INSTALL_DIR`.
2. If BOTH are empty, the reset is provably safe — run the fallback block
below without asking.
3. Otherwise ask via AskUserQuestion (one-way door — destructive), listing
exactly what will be discarded: each dirty file and each unpushed commit
by hash + subject. Options: **A)** Discard them and upgrade (reset) —
requires the explicit letter; **B)** Abort the upgrade so the user can
rescue their work first (recommended when local commits exist). Never
proceed on a vague reply.
```bash
cd "$INSTALL_DIR"
STASH_OUTPUT=$(git stash 2>&1)
git reset --hard origin/main
./setup
```