From 28ad5440c7184de9833f8448bc90153ee4499c83 Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Fri, 27 Feb 2026 15:40:24 +0000 Subject: fix: annotated tags missing from list due to dropped peeled refs RepoState::try_from was explicitly discarding all refs/tags/*^{} entries ("peeled" refs) when parsing the nostr state event. This meant the list command only advertised the tag object OID, but git requires two lines for annotated tags: refs/tags/v1.0.0 refs/tags/v1.0.0^{} Without the ^{} peeled line git cannot resolve the tag to a commit, so git fetch --prune treats it as unresolvable and deletes it. The nostr state event already stores both entries correctly (written by generate_updated_state in push.rs). The fix simply stops try_from from discarding the ^{} entries on read, so they flow through to the list output unchanged. --- CHANGELOG.md | 1 + src/bin/git_remote_nostr/push.rs | 31 +++++++++++++++++++++++++++++++ src/lib/repo_state.rs | 7 +++++-- 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cbf5312..f52d353 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Annotated tags missing from `git-remote-nostr` list output; peeled `^{}` refs were stripped when parsing the nostr state event, so git could not resolve the tag to a commit and `git fetch --prune` deleted it; existing repos with affected state events are self-healed on the next push - Fallback signer relays updated: replaced `nsec.app` with `bucket.coracle.social` and `nos.lol` for nostrconnect resilience - `merge-base` tag in PR events generated by `git push` of a `pr/` branch was set to the parent of the PR tip instead of the actual base commit; multi-commit PRs showed only 1 commit when applied via `ngit apply` - `git-remote-nostr` list now advertises the newest state event whose OIDs are all confirmed present on a git server or locally, rather than unconditionally using the latest nostr state event; this prevents catastrophic fetch/clone failures when a state event was published before the corresponding git push completed diff --git a/src/bin/git_remote_nostr/push.rs b/src/bin/git_remote_nostr/push.rs index 870f22d..ed0f7df 100644 --- a/src/bin/git_remote_nostr/push.rs +++ b/src/bin/git_remote_nostr/push.rs @@ -925,6 +925,37 @@ fn generate_updated_state( ) -> Result> { let mut new_state = existing_state.clone(); + // Backfill missing ^{} peeled refs for any annotated tags already in the + // state. State events published before this fix only stored the tag object + // OID; without the corresponding ^{} entry git cannot resolve the tag to a + // commit and treats it as missing (git fetch --prune deletes it). We fix + // this opportunistically on every push so affected repos self-heal without + // requiring manual intervention. + let tag_refs: Vec<(String, String)> = new_state + .iter() + .filter(|(k, _)| k.starts_with("refs/tags/") && !k.ends_with("^{}")) + .map(|(k, v)| (k.clone(), v.clone())) + .collect(); + for (ref_name, tag_oid) in tag_refs { + let peeled_key = format!("{ref_name}^{{}}"); + if new_state.contains_key(&peeled_key) { + continue; + } + // check if the stored OID is a tag object (annotated tag) + if let Ok(oid) = git2::Oid::from_str(&tag_oid) { + if git_repo + .git_repo + .find_object(oid, Some(git2::ObjectType::Tag)) + .is_ok() + { + // peel to the commit the annotated tag points to + if let Ok(commit_oid) = git_repo.get_commit_or_tip_of_reference(&ref_name) { + new_state.insert(peeled_key, commit_oid.to_string()); + } + } + } + } + for refspec in refspecs { let (from, to) = refspec_to_from_to(refspec)?; if from.is_empty() { diff --git a/src/lib/repo_state.rs b/src/lib/repo_state.rs index 345f05c..223fe56 100644 --- a/src/lib/repo_state.rs +++ b/src/lib/repo_state.rs @@ -22,11 +22,14 @@ impl RepoState { let mut state = HashMap::new(); for tag in event.tags.iter() { if let Some(name) = tag.as_slice().first() { + // include ^{} peeled refs for annotated tags: git requires + // both " refs/tags/v1.0.0" and + // " refs/tags/v1.0.0^{}" in the list output so + // it can resolve the tag to a commit. without the ^{} line + // git fetch --prune deletes the tag as unresolvable. if ["refs/heads/", "refs/tags", "HEAD"] .iter() .any(|s| name.starts_with(*s)) - // dont include dereferenced tags - && !name.ends_with("^{}") { if let Some(value) = tag.as_slice().get(1) { if Oid::from_str(value).is_ok() || value.contains("ref: refs/") { -- cgit v1.2.3