upleb.uk

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

summaryrefslogtreecommitdiff
path: root/tests/git_remote_nostr
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 /tests/git_remote_nostr
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 'tests/git_remote_nostr')
-rw-r--r--tests/git_remote_nostr/list.rs261
1 files changed, 261 insertions, 0 deletions
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::*;