fix push for branches with stale tracking refs#124
Open
skarim wants to merge 1 commit into
Open
Conversation
gh stack sync could rebase a stack successfully then fail the final force push with "stale info" when a branch lacked a local tracking ref (refs/remotes/<remote>/<branch>). This happened because: 1. FetchBranches pre-filtered branches by existing tracking ref, so a branch with no tracking ref was never fetched and never gained one. 2. Push used a bare --force-with-lease flag, which has no lease basis for a branch without a tracking ref, causing git to reject the push. FetchBranches now uses explicit refspecs for every branch: +refs/heads/<branch>:refs/remotes/<remote>/<branch> This creates or updates tracking refs regardless of prior state. The fast-path (single fetch) and per-branch fallback (for branches absent on the remote) are preserved. Push now builds explicit per-branch lease arguments when force=true: --force-with-lease=refs/heads/<branch>:<tracking-ref-sha> for branches with a tracking ref, or: --force-with-lease=refs/heads/<branch>: (empty expected value = "must not exist") for branches absent on the remote. Explicit destination refspecs (<branch>:refs/heads/<branch>) remove dependence on push.default and upstream configuration. The non-force push path is unchanged. Added 6 integration tests using real bare git remotes: - Branch with current tracking ref: push succeeds - Tracking ref deleted locally (regression test for #118): push succeeds - Remote advanced by another client: push rejected (safety preserved) - New branch absent on remote: created via empty-expect lease - New branch race condition: rejected (safety preserved) - Mixed stack (tracked + untracked branches): all succeed after fetch Fixes #118
dc07ed9 to
44aac93
Compare
Contributor
There was a problem hiding this comment.
Pull request overview
This PR fixes issue #118 where gh stack sync could fail during force push when branches lack local tracking refs (refs/remotes/<remote>/<branch>). The fix addresses two root causes: FetchBranches skipping branches without existing tracking refs, and Push using bare --force-with-lease which has no lease basis without a tracking ref.
Changes:
FetchBranchesnow builds explicit refspecs for all requested branches, creating/updating tracking refs regardless of prior statePushwithforce=truenow resolves each branch's tracking ref SHA and uses explicit--force-with-lease=refs/heads/<branch>:<sha>arguments with explicit destination refspecs- Added 6 integration tests covering all scenarios (existing branch, missing tracking ref, remote advanced, new branch, race condition, mixed stack)
Show a summary per file
| File | Description |
|---|---|
| internal/git/gitops.go | Rewrites FetchBranches to remove tracking-ref pre-filter and use explicit refspecs; rewrites Push force path to use per-branch explicit lease SHAs and refspecs |
| internal/git/gitops_test.go | Adds integration tests using real bare git repos covering all combinations of tracking ref state and remote branch state |
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 2/2 changed files
- Comments generated: 0
georgebrock
approved these changes
Jun 12, 2026
georgebrock
left a comment
Member
There was a problem hiding this comment.
The new approach makes sense to me 👍
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
gh stack synccan rebase a stack successfully, then fail the final force push withstale infowhen a branch lacks a local tracking ref (refs/remotes/<remote>/<branch>). This is most visible in checkouts where local tracking refs have been pruned or never existed.Root cause is two layers:
FetchBranchespre-filters branches by existing tracking ref, so a branch with no tracking ref is never fetched and never gains one.Pushuses a bare--force-with-leaseflag, which has no lease basis for a branch without a tracking ref — git rejects the push.The fix addresses both layers while preserving
--force-with-leasesafety (never overwrite work we have not seen).Behavior matrix (after fix)
Changes
internal/git/gitops.go:FetchBranches: Remove pre-filter that skipped branches without existing tracking refs. Build explicit refspecs (+refs/heads/<branch>:refs/remotes/<remote>/<branch>) for every requested branch, creating or updating tracking refs regardless of prior state. The+prefix allows non-fast-forward tracking-ref updates. Keep the existing fast-path (single fetch for all branches) and per-branch fallback (one missing remote branch does not block the rest). Per-branch failures are tolerated — a branch absent on the remote simply gets no tracking ref, which is correct.Push: Whenforce=true, resolve each branch's tracking ref viarev-parse --verify --quiet refs/remotes/<remote>/<branch>. If found, emit--force-with-lease=refs/heads/<branch>:<sha>(explicit SHA lease). If missing (branch genuinely absent on remote), emit--force-with-lease=refs/heads/<branch>:(empty expected value = "must not exist"). Use explicit destination refspecs (<branch>:refs/heads/<branch>) to remove dependence onpush.defaultand upstream configuration. Theforce=falsepath is unchanged.internal/git/gitops_test.go:Other callers
cmd/push.gocallsFetchBranchesthenPushwithforce=true— benefits automaticallycmd/submit.gocallsPushwithforce=trueper-branch — benefits automaticallycmd/link.gocallsPushwithforce=false— unaffected (non-force path unchanged)Stack created with GitHub Stacks CLI • Give Feedback 💬