use std::cell::RefCell; use gix_negotiate::Algorithm; use gix_object::{bstr, bstr::ByteSlice}; use gix_odb::{Find, FindExt}; use gix_ref::{file::ReferenceExt, store::WriteReflog}; #[test] fn run() -> crate::Result { let root = gix_testtools::scripted_fixture_read_only("make_repos.sh")?; for case in [ "no_parents", "clock_skew", "two_colliding_skips", "multi_round", "advertisement_as_filter", ] { let base = root.join(case); for (algo_name, algo) in [ ("noop", Algorithm::Noop), ("consecutive", Algorithm::Consecutive), ("skipping", Algorithm::Skipping), ] { let obj_buf = RefCell::new(Vec::new()); let buf = std::fs::read(base.join(format!("baseline.{algo_name}")))?; let store = gix_odb::at(base.join("client").join(".git/objects"))?; let refs = gix_ref::file::Store::at( base.join("client").join(".git"), WriteReflog::Disable, gix_hash::Kind::Sha1, ); let lookup_names = |names: &[&str]| -> Vec { names .iter() .filter_map(|name| { refs.try_find(*name).expect("one tag per commit").map(|mut r| { r.peel_to_id_in_place(&refs, &mut |id, buf| { store.try_find(&id, buf).map(|d| d.map(|d| (d.kind, d.data))) }) .expect("works"); r.target.into_id() }) }) .collect() }; let message = |id: gix_hash::ObjectId| { store .find_commit(&id, obj_buf.borrow_mut().as_mut()) .expect("present") .message .trim() .as_bstr() .to_owned() }; let debug = false; for use_cache in [false, true] { let cache = use_cache .then(|| gix_commitgraph::at(store.store_ref().path().join("info")).ok()) .flatten(); let mut graph = gix_revwalk::Graph::new( |id, buf| { store .try_find(id, buf) .map(|r| r.and_then(gix_object::Data::try_into_commit_iter)) }, cache, ); let mut negotiator = algo.into_negotiator(); if debug { eprintln!("ALGO {algo_name} CASE {case}"); } // // In --negotiate-only mode, which seems to be the only thing that's working after trying --dry-run, we unfortunately // // don't get to see what happens if known-common commits are added as git itself doesn't do that in this mode // // for some reason. // for common in lookup_names(&["origin/main"]) { // eprintln!("COMMON {name} {common}", name = message(common)); // negotiator.known_common(common)?; // } for tip in lookup_names(&["HEAD"]).into_iter().chain( refs.iter()? .prefixed("refs/heads".as_ref())? .filter_map(Result::ok) .map(|r| r.target.into_id()), ) { if debug { eprintln!("TIP {name} {tip}", name = message(tip)); } negotiator.add_tip(tip, &mut graph)?; } for (round, Round { mut haves, common }) in ParseRounds::new(buf.lines()).enumerate() { if algo == Algorithm::Skipping { if case == "clock_skew" { // Here for some reason the prio-queue of git manages to not sort the parent of C2, which is in the future, to be // ahead of old4 that is in the past. In the git version of this test, they say to expect exactly this sequence // as well even though it's not actually happening (but that they can't see due to the way they are testing). haves = lookup_names(&["c2", "c1", "old4", "old2", "old1"]); } else if case == "two_colliding_skips" { // The same thing, we actually get exactly the right order, whereas git for some reason doesn't. // This is the order expected in the git tests. haves = lookup_names(&["c5side", "c11", "c9", "c6", "c1"]); } else if case == "multi_round" && round == 1 { // Here, probably also because of priority queue quirks, `git` emits the commits out of order, with only one // branch, b5 I think, being out of place. This list puts the expectation in the right order, which is ordered // by commit date. haves = lookup_names(&[ "b8.c14", "b7.c14", "b6.c14", "b5.c14", "b4.c14", "b3.c14", "b2.c14", "b8.c9", "b7.c9", "b6.c9", "b5.c9", "b4.c9", "b3.c9", "b2.c9", "b8.c1", "b7.c1", "b6.c1", "b5.c1", "b4.c1", "b3.c1", "b2.c1", "b8.c0", "b7.c0", "b6.c0", "b5.c0", "b4.c0", "b3.c0", "b2.c0", ]); } else if case == "advertisement_as_filter" { haves = lookup_names(&["c2side", "c5", "origin/main"]) .into_iter() .chain(Some( gix_hash::ObjectId::from_hex(b"f36cefa0be2ac180d360a54b1cc4214985cea60a").unwrap(), )) .collect(); } } for have in haves { let actual = negotiator.next_have(&mut graph).unwrap_or_else(|| { panic!("{algo_name}:cache={use_cache}: one have per baseline: {have} missing or in wrong order", have = message(have)) })?; assert_eq!( actual, have, "{algo_name}:cache={use_cache}: order and commit matches exactly, wanted {expected}, got {actual}, commits left: {:?}", std::iter::from_fn(|| negotiator.next_have(&mut graph)).map(|id| message(id.unwrap())).collect::>(), actual = message(actual), expected = message(have) ); if debug { eprintln!("have {}", message(actual)); } } for common_revision in common { if debug { eprintln!("ACK {}", message(common_revision)); } negotiator.in_common_with_remote(common_revision, &mut graph)?; } } assert!( negotiator.next_have(&mut graph).is_none(), "{algo_name}:cache={use_cache}: negotiator should be depleted after all recorded baseline rounds" ); } } } Ok(()) } struct ParseRounds<'a> { lines: bstr::Lines<'a>, } impl<'a> ParseRounds<'a> { pub fn new(mut lines: bstr::Lines<'a>) -> Self { parse::command(&mut lines, parse::Command::Incoming).expect("handshake"); Self { lines } } } impl<'a> Iterator for ParseRounds<'a> { type Item = Round; fn next(&mut self) -> Option { let haves = parse::object_ids("have", parse::command(&mut self.lines, parse::Command::Outgoing)?); let common = parse::object_ids("ACK", parse::command(&mut self.lines, parse::Command::Incoming)?); if haves.is_empty() { assert!(common.is_empty(), "cannot ack what's not there"); return None; } Round { haves, common }.into() } } struct Round { pub haves: Vec, pub common: Vec, } mod parse { use gix_object::{ bstr, bstr::{BStr, ByteSlice}, }; #[derive(Debug, Eq, PartialEq, Copy, Clone)] pub enum Command { Incoming, Outgoing, } pub fn object_ids(prefix: &str, lines: impl IntoIterator>) -> Vec { lines .into_iter() .filter_map(|line| { line.as_ref() .strip_prefix(prefix.as_bytes()) .map(|id| gix_hash::ObjectId::from_hex(id.trim()).expect("valid hash")) }) .collect() } pub fn command<'a>(lines: &mut bstr::Lines<'a>, wanted: Command) -> Option> { let mut out = Vec::new(); for line in lines { let pos = line.find(b"fetch").expect("fetch token"); let line_mode = match &line[pos + 5..][..2] { b"< " => Command::Incoming, b"> " => Command::Outgoing, invalid => unreachable!("invalid fetch token: {:?}", invalid.as_bstr()), }; assert_eq!(line_mode, wanted, "command with unexpected mode"); let line = line[pos + 7..].as_bstr(); if line == "0000" { break; } out.push(line); } (!out.is_empty()).then_some(out) } }