diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2026-02-26 14:00:12 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2026-02-26 15:26:20 +0000 |
| commit | d8b85cbce5cba9bfb8b15a8bd5c1b7200ff3e488 (patch) | |
| tree | 608d535034e73fe61c5edbf1bbc3c51621f70faa /src/bin | |
| parent | b85683201250e97a30bfe7a5dbba5508f8e86f65 (diff) | |
fix: advertise only state events with resolvable git objects
git-remote-nostr now walks the per-relay state events captured in
FetchReport::state_per_relay (newest first) and advertises the first
one whose every OID is either present on at least one git server
(confirmed via list_refs) or already available locally. If no such
state event exists it falls back to the raw git server state.
Previously the latest nostr state event was always used regardless of
whether its OIDs had been pushed to any server, causing catastrophic
missing-object errors during clone or fetch when a state event was
published ahead of the corresponding git push.
Diffstat (limited to 'src/bin')
| -rw-r--r-- | src/bin/git_remote_nostr/list.rs | 56 | ||||
| -rw-r--r-- | src/bin/git_remote_nostr/main.rs | 18 |
2 files changed, 65 insertions, 9 deletions
diff --git a/src/bin/git_remote_nostr/list.rs b/src/bin/git_remote_nostr/list.rs index 4a7c1ec..a32ed67 100644 --- a/src/bin/git_remote_nostr/list.rs +++ b/src/bin/git_remote_nostr/list.rs | |||
| @@ -4,13 +4,14 @@ use anyhow::{Context, Result}; | |||
| 4 | use client::get_state_from_cache; | 4 | use client::get_state_from_cache; |
| 5 | use git::RepoActions; | 5 | use git::RepoActions; |
| 6 | use ngit::{ | 6 | use ngit::{ |
| 7 | client::{self, is_verbose}, | 7 | client::{self, FetchReport, is_verbose}, |
| 8 | fetch::fetch_from_git_server, | 8 | fetch::fetch_from_git_server, |
| 9 | git::{self}, | 9 | git::{self}, |
| 10 | git_events::{KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE, event_to_cover_letter, tag_value}, | 10 | git_events::{KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE, event_to_cover_letter, tag_value}, |
| 11 | list::list_from_remotes, | 11 | list::list_from_remotes, |
| 12 | login::get_curent_user, | 12 | login::get_curent_user, |
| 13 | repo_ref::{self}, | 13 | repo_ref::{self}, |
| 14 | repo_state::RepoState, | ||
| 14 | utils::{get_all_proposals, get_open_or_draft_proposals}, | 15 | utils::{get_all_proposals, get_open_or_draft_proposals}, |
| 15 | }; | 16 | }; |
| 16 | use repo_ref::RepoRef; | 17 | use repo_ref::RepoRef; |
| @@ -22,6 +23,7 @@ pub async fn run_list( | |||
| 22 | git_repo: &Repo, | 23 | git_repo: &Repo, |
| 23 | repo_ref: &RepoRef, | 24 | repo_ref: &RepoRef, |
| 24 | for_push: bool, | 25 | for_push: bool, |
| 26 | fetch_report: &FetchReport, | ||
| 25 | ) -> Result<HashMap<String, (HashMap<String, String>, bool)>> { | 27 | ) -> Result<HashMap<String, (HashMap<String, String>, bool)>> { |
| 26 | let nostr_state = (get_state_from_cache(Some(git_repo.get_path()?), repo_ref).await).ok(); | 28 | let nostr_state = (get_state_from_cache(Some(git_repo.get_path()?), repo_ref).await).ok(); |
| 27 | 29 | ||
| @@ -30,6 +32,8 @@ pub async fn run_list( | |||
| 30 | if is_verbose() { | 32 | if is_verbose() { |
| 31 | term.write_line("git servers: listing refs...")?; | 33 | term.write_line("git servers: listing refs...")?; |
| 32 | } | 34 | } |
| 35 | // nostr_state is passed to list_from_remotes only for the sync-status | ||
| 36 | // display; the actual ref state we advertise is determined below. | ||
| 33 | let remote_states = list_from_remotes( | 37 | let remote_states = list_from_remotes( |
| 34 | &term, | 38 | &term, |
| 35 | git_repo, | 39 | git_repo, |
| @@ -39,9 +43,55 @@ pub async fn run_list( | |||
| 39 | ) | 43 | ) |
| 40 | .await; | 44 | .await; |
| 41 | 45 | ||
| 42 | let mut state = if let Some(nostr_state) = nostr_state { | 46 | // Collect all OIDs confirmed present on at least one git server. |
| 43 | nostr_state.state | 47 | let git_server_oids: std::collections::HashSet<String> = remote_states |
| 48 | .values() | ||
| 49 | .flat_map(|(state, _)| state.values()) | ||
| 50 | .filter(|v| !v.starts_with("ref: ")) | ||
| 51 | .cloned() | ||
| 52 | .collect(); | ||
| 53 | |||
| 54 | // From the per-relay state events captured during the nostr fetch, find | ||
| 55 | // the newest state event whose every OID is either: | ||
| 56 | // (a) confirmed present on at least one git server, or | ||
| 57 | // (b) already available locally. | ||
| 58 | // This prevents advertising refs whose git objects haven't been pushed to | ||
| 59 | // any server yet, which would cause `git clone` / `git fetch` to fail. | ||
| 60 | let mut candidates: Vec<&nostr::Event> = fetch_report | ||
| 61 | .state_per_relay | ||
| 62 | .values() | ||
| 63 | .filter_map(|maybe| maybe.as_ref()) | ||
| 64 | .collect(); | ||
| 65 | // Sort newest-first (by created_at, then by id for tie-breaking). | ||
| 66 | candidates.sort_by(|a, b| { | ||
| 67 | b.created_at | ||
| 68 | .cmp(&a.created_at) | ||
| 69 | .then_with(|| b.id.cmp(&a.id)) | ||
| 70 | }); | ||
| 71 | // Deduplicate by event id so we don't check the same event twice. | ||
| 72 | candidates.dedup_by_key(|e| e.id); | ||
| 73 | |||
| 74 | let best_state: Option<HashMap<String, String>> = candidates.into_iter().find_map(|event| { | ||
| 75 | if let Ok(rs) = RepoState::try_from(vec![event.clone()]) { | ||
| 76 | let all_resolvable = rs.state.values().all(|v| { | ||
| 77 | v.starts_with("ref: ") | ||
| 78 | || git_server_oids.contains(v) | ||
| 79 | || git_repo.does_commit_exist(v).is_ok_and(|exists| exists) | ||
| 80 | }); | ||
| 81 | if all_resolvable { Some(rs.state) } else { None } | ||
| 82 | } else { | ||
| 83 | None | ||
| 84 | } | ||
| 85 | }); | ||
| 86 | |||
| 87 | let mut state = if let Some(state) = best_state { | ||
| 88 | state | ||
| 44 | } else { | 89 | } else { |
| 90 | // No relay returned a state event whose OIDs are all resolvable | ||
| 91 | // (either no state events were seen on any relay, or every candidate | ||
| 92 | // references git objects not yet on any server). Fall back to | ||
| 93 | // whatever the git servers actually report so we never advertise OIDs | ||
| 94 | // that cannot be fetched. | ||
| 45 | let (state, _is_grasp_server) = repo_ref | 95 | let (state, _is_grasp_server) = repo_ref |
| 46 | .git_server | 96 | .git_server |
| 47 | .iter() | 97 | .iter() |
diff --git a/src/bin/git_remote_nostr/main.rs b/src/bin/git_remote_nostr/main.rs index 6186ed3..dad8a99 100644 --- a/src/bin/git_remote_nostr/main.rs +++ b/src/bin/git_remote_nostr/main.rs | |||
| @@ -12,7 +12,9 @@ use std::{ | |||
| 12 | }; | 12 | }; |
| 13 | 13 | ||
| 14 | use anyhow::{Context, Result, bail}; | 14 | use anyhow::{Context, Result, bail}; |
| 15 | use client::{Connect, consolidate_fetch_reports, get_repo_ref_from_cache, is_verbose}; | 15 | use client::{ |
| 16 | Connect, FetchReport, consolidate_fetch_reports, get_repo_ref_from_cache, is_verbose, | ||
| 17 | }; | ||
| 16 | use git::{RepoActions, nostr_url::NostrUrlDecoded}; | 18 | use git::{RepoActions, nostr_url::NostrUrlDecoded}; |
| 17 | use ngit::{ | 19 | use ngit::{ |
| 18 | client::{self, Params}, | 20 | client::{self, Params}, |
| @@ -149,7 +151,9 @@ async fn main() -> Result<()> { | |||
| 149 | client.set_signer(signer).await; | 151 | client.set_signer(signer).await; |
| 150 | } | 152 | } |
| 151 | 153 | ||
| 152 | fetching_with_report_for_helper(git_repo_path, &client, &decoded_nostr_url.coordinate).await?; | 154 | let fetch_report = |
| 155 | fetching_with_report_for_helper(git_repo_path, &client, &decoded_nostr_url.coordinate) | ||
| 156 | .await?; | ||
| 153 | 157 | ||
| 154 | let mut repo_ref = | 158 | let mut repo_ref = |
| 155 | get_repo_ref_from_cache(Some(git_repo_path), &decoded_nostr_url.coordinate).await?; | 159 | get_repo_ref_from_cache(Some(git_repo_path), &decoded_nostr_url.coordinate).await?; |
| @@ -221,10 +225,12 @@ async fn main() -> Result<()> { | |||
| 221 | push_options = PushOptions::default(); | 225 | push_options = PushOptions::default(); |
| 222 | } | 226 | } |
| 223 | ["list"] => { | 227 | ["list"] => { |
| 224 | list_outputs = Some(list::run_list(&git_repo, &repo_ref, false).await?); | 228 | list_outputs = |
| 229 | Some(list::run_list(&git_repo, &repo_ref, false, &fetch_report).await?); | ||
| 225 | } | 230 | } |
| 226 | ["list", "for-push"] => { | 231 | ["list", "for-push"] => { |
| 227 | list_outputs = Some(list::run_list(&git_repo, &repo_ref, true).await?); | 232 | list_outputs = |
| 233 | Some(list::run_list(&git_repo, &repo_ref, true, &fetch_report).await?); | ||
| 228 | } | 234 | } |
| 229 | [] => { | 235 | [] => { |
| 230 | return Ok(()); | 236 | return Ok(()); |
| @@ -283,7 +289,7 @@ async fn fetching_with_report_for_helper( | |||
| 283 | git_repo_path: &Path, | 289 | git_repo_path: &Path, |
| 284 | client: &Client, | 290 | client: &Client, |
| 285 | trusted_maintainer_coordinate: &Nip19Coordinate, | 291 | trusted_maintainer_coordinate: &Nip19Coordinate, |
| 286 | ) -> Result<()> { | 292 | ) -> Result<FetchReport> { |
| 287 | let term = console::Term::stderr(); | 293 | let term = console::Term::stderr(); |
| 288 | let verbose = is_verbose(); | 294 | let verbose = is_verbose(); |
| 289 | if verbose { | 295 | if verbose { |
| @@ -308,7 +314,7 @@ async fn fetching_with_report_for_helper( | |||
| 308 | } else { | 314 | } else { |
| 309 | term.write_line(&format!("nostr updates: {report}"))?; | 315 | term.write_line(&format!("nostr updates: {report}"))?; |
| 310 | } | 316 | } |
| 311 | Ok(()) | 317 | Ok(report) |
| 312 | } | 318 | } |
| 313 | 319 | ||
| 314 | #[cfg(test)] | 320 | #[cfg(test)] |