Skip to content

Push().WithBranch(x).DeletingRemoteBranch() deletes the remote tag x when no branch x exists; push/pull WithBranch can act on tags because operands aren't qualified as refs/heads/ #258

Open

Description

@matt-edmondson

What's wrong

GitPushBuilder and GitPullBuilder take the branch as a GitBranchName, but they pass it to git as a bare short name:

  • GitIntegration/Builders/GitPushBuilder.cs:260: AppendOperands(arguments, _remote.WeakString, _branch.WeakString)
  • GitIntegration/Builders/GitPullBuilder.cs:272: the same call

Git resolves a bare refspec by DWIM against every ref namespace, not only refs/heads/. So the API promises a branch, but git can act on a tag instead.

Reproduced on git 2.43 against a local bare remote

  1. Delete removes a tag. The remote has tag v1 and no branch v1.
    • Call: Push().ToRemote(origin).WithBranch("v1").DeletingRemoteBranch()
    • Command run: git push --porcelain --delete --end-of-options origin v1
    • Result: git exits 0 and prints - :refs/tags/v1 [deleted]. The remote tag is gone, ExecuteAsync reports success, and HasRejections is false.
    • git push origin :<tag> is git's documented way to delete a tag, so git is behaving as designed. A method named DeletingRemoteBranch should never do this.
  2. Pull fast-forwards to a tag. The remote has tag onlytag and no branch of that name.
    • Call: Pull().FromRemote(origin).WithBranch("onlytag").FastForwardOnly()
    • Result: git prints * tag onlytag -> FETCH_HEAD, fast-forwards the current branch to the tag's commit, and exits 0.
    • Without FastForwardOnly(), the same call merges the tag.
  3. Push fails, or pushes a tag, when names collide locally. This is the case Branches()/Tags() report "heads/v1" / "tags/v1" when a branch and a tag share a name, so the name can't be passed back to git #137 handled for listing.
    • A local branch and a local tag are both named v2: Push().ToRemote(origin).WithBranch("v2") fails with error: src refspec v2 matches more than one. There are no porcelain records, so it surfaces as a generic GitCommandException.
    • Only a local tag v2 exists, with no branch: the tag is pushed.

Related: a delete with no branch isn't validated

DeletingRemoteBranch() without WithBranch(...) builds git push --delete origin, which dies with fatal: --delete doesn't make sense without any refs. AppendRefspec (GitPushBuilder.cs:238-261) already refuses a branch with no remote in BuildArguments, but it doesn't refuse a delete with no branch.

Suggested fix

  • Push: emit refs/heads/<name> as the source refspec, with no :dst, so git still updates the remote branch of the same name. For --delete, emit refs/heads/<name>. Checked on git 2.43:
    • git push --porcelain --set-upstream origin refs/heads/v2 still sets branch.v2.remote and branch.v2.merge.
    • git push --delete origin refs/heads/v1 leaves refs/tags/v1 alone.
    • Porcelain output already prints full ref names, so GitPushParser needs no change.
  • Pull: emit refs/heads/<name>. git pull origin refs/heads/onlytag correctly fails with couldn't find remote ref refs/heads/onlytag.
  • Validation: throw InvalidOperationException from BuildArguments when DeletingRemoteBranch() is set without WithBranch(...). This matches the existing branch-without-remote guard.

Acceptance criteria

  • Builder tests assert the qualified refspec for WithBranch:
    • on push, without DeletingRemoteBranch
    • on push, with DeletingRemoteBranch
    • on pull
  • Integration tests in GitRemoteSyncTests:
    • DeletingRemoteBranch on a name that exists only as a remote tag leaves the tag in place and doesn't report a deletion.
    • Push WithBranch(x) succeeds when a local branch and a local tag are both named x.
    • Pull WithBranch(x) fails, rather than merging, when only a remote tag x exists.
  • DeletingRemoteBranch() without WithBranch throws InvalidOperationException from BuildArguments.

Activity

  1. matt-edmondson commented on Oct 19, 2026

    @matt-edmondson
    ContributorAuthor

    Triage


    Generated by Claude Code

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions