Skip to content

Fully-qualify branch refspecs when pushing#165

Open
skarim wants to merge 2 commits into
skarim/reject-symlink-templatesfrom
skarim/branch-name-refspec-fix
Open

Fully-qualify branch refspecs when pushing#165
skarim wants to merge 2 commits into
skarim/reject-symlink-templatesfrom
skarim/branch-name-refspec-fix

Conversation

@skarim

@skarim skarim commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

gh stack push, submit, sync, and link push stack branches by passing their names to git push. Branch names were used as raw refspec operands: the force path built <branch>:refs/heads/<branch> and the non-force path passed bare branch names. A git refspec treats a leading + as "force update", and Git allows branch names that begin with +, so a branch named +feature was parsed as refspec syntax for feature — the force path pushed local feature into remote +feature, and the non-force path force-updated remote feature.

This builds fully-qualified refspecs for both the source and destination of every push, so a branch name can never be reinterpreted as a refspec modifier. It mirrors the fully-qualified form gh pr create uses.

Behavior

  • Every branch is pushed with an explicit refs/heads/<branch>:refs/heads/<branch> refspec; a leading + (or any other refspec-significant character) is treated as part of the branch name.
  • Force updates are still requested through the existing --force-with-lease flags — their ref names were already fully-qualified — so the force / non-force behavior of push, submit, sync, and link is otherwise unchanged.

Changes

internal/git/gitops.go:

  • Push builds refs/heads/<branch>:refs/heads/<branch> refspecs in both the force and non-force paths, replacing the bare source (<branch>:refs/heads/<branch>) and the bare branch operands.
  • DeleteRemoteBranch deletes refs/heads/<branch> rather than a bare branch name.
  • FetchBranches already used fully-qualified refspecs and is unchanged.

The Push signature and every call site (push / submit / sync / link) are unchanged.

Testing

internal/git/gitops_test.go:

  • Adds real-git integration tests for the force and non-force paths that push a +-prefixed branch and assert the remote +feature ref receives the +feature commit while the sibling feature ref is left untouched.

Stack created with GitHub Stacks CLIGive Feedback 💬

gh-stack builds `git push` arguments from stack branch names in `internal/git`.
The force path passed `<branch>:refs/heads/<branch>`, and the non-force path
passed bare branch names. A git refspec treats a leading `+` as "force update",
and Git allows branch names that begin with `+`, so a branch named `+feature`
was parsed as refspec syntax for `feature`: the force path pushed local
`feature` into remote `+feature`, and the non-force path force-updated remote
`feature`.

Build fully-qualified refspecs for both the source and destination of every
push: `refs/heads/<branch>:refs/heads/<branch>`. A branch name can no longer be
reinterpreted as a refspec modifier. Force updates are still requested via the
existing `--force-with-lease` flags, whose ref names were already
fully-qualified. `DeleteRemoteBranch` is fully-qualified the same way.

The `Push` signature and every call site are unchanged. Add real-git
integration tests covering the force and non-force paths with a `+`-prefixed
branch.
Copilot AI review requested due to automatic review settings July 9, 2026 04:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens branch pushing/deletion in the git layer so that branch names beginning with + (or other refspec-significant characters) can never be reinterpreted as git refspec modifiers. Previously the force path built <branch>:refs/heads/<branch> and the non-force path passed bare branch names, so a branch named +feature was parsed as force syntax targeting feature. The change makes every push/delete use fully-qualified refs/heads/<branch> refspecs on both sides, mirroring the form gh pr create uses.

Changes:

  • Push now builds refs/heads/<b>:refs/heads/<b> refspecs in both the force and non-force paths.
  • DeleteRemoteBranch deletes refs/heads/<branch> instead of a bare branch name.
  • Adds real-git integration tests covering the force and non-force paths with a +-prefixed branch, asserting the correct remote ref is updated and the sibling feature ref is untouched.
Show a summary per file
File Description
internal/git/gitops.go Fully-qualifies refspecs in Push (force + non-force) and DeleteRemoteBranch so branch names can't be parsed as refspec syntax.
internal/git/gitops_test.go Adds TestIntegration_Push_PlusPrefixedBranch_Force/_NonForce verifying a +-prefixed branch pushes its own ref without disturbing a similarly named sibling.

Review details

  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Medium

Comment thread internal/git/gitops.go Outdated
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.

2 participants