`gh stack push` silently discards commits that exist only on the remote (fetch-then-lease defeats --force-with-lease)
Dieses Issue hat noch niemand übernommen.
Bewertung
- Schwierigkeit
- 4/5
- Geschätzter Aufwand
- 3-5 Tage
- Anfängerfreundlichkeit
- 55/100
Rechercherichtung
Führe zunächst die bereitgestellte vollständig lokale Reproduktion mit einem Bare-Repository aus und untersuche anschließend die Implementierung von gh stack push und vergleiche sie mit gh stack sync. Überprüfe, wie der Stack-Zustand in .git/gh-stack erfasst wird und wie der Fetch die Tracking-Refs aktualisiert. Als erledigt gilt die Aufgabe, wenn nur auf dem Remote vorhandene Commits erhalten bleiben und der Befehl entweder sicher ablehnt oder die erforderliche Synchronisierung meldet, wobei die Reproduktion den CI-Commit nicht mehr verliert.
Vom Indexierungsmodell aus dem Issue-Text verfasst.
Beschreibung
Summary
gh stack push fetches the stack branches and then uses the just-fetched SHA as the --force-with-lease expected value. When a stack branch on the remote contains commits the local branch does not have — for example a CI job that commits generated artifacts back onto the PR head branch — gh stack push silently rewinds the remote branch and those commits become unreachable. It exits 0 and reports ✓ Pushed N branches.
In the identical repository state, plain git push --force-with-lease refuses with ! [rejected] (stale info).
This looks like an unintended consequence of the fix for #118. That issue asked for the lease source to be the fetched tracking ref, and its safety argument covers the narrow race "someone updates b1 after the fetch". It does not cover the much more common case where the remote branch already had commits the local branch never incorporated — for those, fetch-then-lease makes the lease vacuous by construction.
Environment
gh2.97.0gh-stackv0.1.0- git 2.39.5
- darwin/arm64
Reproduction
Fully local — uses a bare repo as origin, no GitHub access required.
#!/usr/bin/env bash
set -euo pipefail
ROOT=$(mktemp -d)
git init -q --bare -b main "$ROOT/origin.git"
# --- developer clone ---
git init -q -b main "$ROOT/work"
cd "$ROOT/work"
git config user.email dev@example.com
git config user.name dev
git config rerere.enabled true
git remote add origin "$ROOT/origin.git"
printf 'base\n' > base.txt
git add base.txt && git commit -q -m "base commit"
git push -q origin main
# --- build a two-layer stack and push it ---
gh stack init --base main layer1 >/dev/null
printf 'layer1\n' > layer1.txt && git add layer1.txt && git commit -q -m "layer1"
gh stack add layer2 >/dev/null
printf 'layer2\n' > layer2.txt && git add layer2.txt && git commit -q -m "layer2"
gh stack push >/dev/null 2>&1
# --- a CI job commits generated output back onto the remote layer1 ---
git clone -q "$ROOT/origin.git" "$ROOT/ci"
cd "$ROOT/ci"
git config user.email ci@example.com
git config user.name ci-bot
git checkout -q layer1
printf 'generated\n' > generated.txt
git add generated.txt && git commit -q -m "ci: regenerate artifacts"
git push -q origin layer1
CI_SHA=$(git rev-parse HEAD)
# --- back in the developer clone, WITHOUT fetching ---
cd "$ROOT/work"
echo "tracking ref (stale) = $(git rev-parse origin/layer1)"
echo "actual remote = $(git ls-remote origin refs/heads/layer1 | cut -f1)"
echo "### plain git:"
git push --force-with-lease origin layer1 || true
echo "### gh stack push:"
gh stack push || true
AFTER=$(git ls-remote origin refs/heads/layer1 | cut -f1)
[ "$AFTER" = "$CI_SHA" ] && echo "CI commit preserved" || echo "CI commit LOST (remote rewound to $AFTER)"
git reflog show origin/layer1 | head -3
Observed output
tracking ref (stale) = 1f13a4980b1c0b6bd679874fdc47f6d87c2a26cb
actual remote = 1a28a7e24b2014437d0e24d94a4489b3a6ac896f
### plain git:
! [rejected] layer1 -> layer1 (stale info)
error: failed to push some refs to '.../origin.git'
### gh stack push:
Pushing 2 branches to origin...
✓ Pushed 2 branches
To create PRs for this stack, run `gh stack submit`
exit code: 0
CI commit LOST (remote rewound to 1f13a4980b1c0b6bd679874fdc47f6d87c2a26cb)
The reflog of the tracking ref shows the lease being refreshed immediately before the push:
1f13a49 refs/remotes/origin/layer1@{0}: update by push
1a28a7e refs/remotes/origin/layer1@{1}: fetch origin +refs/heads/layer1:refs/remotes/origin/layer1 +refs/heads/layer2:refs/remotes/origin/layer2: fast-forward
1f13a49 refs/remotes/origin/layer1@{2}: update by push
@{1} is gh stack push's own fetch picking up the CI commit; @{0} is the force push discarding it.
Expected behavior
gh stack push should not silently discard commits that exist only on the remote. Either:
- refuse the push and tell the user to run
gh stack sync(which is already safe — it fast-forwards the local branch onto the remote first), or - lease against the SHA
gh-stackitself last pushed (it already tracks stack state in.git/gh-stack) instead of a freshly fetched value, or - keep the current behavior behind an explicit opt-in flag.
A check along the lines of "every commit reachable from the remote ref is either reachable from the local branch or was replayed by the rebase that just ran" would cover the CI case while still allowing normal stack rebases.
Why this matters
Committing generated output back onto the PR head branch is a common CI pattern — regenerated API clients, lockfiles, schema dumps, formatting. In our repository two workflows do exactly that, so any lower layer of a stack routinely has commits that only exist on the remote. gh stack push drops them without any warning, and the layer merges with generated files that no longer match the source.
gh stack sync handles this correctly (✓ Fast-forwarded layer1 to <sha> before rebasing and pushing), so the practical workaround is to never call gh stack push on its own. The concern is that the unsafe path is the quieter one: it reports success rather than failing loudly.
Happy to test a patch if that helps.
- Vorherrschende Sprache
- Go
- Sterne
- 1.5k
- Forks
- 73
- Ø Merge
- 1 T. 8 Std.
- Gemergte PRs (30 T.)
- 7
Beitragsleitfaden
Erste Schritte
- Lesen Sie das ganze Issue und danach den Beitragsleitfaden des Projekts.
- Schreiben Sie ins Issue, dass Sie es übernehmen — das erspart doppelte Arbeit.
- Forken Sie das Repository und arbeiten Sie in einem Branch.
- Öffnen Sie einen Pull Request, der die Issue-Nummer nennt.
Mehr aus github/gh-stack
-
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 85/100
-
Schwierigkeit 1/5 Unter einer Stunde Anfängerfreundlichkeit 92/100
-
feature request topic: cli - general
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 68/100
-
feature request topic: auto-merge
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 68/100
-
bug topic: docs
Schwierigkeit 1/5 Unter einer Stunde Anfängerfreundlichkeit 68/100
Alle Issues in github/gh-stack
Ähnliche Issues
-
textual definition
Schwierigkeit 1/5 Unter einer Stunde Anfängerfreundlichkeit 90/100
geneontology/go-ontology#32653 ·
-
Schwierigkeit 1/5 Unter einer Stunde Anfängerfreundlichkeit 75/100
-
needs design
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 75/100
-
Priority/High ready-for-agent Severity/Major Type/Bug
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 75/100
-
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 70/100