fix: ignore peeled tag entries (^{}) in state event ref parsing

State events (kind 30618) can include refs/tags/<name>^{} entries which
are git's notation for the dereferenced commit behind an annotated tag.
These are not real git refs and are never sent as part of a push.

extract_refs_from_state and RepositoryState::from_event were treating
them as real refs, causing can_satisfy_state to reject valid annotated
tag pushes: the would-be state after the push lacked the spurious ^{}
entry, so the exact-equality check always failed.
This commit is contained in:
DanConwayDev
2026-02-26 15:38:51 +00:00
parent b13c6d924f
commit a2ecfc5a63
3 changed files with 12 additions and 2 deletions
+4
View File
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Fixed
- Push authorization now correctly ignores `refs/tags/<name>^{}` peeled-tag entries in state events (kind 30618). These entries are git's internal notation for the dereferenced commit behind an annotated tag and are never sent as part of a push. Previously, their presence in the state event caused `can_satisfy_state` to reject valid annotated-tag pushes because the would-be ref state after the push did not include the spurious `^{}` entry, making the exact-equality check fail.
### Changed
- Push auth rejections now send the reason to the git client via ERR pkt-line (e.g. "authorisation failed: No state events in purgatory") instead of a generic HTTP 403, so users see actionable error messages directly in their terminal
+3 -1
View File
@@ -260,12 +260,14 @@ impl RepositoryState {
// Extract tags (refs/tags/*)
// Tag format: ["refs/tags/v1.0", "commit_hash"]
// Exclude peeled tag notation ("refs/tags/v1.0^{}") — these are git's internal
// dereference markers pointing to the underlying commit, not real refs.
let tags = event
.tags
.iter()
.filter_map(|t| {
if let TagKind::Custom(s) = t.kind() {
if s.as_ref().starts_with("refs/tags/") {
if s.as_ref().starts_with("refs/tags/") && !s.as_ref().ends_with("^{}") {
let parts = t.clone().to_vec();
if parts.len() >= 2 {
Some(TagState {
+5 -1
View File
@@ -58,7 +58,11 @@ pub fn extract_refs_from_state(event: &Event) -> Vec<RefPair> {
let ref_str = ref_name.as_ref();
// Only process refs/heads/* and refs/tags/*
if ref_str.starts_with("refs/heads/") || ref_str.starts_with("refs/tags/") {
// Exclude peeled tag notation (e.g. "refs/tags/v1.0.0^{}") — these are
// git's internal dereference markers, not real refs that get pushed.
if (ref_str.starts_with("refs/heads/") || ref_str.starts_with("refs/tags/"))
&& !ref_str.ends_with("^{}")
{
// Get the object SHA (first value in tag)
let parts = tag.clone().to_vec();
if parts.len() >= 2 {