You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
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
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.
Pull fast-forwards to a tag. The remote has tag onlytag and no branch of that name.
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.
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.
Priority: High. DeletingRemoteBranch() can delete a remote tag and report success, and Pull().WithBranch(x) can fast-forward or merge a tag. That's silent, hard-to-reverse damage on a shared remote from an API that promises to act on a branch.
Area / suggested assignee:GitPushBuilder / GitPullBuilder refspec construction (AppendRefspec, AppendOperands). Owner: @matt-edmondson
Next step: qualify operands as refs/heads/<name> for push, delete and pull, refuse DeletingRemoteBranch() without a branch in BuildArguments, and add the local-bare-remote tests from the issue (tag-only delete, tag-only pull, and the branch/tag name collision).
Activity
matt-edmondson commentedon Oct 19, 2026
Triage
DeletingRemoteBranch()can delete a remote tag and report success, andPull().WithBranch(x)can fast-forward or merge a tag. That's silent, hard-to-reverse damage on a shared remote from an API that promises to act on a branch.GitPushBuilder/GitPullBuilderrefspec construction (AppendRefspec,AppendOperands). Owner: @matt-edmondsonrefs/heads/<name>for push, delete and pull, refuseDeletingRemoteBranch()without a branch inBuildArguments, and add the local-bare-remote tests from the issue (tag-only delete, tag-only pull, and the branch/tag name collision).Generated by Claude Code