diff options
| -rw-r--r-- | CHANGELOG.md | 1 | ||||
| -rw-r--r-- | src/bin/git_remote_nostr/list.rs | 56 | ||||
| -rw-r--r-- | src/bin/git_remote_nostr/main.rs | 18 | ||||
| -rw-r--r-- | tests/git_remote_nostr/list.rs | 261 |
4 files changed, 327 insertions, 9 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e4fdbd..4130ce2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md | |||
| @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 | |||
| 14 | 14 | ||
| 15 | ### Fixed | 15 | ### Fixed |
| 16 | 16 | ||
| 17 | - `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 | ||
| 17 | - Tag tracking refs written with wrong path (`refs/remotes/origin/refs/tags/v1.0.0` instead of `refs/remotes/origin/v1.0.0`) after a push via `git-remote-nostr`, causing `ngit sync` to fail with "src refspec does not match any existing object" when syncing tags | 18 | - Tag tracking refs written with wrong path (`refs/remotes/origin/refs/tags/v1.0.0` instead of `refs/remotes/origin/v1.0.0`) after a push via `git-remote-nostr`, causing `ngit sync` to fail with "src refspec does not match any existing object" when syncing tags |
| 18 | - `ngit sync` using wrong refspec source (`refs/remotes/origin/refs/heads/master` instead of `refs/remotes/origin/master`), causing sync to fail with "src refspec does not match any existing object" | 19 | - `ngit sync` using wrong refspec source (`refs/remotes/origin/refs/heads/master` instead of `refs/remotes/origin/master`), causing sync to fail with "src refspec does not match any existing object" |
| 19 | - State event publish failures silently swallowed during push; summary now shows `"Published to X/N relays (failed: relay1 relay2)"` instead of unconditional success message | 20 | - State event publish failures silently swallowed during push; summary now shows `"Published to X/N relays (failed: relay1 relay2)"` instead of unconditional success message |
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)] |
diff --git a/tests/git_remote_nostr/list.rs b/tests/git_remote_nostr/list.rs index 88bd3f7..71e7114 100644 --- a/tests/git_remote_nostr/list.rs +++ b/tests/git_remote_nostr/list.rs | |||
| @@ -151,6 +151,267 @@ mod with_state_announcement { | |||
| 151 | Ok(()) | 151 | Ok(()) |
| 152 | } | 152 | } |
| 153 | } | 153 | } |
| 154 | mod when_state_event_references_oids_not_on_git_server { | ||
| 155 | |||
| 156 | use super::*; | ||
| 157 | |||
| 158 | /// Regression test for the bug where a state event published ahead of | ||
| 159 | /// the corresponding `git push` caused `git clone` / `git fetch` to | ||
| 160 | /// fail with missing-object errors. | ||
| 161 | /// | ||
| 162 | /// The fix walks per-relay state events newest-first and picks the | ||
| 163 | /// first one whose every OID is either present on a git server or | ||
| 164 | /// already available locally. When no such event exists it falls back | ||
| 165 | /// to the raw git-server state. | ||
| 166 | #[tokio::test] | ||
| 167 | #[serial] | ||
| 168 | async fn falls_back_to_git_server_state() -> Result<()> { | ||
| 169 | // Build a real git repo that acts as the git server. | ||
| 170 | let source_git_repo = prep_git_repo()?; | ||
| 171 | std::fs::write(source_git_repo.dir.join("initial.md"), "initial")?; | ||
| 172 | let main_commit_id = source_git_repo.stage_and_commit("initial.md")?; | ||
| 173 | |||
| 174 | // Craft a state event that claims main points at a commit that has | ||
| 175 | // NOT been pushed to the git server yet (a plausible OID that does | ||
| 176 | // not exist anywhere). | ||
| 177 | let fake_oid = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; | ||
| 178 | let root_commit = "9ee507fc4357d7ee16a5d8901bedcd103f23c17d"; | ||
| 179 | let state_event = nostr::event::EventBuilder::new(STATE_KIND, "") | ||
| 180 | .tags([ | ||
| 181 | nostr::Tag::identifier(format!("{root_commit}-consider-it-random")), | ||
| 182 | nostr::Tag::custom( | ||
| 183 | nostr::TagKind::Custom(std::borrow::Cow::Borrowed("HEAD")), | ||
| 184 | vec!["ref: refs/heads/main".to_string()], | ||
| 185 | ), | ||
| 186 | nostr::Tag::custom( | ||
| 187 | nostr::TagKind::Custom(std::borrow::Cow::Borrowed("refs/heads/main")), | ||
| 188 | vec![fake_oid.to_string()], | ||
| 189 | ), | ||
| 190 | ]) | ||
| 191 | .sign_with_keys(&TEST_KEY_1_KEYS) | ||
| 192 | .unwrap(); | ||
| 193 | |||
| 194 | let git_repo = prep_git_repo()?; | ||
| 195 | let events = vec![ | ||
| 196 | generate_test_key_1_metadata_event("fred"), | ||
| 197 | generate_test_key_1_relay_list_event(), | ||
| 198 | generate_repo_ref_event_with_git_server(vec![ | ||
| 199 | source_git_repo.dir.to_str().unwrap().to_string(), | ||
| 200 | ]), | ||
| 201 | state_event, | ||
| 202 | ]; | ||
| 203 | // fallback (51,52) user write (53, 55) repo (55, 56) blaster (57) | ||
| 204 | let (mut r51, mut r52, mut r53, mut r55, mut r56, mut r57) = ( | ||
| 205 | Relay::new(8051, None, None), | ||
| 206 | Relay::new(8052, None, None), | ||
| 207 | Relay::new(8053, None, None), | ||
| 208 | Relay::new(8055, None, None), | ||
| 209 | Relay::new(8056, None, None), | ||
| 210 | Relay::new(8057, None, None), | ||
| 211 | ); | ||
| 212 | r51.events = events.clone(); | ||
| 213 | r55.events = events; | ||
| 214 | |||
| 215 | let cli_tester_handle = std::thread::spawn(move || -> Result<()> { | ||
| 216 | let mut p = cli_tester_after_fetch(&git_repo)?; | ||
| 217 | p.send_line("list")?; | ||
| 218 | p.expect("git servers: listing refs...\r\n")?; | ||
| 219 | let res = p.expect_eventually("\r\n\r\n")?; | ||
| 220 | p.exit()?; | ||
| 221 | for p in [51, 52, 53, 55, 56, 57] { | ||
| 222 | relay::shutdown_relay(8000 + p)?; | ||
| 223 | } | ||
| 224 | let lines: HashSet<String> = res | ||
| 225 | .split("\r\n") | ||
| 226 | .map(|e| e.to_string()) | ||
| 227 | .filter(|s| { | ||
| 228 | !s.contains("remote: ") | ||
| 229 | && !s.contains("Receiving objects") | ||
| 230 | && !s.contains("Resolving deltas") | ||
| 231 | && !s.contains("fetching /") | ||
| 232 | }) | ||
| 233 | .collect(); | ||
| 234 | // The fake OID must NOT appear – the list must fall back to | ||
| 235 | // what the git server actually has. | ||
| 236 | assert!( | ||
| 237 | !lines.iter().any(|l| l.contains(fake_oid)), | ||
| 238 | "fake OID from unresolvable state event must not be advertised; got: {lines:?}" | ||
| 239 | ); | ||
| 240 | // The real commit that IS on the git server must be advertised. | ||
| 241 | assert!( | ||
| 242 | lines.contains(&format!("{main_commit_id} refs/heads/main")), | ||
| 243 | "real git-server commit must be advertised; got: {lines:?}" | ||
| 244 | ); | ||
| 245 | Ok(()) | ||
| 246 | }); | ||
| 247 | // launch relays | ||
| 248 | let _ = join!( | ||
| 249 | r51.listen_until_close(), | ||
| 250 | r52.listen_until_close(), | ||
| 251 | r53.listen_until_close(), | ||
| 252 | r55.listen_until_close(), | ||
| 253 | r56.listen_until_close(), | ||
| 254 | r57.listen_until_close(), | ||
| 255 | ); | ||
| 256 | cli_tester_handle.join().unwrap()?; | ||
| 257 | Ok(()) | ||
| 258 | } | ||
| 259 | } | ||
| 260 | |||
| 261 | mod when_newer_relay_state_has_missing_oid_but_older_relay_state_is_resolvable { | ||
| 262 | |||
| 263 | use super::*; | ||
| 264 | |||
| 265 | /// Two relays serve different state events; two git servers each have | ||
| 266 | /// different OIDs. | ||
| 267 | /// | ||
| 268 | /// - Relay 55 (repo relay A): **newer** state event → main = fake_oid | ||
| 269 | /// (not on any git server) | ||
| 270 | /// - Relay 56 (repo relay B): **older** state event → main = commit_a | ||
| 271 | /// (present on git_server_1) | ||
| 272 | /// - git_server_1: main = commit_a | ||
| 273 | /// - git_server_2: main = commit_b (a different real commit) | ||
| 274 | /// | ||
| 275 | /// Expected: `list` skips the newer unresolvable event and advertises | ||
| 276 | /// `commit_a` from the older-but-resolvable state event. | ||
| 277 | #[tokio::test] | ||
| 278 | #[serial] | ||
| 279 | async fn uses_older_resolvable_state_event() -> Result<()> { | ||
| 280 | // --- git_server_1: has commit_a on main --- | ||
| 281 | let git_server_1 = prep_git_repo()?; | ||
| 282 | std::fs::write(git_server_1.dir.join("server1.md"), "server1")?; | ||
| 283 | let commit_a = git_server_1.stage_and_commit("server1.md")?; | ||
| 284 | let bare_server_1 = GitTestRepo::recreate_as_bare(&git_server_1)?; | ||
| 285 | |||
| 286 | // --- git_server_2: has commit_b on main (different commit) --- | ||
| 287 | let git_server_2 = prep_git_repo()?; | ||
| 288 | std::fs::write(git_server_2.dir.join("server2.md"), "server2")?; | ||
| 289 | let commit_b = git_server_2.stage_and_commit("server2.md")?; | ||
| 290 | let bare_server_2 = GitTestRepo::recreate_as_bare(&git_server_2)?; | ||
| 291 | |||
| 292 | assert_ne!(commit_a, commit_b); | ||
| 293 | |||
| 294 | let fake_oid = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; | ||
| 295 | let root_commit = "9ee507fc4357d7ee16a5d8901bedcd103f23c17d"; | ||
| 296 | let identifier = format!("{root_commit}-consider-it-random"); | ||
| 297 | |||
| 298 | // Older state event: main = commit_a (resolvable via git_server_1) | ||
| 299 | let older_state_event = make_event_old_or_change_user( | ||
| 300 | nostr::event::EventBuilder::new(STATE_KIND, "") | ||
| 301 | .tags([ | ||
| 302 | nostr::Tag::identifier(identifier.clone()), | ||
| 303 | nostr::Tag::custom( | ||
| 304 | nostr::TagKind::Custom(std::borrow::Cow::Borrowed("HEAD")), | ||
| 305 | vec!["ref: refs/heads/main".to_string()], | ||
| 306 | ), | ||
| 307 | nostr::Tag::custom( | ||
| 308 | nostr::TagKind::Custom(std::borrow::Cow::Borrowed("refs/heads/main")), | ||
| 309 | vec![commit_a.to_string()], | ||
| 310 | ), | ||
| 311 | ]) | ||
| 312 | .sign_with_keys(&TEST_KEY_1_KEYS) | ||
| 313 | .unwrap(), | ||
| 314 | &TEST_KEY_1_KEYS, | ||
| 315 | 60, // 60 seconds old | ||
| 316 | ); | ||
| 317 | |||
| 318 | // Newer state event: main = fake_oid (NOT on any git server) | ||
| 319 | let newer_state_event = nostr::event::EventBuilder::new(STATE_KIND, "") | ||
| 320 | .tags([ | ||
| 321 | nostr::Tag::identifier(identifier.clone()), | ||
| 322 | nostr::Tag::custom( | ||
| 323 | nostr::TagKind::Custom(std::borrow::Cow::Borrowed("HEAD")), | ||
| 324 | vec!["ref: refs/heads/main".to_string()], | ||
| 325 | ), | ||
| 326 | nostr::Tag::custom( | ||
| 327 | nostr::TagKind::Custom(std::borrow::Cow::Borrowed("refs/heads/main")), | ||
| 328 | vec![fake_oid.to_string()], | ||
| 329 | ), | ||
| 330 | ]) | ||
| 331 | .sign_with_keys(&TEST_KEY_1_KEYS) | ||
| 332 | .unwrap(); | ||
| 333 | |||
| 334 | let git_repo = prep_git_repo()?; | ||
| 335 | |||
| 336 | // Base events (metadata + relay list + repo ref) go on both relays. | ||
| 337 | let repo_ref_event = generate_repo_ref_event_with_git_server(vec![ | ||
| 338 | bare_server_1.dir.to_str().unwrap().to_string(), | ||
| 339 | bare_server_2.dir.to_str().unwrap().to_string(), | ||
| 340 | ]); | ||
| 341 | let base_events = vec![ | ||
| 342 | generate_test_key_1_metadata_event("fred"), | ||
| 343 | generate_test_key_1_relay_list_event(), | ||
| 344 | repo_ref_event, | ||
| 345 | ]; | ||
| 346 | |||
| 347 | // fallback (51,52) user write (53, 55) repo (55, 56) blaster (57) | ||
| 348 | let (mut r51, mut r52, mut r53, mut r55, mut r56, mut r57) = ( | ||
| 349 | Relay::new(8051, None, None), | ||
| 350 | Relay::new(8052, None, None), | ||
| 351 | Relay::new(8053, None, None), | ||
| 352 | Relay::new(8055, None, None), | ||
| 353 | Relay::new(8056, None, None), | ||
| 354 | Relay::new(8057, None, None), | ||
| 355 | ); | ||
| 356 | r51.events = base_events.clone(); | ||
| 357 | // r55 (repo relay A) serves the newer state event with the fake OID | ||
| 358 | r55.events = [base_events.clone(), vec![newer_state_event]].concat(); | ||
| 359 | // r56 (repo relay B) serves the older state event with commit_a | ||
| 360 | r56.events = [base_events, vec![older_state_event]].concat(); | ||
| 361 | |||
| 362 | let cli_tester_handle = std::thread::spawn(move || -> Result<()> { | ||
| 363 | let mut p = cli_tester_after_fetch(&git_repo)?; | ||
| 364 | p.send_line("list")?; | ||
| 365 | p.expect("git servers: listing refs...\r\n")?; | ||
| 366 | let res = p.expect_eventually("\r\n\r\n")?; | ||
| 367 | p.exit()?; | ||
| 368 | for p in [51, 52, 53, 55, 56, 57] { | ||
| 369 | relay::shutdown_relay(8000 + p)?; | ||
| 370 | } | ||
| 371 | let lines: HashSet<String> = res | ||
| 372 | .split("\r\n") | ||
| 373 | .map(|e| e.to_string()) | ||
| 374 | .filter(|s| { | ||
| 375 | !s.contains("remote: ") | ||
| 376 | && !s.contains("Receiving objects") | ||
| 377 | && !s.contains("Resolving deltas") | ||
| 378 | && !s.contains("fetching /") | ||
| 379 | }) | ||
| 380 | .collect(); | ||
| 381 | // The fake OID from the newer-but-unresolvable state event must | ||
| 382 | // NOT appear. | ||
| 383 | assert!( | ||
| 384 | !lines.iter().any(|l| l.contains(fake_oid)), | ||
| 385 | "fake OID from newer unresolvable state event must not be advertised; got: {lines:?}" | ||
| 386 | ); | ||
| 387 | // commit_a from the older-but-resolvable state event must be | ||
| 388 | // advertised for main. | ||
| 389 | assert!( | ||
| 390 | lines.contains(&format!("{commit_a} refs/heads/main")), | ||
| 391 | "commit_a from older resolvable state event must be advertised; got: {lines:?}" | ||
| 392 | ); | ||
| 393 | // commit_b (only on git_server_2, not referenced by any state | ||
| 394 | // event) must NOT appear for main. | ||
| 395 | assert!( | ||
| 396 | !lines.contains(&format!("{commit_b} refs/heads/main")), | ||
| 397 | "commit_b from git_server_2 must not override the chosen state event; got: {lines:?}" | ||
| 398 | ); | ||
| 399 | Ok(()) | ||
| 400 | }); | ||
| 401 | // launch relays | ||
| 402 | let _ = join!( | ||
| 403 | r51.listen_until_close(), | ||
| 404 | r52.listen_until_close(), | ||
| 405 | r53.listen_until_close(), | ||
| 406 | r55.listen_until_close(), | ||
| 407 | r56.listen_until_close(), | ||
| 408 | r57.listen_until_close(), | ||
| 409 | ); | ||
| 410 | cli_tester_handle.join().unwrap()?; | ||
| 411 | Ok(()) | ||
| 412 | } | ||
| 413 | } | ||
| 414 | |||
| 154 | mod when_announcement_doesnt_match_git_server { | 415 | mod when_announcement_doesnt_match_git_server { |
| 155 | 416 | ||
| 156 | use super::*; | 417 | use super::*; |