Skip to content

fix: gstack-upgrade uses ff-only merge before the hard reset#1033

Open
chaotix345 wants to merge 1 commit intogarrytan:mainfrom
chaotix345:fix/ff-only-upgrade
Open

fix: gstack-upgrade uses ff-only merge before the hard reset#1033
chaotix345 wants to merge 1 commit intogarrytan:mainfrom
chaotix345:fix/ff-only-upgrade

Conversation

@chaotix345
Copy link
Copy Markdown

Problem

gstack-upgrade calls git reset --hard origin/main as its advance-HEAD step. This trips destructive-command blockers: Claude Code PreToolUse hooks, pre-commit wrappers, /careful, /guard, and similar safety tools commonly flag the hard reset as destructive.

Hit this today on Windows 11 with auto_upgrade: true and a Claude Code guard hook that blocks destructive git operations. Upgrade aborted mid-flow; had to manually work around the block.

Fix

Try git merge --ff-only origin/main first. Falls back to the old behavior if fast-forward fails (divergent history, local commits on the skills repo).

Also switches git stash to git stash push --include-untracked -m "pre-upgrade-<date>" so untracked build artifacts (.bak/, compiled .exe binaries from prior runs) don't block the merge, and the stash is self-describing for anyone inspecting it later.

Why this is safe

  • Clean global install with no local commits: merge --ff-only succeeds, identical end state to the hard reset, no destructive flag ever fires.
  • Divergent history (user committed locally to the skills repo): falls back to the old behavior, same as before.
  • Dirty working tree: handled by the git stash push --include-untracked above.

No behavior change for the success path, just a less destructive primary command.

Testing

Ran on Windows 11 with a Claude Code PreToolUse guard that blocks destructive git operations. Upgraded from v0.16.2.0 to v0.18.1.0 cleanly via the ff-only path. Setup regenerated customizations as expected.

Note

This PR only touches gstack-upgrade/SKILL.md.tmpl. The generated gstack-upgrade/SKILL.md will pick up the change on the next bun run gen:skill-docs or CI regen, so maintainer can regen as part of merge or leave it to the next templated build.

The upgrade flow uses `git reset --hard origin/main` to advance HEAD.
This trips destructive-command blockers (Claude Code PreToolUse hooks,
pre-commit wrappers, /careful, /guard) and can abort auto-upgrades
mid-flow.

Switch to `git merge --ff-only origin/main` as the primary path. Falls
back to the old behavior if fast-forward fails (divergent history,
local commits). Clean global installs never hit the destructive flag.

Also switches `git stash` to `git stash push --include-untracked -m
"pre-upgrade-<date>"` so untracked build artifacts (.bak/, compiled
binaries) don't block the merge, and the stash is self-describing.

Tested on Windows 11 with a Claude Code PreToolUse guard that blocks
destructive git operations. Upgraded from v0.16.2.0 to v0.18.1.0
cleanly via the ff-only path, setup regenerated customizations as
expected.
@chaotix345
Copy link
Copy Markdown
Author

Hit an edge case today that this PR's ff-only path doesn't fully cover, logging for context (not a blocker for merge).

Environment: Windows 11, Git for Windows default (core.autocrlf=true), pull.rebase=true globally, Claude Code with /guard + /careful hooks active. Upgrading from v0.18.1.0 → v1.5.1.0 via auto_upgrade: true.

What happened:

  1. git status in ~/.claude/skills/gstack/ shows ~40 skill files as M. All diffs are LF→CRLF conversion at checkout time, no semantic changes (git diff --ignore-cr-at-eol HEAD is empty).
  2. The stash line in this PR runs successfully and prints stash@{0}: On main: pre-upgrade-YYYY-MM-DD, but the working tree stays flagged as dirty — the CRLF-converted-on-checkout files are effectively re-dirtied by autocrlf during subsequent reads.
  3. git merge --ff-only origin/main refuses the ff because the tree is still considered dirty (the pull.rebase=true config also surfaces as error: cannot pull with rebase: You have unstaged changes on git pull --ff-only).
  4. Falls through to the hard-reset fallback, which my /guard hook blocks. Upgrade aborts mid-flow with a stale install pointing at old HEAD.

Workaround I used: renamed ~/.claude/skills/gstack/ out of the way, re-cloned fresh from origin, re-ran ./setup. Config in ~/.gstack/ is preserved across re-clone so no data loss, but it's manual and defeats auto_upgrade.

What I tried that doesn't work: detecting the CRLF-only case inside the upgrade flow with a pre-stash git -c core.autocrlf=false checkout-index --all --force. The checkout-index rewrites the worktree as LF, which under a global autocrlf=true then looks dirty in the opposite direction (git expects CRLF on disk, sees LF, flags modified). Cure worse than disease. Flagging this so nobody else burns time on the same path.

Possible follow-up that should actually work — no expectation you pick this up, just throwing it out:

  • Add a .gitattributes to the gstack repo with * -text (or scoped to **/*.md + **/*.sh). Disables line-ending conversion for the tracked files entirely, which eliminates the phantom-modified state at the source and sidesteps all of the above. Orthogonal to this PR — both can land.
  • Alternatively, ./setup could run git config --local core.autocrlf false inside the skill repo during first install, confining the fix to the repo and leaving the user's global config untouched.

Happy to open a separate PR for the .gitattributes approach if that direction is welcome.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant