ensure_safe_refname_fragment (src/git/validate.rs:30) explicitly permits tabs, with a test pinning the behaviour:
#[test]
fn allows_tab() {
// Tabs are technically allowed in refs (rare but not unsafe).
assert!(ensure_safe_refname_fragment("foo\tbar", "tag").is_ok());
}
But every consumer of git ls-remote output parses it as tab-delimited:
parse_ls_remote_tags (src/git/push.rs:32): line.split_once('\t')
verify_remote_branch (src/git/push.rs:126): line.split_once('\t')
ls-remote emits <sha>\t<refname>. A refname containing a tab produces a line with two tabs, so split_once yields refname = "foo" and the rest is dropped. The tag is then absent from the returned map, or matched under a truncated name.
Concretely, for a tag like release\tcandidate (or any name a tagTemplate produces with a tab in a scope):
remote_tag_target_shas reports the tag as not present on the remote, so the "already at the right sha" fast path is skipped and a floating-tag force-push happens that shouldn't.
verify_remote_branch never matches expected_ref and fails with "Remote branch not found after push" even though the push succeeded.
Neither is dangerous — worst case is a redundant force-push or a false negative on verification — but it is a validator and a parser that disagree, and the validator has a test asserting the disagreement is intentional.
Fix
Pick one:
- Reject tabs in the validator. Simplest, and defensible: git allows them, but they break the plumbing format that git itself uses to report refs, so nothing downstream can round-trip them. One-line change plus flipping
allows_tab to rejects_tab.
- Split on the first tab only and keep the remainder, i.e. treat everything after the first tab as the refname.
split_once already does this — the bug is that strip_prefix("refs/tags/") then operates on the full remainder, which is actually correct. Worth re-checking: the real failure may be narrower than described, so confirm with a test before choosing.
Recommend the first. A tab in a release tag has no legitimate use and every other tool in the chain (GitHub's ref API, git for-each-ref --format) has the same problem.
Tests
- Create a tag containing a tab, push it, and assert
remote_tag_target_shas either returns it correctly or the validator rejected it upstream — whichever fix is chosen, the two must agree.
- Same for a branch name in
verify_remote_branch.
ensure_safe_refname_fragment(src/git/validate.rs:30) explicitly permits tabs, with a test pinning the behaviour:But every consumer of
git ls-remoteoutput parses it as tab-delimited:parse_ls_remote_tags(src/git/push.rs:32):line.split_once('\t')verify_remote_branch(src/git/push.rs:126):line.split_once('\t')ls-remoteemits<sha>\t<refname>. A refname containing a tab produces a line with two tabs, sosplit_onceyieldsrefname = "foo"and the rest is dropped. The tag is then absent from the returned map, or matched under a truncated name.Concretely, for a tag like
release\tcandidate(or any name atagTemplateproduces with a tab in a scope):remote_tag_target_shasreports the tag as not present on the remote, so the "already at the right sha" fast path is skipped and a floating-tag force-push happens that shouldn't.verify_remote_branchnever matchesexpected_refand fails with "Remote branch not found after push" even though the push succeeded.Neither is dangerous — worst case is a redundant force-push or a false negative on verification — but it is a validator and a parser that disagree, and the validator has a test asserting the disagreement is intentional.
Fix
Pick one:
allows_tabtorejects_tab.split_oncealready does this — the bug is thatstrip_prefix("refs/tags/")then operates on the full remainder, which is actually correct. Worth re-checking: the real failure may be narrower than described, so confirm with a test before choosing.Recommend the first. A tab in a release tag has no legitimate use and every other tool in the chain (GitHub's ref API,
git for-each-ref --format) has the same problem.Tests
remote_tag_target_shaseither returns it correctly or the validator rejected it upstream — whichever fix is chosen, the two must agree.verify_remote_branch.