upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src/bin
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2026-02-26 14:00:12 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2026-02-26 15:26:20 +0000
commitd8b85cbce5cba9bfb8b15a8bd5c1b7200ff3e488 (patch)
tree608d535034e73fe61c5edbf1bbc3c51621f70faa /src/bin
parentb85683201250e97a30bfe7a5dbba5508f8e86f65 (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.rs56
-rw-r--r--src/bin/git_remote_nostr/main.rs18
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};
4use client::get_state_from_cache; 4use client::get_state_from_cache;
5use git::RepoActions; 5use git::RepoActions;
6use ngit::{ 6use 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};
16use repo_ref::RepoRef; 17use 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
14use anyhow::{Context, Result, bail}; 14use anyhow::{Context, Result, bail};
15use client::{Connect, consolidate_fetch_reports, get_repo_ref_from_cache, is_verbose}; 15use client::{
16 Connect, FetchReport, consolidate_fetch_reports, get_repo_ref_from_cache, is_verbose,
17};
16use git::{RepoActions, nostr_url::NostrUrlDecoded}; 18use git::{RepoActions, nostr_url::NostrUrlDecoded};
17use ngit::{ 19use 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)]