mod shallow { use gix::remote::fetch::Shallow; #[test] fn undo() { assert_eq!( Shallow::undo(), Shallow::DepthAtRemote(2147483647u32.try_into().expect("known at compile time")) ); }} #[cfg(any(feature = "blocking-network-client", feature = "async-network-client-async-std"))]mod blocking_and_async_io { use std::sync::atomic::AtomicBool; use gix::remote::{fetch, fetch::Status, Direction::Fetch}; use gix_features::progress; use gix_protocol::maybe_async; use gix_testtools::tempfile::TempDir; use crate::{ remote, remote::{into_daemon_remote_if_async, spawn_git_daemon_if_async}, util::hex_to_id, }; pub(crate) fn base_repo_path() -> String { gix::path::realpath( gix_testtools::scripted_fixture_read_only("make_remote_repos.sh") .unwrap() .join("base"), ) .unwrap() .to_string_lossy() .into_owned() } pub(crate) fn repo_path(name: &str) -> std::path::PathBuf { let dir = gix_testtools::scripted_fixture_read_only_with_args("make_fetch_repos.sh", [base_repo_path()]).unwrap(); dir.join(name) } #[allow(clippy::result_large_err)] pub(crate) fn try_repo_rw( name: &str, ) -> Result<(gix::Repository, gix_testtools::tempfile::TempDir), gix::open::Error> { try_repo_rw_args(name, Vec::<String>::new(), Mode::FastClone) } pub(crate) enum Mode { FastClone, CloneWithShallowSupport, } #[allow(clippy::result_large_err)] pub(crate) fn try_repo_rw_args<S: Into<String>>( name: &str, args: impl IntoIterator<Item = S>, mode: Mode, ) -> Result<(gix::Repository, gix_testtools::tempfile::TempDir), gix::open::Error> { let dir = gix_testtools::scripted_fixture_writable_with_args( "make_fetch_repos.sh", [{ let mut url = base_repo_path(); if matches!(mode, Mode::CloneWithShallowSupport) { url.insert_str(0, "file://"); } url }] .into_iter() .chain(args.into_iter().map(Into::into)), gix_testtools::Creation::ExecuteScript, ) .unwrap(); let repo = gix::open_opts(dir.path().join(name), crate::restricted())?; Ok((repo, dir)) } pub(crate) fn repo_rw(name: &str) -> (gix::Repository, gix_testtools::tempfile::TempDir) { try_repo_rw(name).unwrap() } #[test] #[cfg(feature = "blocking-network-client")] fn collate_fetch_error() -> Result<(), gix::env::collate::fetch::Error<std::io::Error>> { let (repo, _tmp) = try_repo_rw("two-origins")?; let remote = repo .head()? .into_remote(Fetch) .expect("present")? .with_fetch_tags(fetch::Tags::Included); remote .connect(Fetch)? .prepare_fetch(gix::progress::Discard, Default::default())? .receive(gix::progress::Discard, &AtomicBool::default())?; assert!( repo.path() .join("HEAD") .metadata() .map_err(gix::env::collate::fetch::Error::Other)? .is_file(), "just to show off the 'Other' error type" ); Ok(()) } #[test] #[cfg(feature = "blocking-network-client")] fn fetch_with_alternates_adds_tips_from_alternates() -> crate::Result<()> { let tmp = gix_testtools::tempfile::TempDir::new()?; let remote_repo = remote::repo("base"); let (_repo, out) = gix::clone::PrepareFetch::new( remote::repo("multi_round/server").path(), tmp.path(), gix::create::Kind::Bare, Default::default(), gix::open::Options::isolated(), )? .configure_remote({ move |r| { std::fs::write( r.repo().objects.store_ref().path().join("info").join("alternates"), format!( "{}\n", gix::path::realpath(remote_repo.objects.store_ref().path())?.display() ) .as_bytes(), )?; Ok(r) } }) .fetch_only(gix::progress::Discard, &AtomicBool::default())?; match out.status { Status::Change { negotiation_rounds, write_pack_bundle, .. } => { assert_eq!( negotiation_rounds, 1, "we don't really have a way to see that tips from alternates were added, I think" ); assert_eq!( write_pack_bundle.index.num_objects, 66, "this test just exercises code for adding alternate-repo tips to the negotiator" ); } _ => unreachable!("we get a pack as alternates are unrelated"), } Ok(()) } #[maybe_async::test( feature = "blocking-network-client", async(feature = "async-network-client-async-std", async_std::test) )] async fn fetch_with_multi_round_negotiation() -> crate::Result { for (algorithm, expected_negotiation_rounds) in [ (gix::negotiate::Algorithm::Consecutive, 4), (gix::negotiate::Algorithm::Skipping, 2), ] { for version in [ gix::protocol::transport::Protocol::V1, gix::protocol::transport::Protocol::V2, ] { let (mut client_repo, _tmp) = { let client_repo = remote::repo("multi_round/client"); let daemon = spawn_git_daemon_if_async(client_repo.work_dir().expect("non-bare"))?; let tmp = TempDir::new()?; let repo = gix::prepare_clone_bare( daemon.as_ref().map_or_else( || client_repo.git_dir().to_owned(), |d| std::path::PathBuf::from(format!("{}/", d.url)), ), tmp.path(), )? .fetch_only(gix::progress::Discard, &std::sync::atomic::AtomicBool::default()) .await? .0; (repo, tmp) }; { let mut config = client_repo.config_snapshot_mut(); config.set_value( &gix::config::tree::Protocol::VERSION, (version as u8).to_string().as_str(), )?; config.set_value( &gix::config::tree::Fetch::NEGOTIATION_ALGORITHM, algorithm.to_string().as_str(), )?; } let server_repo = remote::repo("multi_round/server"); let daemon = spawn_git_daemon_if_async(server_repo.work_dir().expect("non-bare"))?; let remote = into_daemon_remote_if_async( client_repo.remote_at(server_repo.work_dir().expect("non-bare"))?, daemon.as_ref(), None, ); let changes = remote .with_refspecs(Some("refs/heads/*:refs/remotes/origin/*"), Fetch)? .connect(Fetch) .await? .prepare_fetch(gix::progress::Discard, Default::default()) .await? .receive(gix::progress::Discard, &AtomicBool::default()) .await?; match changes.status { Status::Change { write_pack_bundle, negotiation_rounds, .. } => { assert_eq!( negotiation_rounds, expected_negotiation_rounds, "we need multiple rounds" ); // the server only has our `b1` and an extra commit or two. assert_eq!( write_pack_bundle.index.num_objects, 7, "this is the number git gets as well, we are quite perfectly aligned :)" ); } _ => unreachable!("We expect a pack for sure"), } } } Ok(()) } #[maybe_async::test( feature = "blocking-network-client", async(feature = "async-network-client-async-std", async_std::test) )] async fn fetch_shallow_deepen_zero_does_not_fail() -> crate::Result { let (repo, tmp) = try_repo_rw_args("two-origins", ["--depth=2"], Mode::CloneWithShallowSupport)?; let daemon = spawn_git_daemon_if_async(tmp.path().join("base"))?; let remote = into_daemon_remote_if_async( repo.head()? .into_remote(Fetch) .expect("present")? .with_fetch_tags(fetch::Tags::Included), daemon.as_ref(), None, ); let prev_commits = repo.head_id()?.ancestors().all()?.count(); let changes = remote .connect(Fetch) .await? .prepare_fetch(gix::progress::Discard, Default::default()) .await? .with_shallow(fetch::Shallow::Deepen(0)) .receive(gix::progress::Discard, &AtomicBool::default()) .await?; assert!( matches!(changes.status, Status::NoPackReceived { .. }), "we didn't negotiate at all, but ran all other ref updates" ); assert_eq!( repo.head_id()?.ancestors().all()?.count(), prev_commits, "no more commits are available - we didn't fetch anything and there would be nothing to fetch" ); Ok(()) } #[maybe_async::test( feature = "blocking-network-client", async(feature = "async-network-client-async-std", async_std::test) )] async fn fetch_shallow_deepen_not_possible() -> crate::Result { let (repo, tmp) = try_repo_rw_args("two-origins", ["--depth=2"], Mode::CloneWithShallowSupport)?; let daemon = spawn_git_daemon_if_async(tmp.path().join("base"))?; let remote = into_daemon_remote_if_async( repo.head()? .into_remote(Fetch) .expect("present")? .with_fetch_tags(fetch::Tags::Included), daemon.as_ref(), None, ); assert_eq!( repo.shallow_commits()?.expect("shallow clone").as_slice(), [ hex_to_id("2d9d136fb0765f2e24c44a0f91984318d580d03b"), hex_to_id("dfd0954dabef3b64f458321ef15571cc1a46d552"), hex_to_id("dfd0954dabef3b64f458321ef15571cc1a46d552") ] ); let prev_commits = repo.head_id()?.ancestors().all()?.count(); let changes = remote .connect(Fetch) .await? .prepare_fetch(gix::progress::Discard, Default::default()) .await? .with_shallow(fetch::Shallow::Deepen(1)) .receive(gix::progress::Discard, &AtomicBool::default()) .await?; assert!( matches!(changes.status, Status::Change {write_pack_bundle, ..} if write_pack_bundle.index.num_objects == 0), "we get an empty pack as there is nothing to do" ); assert_eq!( repo.shallow_commits()?.expect("shallow clone").as_slice(), [ hex_to_id("2d9d136fb0765f2e24c44a0f91984318d580d03b"), hex_to_id("dfd0954dabef3b64f458321ef15571cc1a46d552"), hex_to_id("dfd0954dabef3b64f458321ef15571cc1a46d552") ], "the base is shallow, and so is the clone, and we can't extend further" ); assert_eq!( repo.head_id()?.ancestors().all()?.count(), prev_commits, "no more commits are available - there simply isn't more information" ); Ok(()) } #[maybe_async::test( feature = "blocking-network-client", async(feature = "async-network-client-async-std", async_std::test) )] async fn fetch_empty_pack() -> crate::Result { let daemon = spawn_git_daemon_if_async(repo_path("base"))?; for (fetch_tags, expected_ref_count) in [ (fetch::Tags::None, 1), (fetch::Tags::Included, 7), (fetch::Tags::All, 7), ] { let (repo, _tmp) = repo_rw("two-origins"); let mut remote = into_daemon_remote_if_async( repo.head()? .into_remote(Fetch) .expect("present")? .with_fetch_tags(fetch_tags), daemon.as_ref(), None, ); remote.replace_refspecs(Some("HEAD:refs/remotes/origin/does-not-yet-exist"), Fetch)?; let res = remote .connect(Fetch) .await? .prepare_fetch(gix::progress::Discard, Default::default()) .await? .receive(gix::progress::Discard, &AtomicBool::default()) .await?; match res.status { fetch::Status::NoPackReceived { update_refs } => { assert_eq!(update_refs.edits.len(), expected_ref_count); } _ => unreachable!( "default negotiation is able to realize nothing is required and doesn't get to receiving a pack" ), } } Ok(()) } #[maybe_async::test( feature = "blocking-network-client", async(feature = "async-network-client-async-std", async_std::test) )] async fn fetch_pack_without_local_destination() -> crate::Result { let daemon = spawn_git_daemon_if_async(repo_path("clone-as-base-with-changes"))?; for (fetch_tags, expected_data_hash, num_objects_offset, expected_ref_edits) in [ ( gix::remote::fetch::Tags::None, "de303ef102bd5705a40a0c42ae2972eb1a668455", 0, 0, ), ( gix::remote::fetch::Tags::Included, "edc8cc8a25e64e73aacea469fc765564dd2c3f65", 1, 7, ), ( gix::remote::fetch::Tags::All, "edc8cc8a25e64e73aacea469fc765564dd2c3f65", 1, 7, ), ] { let (repo, _tmp) = repo_rw("two-origins"); let mut remote = into_daemon_remote_if_async( repo.find_remote("changes-on-top-of-origin")? .with_fetch_tags(fetch_tags), daemon.as_ref(), None, ); remote.replace_refspecs(Some("HEAD"), Fetch)?; let res: gix::remote::fetch::Outcome = remote .connect(Fetch) .await? .prepare_fetch(gix::progress::Discard, Default::default()) .await? .receive(gix::progress::Discard, &AtomicBool::default()) .await?; match res.status { gix::remote::fetch::Status::Change { write_pack_bundle, update_refs, negotiation_rounds } => { assert_eq!(negotiation_rounds, 1); assert_eq!(write_pack_bundle.index.data_hash, hex_to_id(expected_data_hash), ); assert_eq!(write_pack_bundle.index.num_objects, 3 + num_objects_offset, "{fetch_tags:?}"); assert!(write_pack_bundle.data_path.as_deref().map_or(false, |p| p.is_file())); assert!(write_pack_bundle.index_path.as_deref().map_or(false, |p| p.is_file())); assert_eq!(update_refs.edits.len(), expected_ref_edits, "{fetch_tags:?}"); assert_eq!(write_pack_bundle.keep_path.as_deref().map_or(false, |p| p.is_file()), update_refs.edits.is_empty(),".keep are kept if there was no edit to prevent `git gc` from clearing out the pack as it's not referred to necessarily"); }, _ => unreachable!("Naive negotiation sends the same have and wants, resulting in an empty pack (technically no change, but we don't detect it) - empty packs are fine") } } Ok(()) } #[maybe_async::test( feature = "blocking-network-client", async(feature = "async-network-client-async-std", async_std::test) )] async fn fetch_pack() -> crate::Result { let daemon = spawn_git_daemon_if_async({ let mut p = repo_path("base"); p.pop(); p })?; for version in [ None, Some(gix::protocol::transport::Protocol::V2), Some(gix::protocol::transport::Protocol::V1), ] { let (mut repo, _tmp) = repo_rw("two-origins"); if let Some(version) = version { repo.config_snapshot_mut().set_raw_value( "protocol", None, "version", (version as u8).to_string().as_str(), )?; } // No updates { let remote = into_daemon_remote_if_async(repo.find_remote("origin")?, daemon.as_ref(), "base"); { remote .connect(Fetch) .await? .prepare_fetch(progress::Discard, Default::default()) .await?; // early drops are fine and won't block. } let outcome = remote .connect(Fetch) .await? .prepare_fetch(progress::Discard, Default::default()) .await? .receive(progress::Discard, &AtomicBool::default()) .await?; assert!(matches!( outcome.status, gix::remote::fetch::Status::NoPackReceived { .. } )); } // Some updates to be fetched for dry_run in [true, false] { let remote = into_daemon_remote_if_async( repo.find_remote("changes-on-top-of-origin")? .with_fetch_tags(gix::remote::fetch::Tags::None), daemon.as_ref(), "clone-as-base-with-changes", ); let outcome: gix::remote::fetch::Outcome = remote .connect(Fetch) .await? .prepare_fetch(progress::Discard, Default::default()) .await? .with_dry_run(dry_run) .receive(progress::Discard, &AtomicBool::default()) .await?; let refs = match outcome.status { fetch::Status::Change {Lines 1–500 of 594. Load more
mod shallow { use gix::remote::fetch::Shallow; #[test] fn undo() { assert_eq!( Shallow::undo(), Shallow::DepthAtRemote(2147483647u32.try_into().expect("known at compile time")) ); } } #[cfg(any(feature = "blocking-network-client", feature = "async-network-client-async-std"))] mod blocking_and_async_io { use std::sync::atomic::AtomicBool; use gix::remote::{fetch, fetch::Status, Direction::Fetch}; use gix_features::progress; use gix_protocol::maybe_async; use gix_testtools::tempfile::TempDir; use crate::{ remote, remote::{into_daemon_remote_if_async, spawn_git_daemon_if_async}, util::hex_to_id, }; pub(crate) fn base_repo_path() -> String { gix::path::realpath( gix_testtools::scripted_fixture_read_only("make_remote_repos.sh") .unwrap() .join("base"), ) .unwrap() .to_string_lossy() .into_owned() } pub(crate) fn repo_path(name: &str) -> std::path::PathBuf { let dir = gix_testtools::scripted_fixture_read_only_with_args("make_fetch_repos.sh", [base_repo_path()]).unwrap(); dir.join(name) } #[allow(clippy::result_large_err)] pub(crate) fn try_repo_rw( name: &str, ) -> Result<(gix::Repository, gix_testtools::tempfile::TempDir), gix::open::Error> { try_repo_rw_args(name, Vec::<String>::new(), Mode::FastClone) } pub(crate) enum Mode { FastClone, CloneWithShallowSupport, } #[allow(clippy::result_large_err)] pub(crate) fn try_repo_rw_args<S: Into<String>>( name: &str, args: impl IntoIterator<Item = S>, mode: Mode, ) -> Result<(gix::Repository, gix_testtools::tempfile::TempDir), gix::open::Error> { let dir = gix_testtools::scripted_fixture_writable_with_args( "make_fetch_repos.sh", [{ let mut url = base_repo_path(); if matches!(mode, Mode::CloneWithShallowSupport) { url.insert_str(0, "file://"); } url }] .into_iter() .chain(args.into_iter().map(Into::into)), gix_testtools::Creation::ExecuteScript, ) .unwrap(); let repo = gix::open_opts(dir.path().join(name), crate::restricted())?; Ok((repo, dir)) } pub(crate) fn repo_rw(name: &str) -> (gix::Repository, gix_testtools::tempfile::TempDir) { try_repo_rw(name).unwrap() } #[test] #[cfg(feature = "blocking-network-client")] fn collate_fetch_error() -> Result<(), gix::env::collate::fetch::Error<std::io::Error>> { let (repo, _tmp) = try_repo_rw("two-origins")?; let remote = repo .head()? .into_remote(Fetch) .expect("present")? .with_fetch_tags(fetch::Tags::Included); remote .connect(Fetch)? .prepare_fetch(gix::progress::Discard, Default::default())? .receive(gix::progress::Discard, &AtomicBool::default())?; assert!( repo.path() .join("HEAD") .metadata() .map_err(gix::env::collate::fetch::Error::Other)? .is_file(), "just to show off the 'Other' error type" ); Ok(()) } #[test] #[cfg(feature = "blocking-network-client")] fn fetch_with_alternates_adds_tips_from_alternates() -> crate::Result<()> { let tmp = gix_testtools::tempfile::TempDir::new()?; let remote_repo = remote::repo("base"); let (_repo, out) = gix::clone::PrepareFetch::new( remote::repo("multi_round/server").path(), tmp.path(), gix::create::Kind::Bare, Default::default(), gix::open::Options::isolated(), )? .configure_remote({ move |r| { std::fs::write( r.repo().objects.store_ref().path().join("info").join("alternates"), format!( "{}\n", gix::path::realpath(remote_repo.objects.store_ref().path())?.display() ) .as_bytes(), )?; Ok(r) } }) .fetch_only(gix::progress::Discard, &AtomicBool::default())?; match out.status { Status::Change { negotiation_rounds, write_pack_bundle, .. } => { assert_eq!( negotiation_rounds, 1, "we don't really have a way to see that tips from alternates were added, I think" ); assert_eq!( write_pack_bundle.index.num_objects, 66, "this test just exercises code for adding alternate-repo tips to the negotiator" ); } _ => unreachable!("we get a pack as alternates are unrelated"), } Ok(()) } #[maybe_async::test( feature = "blocking-network-client", async(feature = "async-network-client-async-std", async_std::test) )] async fn fetch_with_multi_round_negotiation() -> crate::Result { for (algorithm, expected_negotiation_rounds) in [ (gix::negotiate::Algorithm::Consecutive, 4), (gix::negotiate::Algorithm::Skipping, 2), ] { for version in [ gix::protocol::transport::Protocol::V1, gix::protocol::transport::Protocol::V2, ] { let (mut client_repo, _tmp) = { let client_repo = remote::repo("multi_round/client"); let daemon = spawn_git_daemon_if_async(client_repo.work_dir().expect("non-bare"))?; let tmp = TempDir::new()?; let repo = gix::prepare_clone_bare( daemon.as_ref().map_or_else( || client_repo.git_dir().to_owned(), |d| std::path::PathBuf::from(format!("{}/", d.url)), ), tmp.path(), )? .fetch_only(gix::progress::Discard, &std::sync::atomic::AtomicBool::default()) .await? .0; (repo, tmp) }; { let mut config = client_repo.config_snapshot_mut(); config.set_value( &gix::config::tree::Protocol::VERSION, (version as u8).to_string().as_str(), )?; config.set_value( &gix::config::tree::Fetch::NEGOTIATION_ALGORITHM, algorithm.to_string().as_str(), )?; } let server_repo = remote::repo("multi_round/server"); let daemon = spawn_git_daemon_if_async(server_repo.work_dir().expect("non-bare"))?; let remote = into_daemon_remote_if_async( client_repo.remote_at(server_repo.work_dir().expect("non-bare"))?, daemon.as_ref(), None, ); let changes = remote .with_refspecs(Some("refs/heads/*:refs/remotes/origin/*"), Fetch)? .connect(Fetch) .await? .prepare_fetch(gix::progress::Discard, Default::default()) .await? .receive(gix::progress::Discard, &AtomicBool::default()) .await?; match changes.status { Status::Change { write_pack_bundle, negotiation_rounds, .. } => { assert_eq!( negotiation_rounds, expected_negotiation_rounds, "we need multiple rounds" ); // the server only has our `b1` and an extra commit or two. assert_eq!( write_pack_bundle.index.num_objects, 7, "this is the number git gets as well, we are quite perfectly aligned :)" ); } _ => unreachable!("We expect a pack for sure"), } } } Ok(()) } #[maybe_async::test( feature = "blocking-network-client", async(feature = "async-network-client-async-std", async_std::test) )] async fn fetch_shallow_deepen_zero_does_not_fail() -> crate::Result { let (repo, tmp) = try_repo_rw_args("two-origins", ["--depth=2"], Mode::CloneWithShallowSupport)?; let daemon = spawn_git_daemon_if_async(tmp.path().join("base"))?; let remote = into_daemon_remote_if_async( repo.head()? .into_remote(Fetch) .expect("present")? .with_fetch_tags(fetch::Tags::Included), daemon.as_ref(), None, ); let prev_commits = repo.head_id()?.ancestors().all()?.count(); let changes = remote .connect(Fetch) .await? .prepare_fetch(gix::progress::Discard, Default::default()) .await? .with_shallow(fetch::Shallow::Deepen(0)) .receive(gix::progress::Discard, &AtomicBool::default()) .await?; assert!( matches!(changes.status, Status::NoPackReceived { .. }), "we didn't negotiate at all, but ran all other ref updates" ); assert_eq!( repo.head_id()?.ancestors().all()?.count(), prev_commits, "no more commits are available - we didn't fetch anything and there would be nothing to fetch" ); Ok(()) } #[maybe_async::test( feature = "blocking-network-client", async(feature = "async-network-client-async-std", async_std::test) )] async fn fetch_shallow_deepen_not_possible() -> crate::Result { let (repo, tmp) = try_repo_rw_args("two-origins", ["--depth=2"], Mode::CloneWithShallowSupport)?; let daemon = spawn_git_daemon_if_async(tmp.path().join("base"))?; let remote = into_daemon_remote_if_async( repo.head()? .into_remote(Fetch) .expect("present")? .with_fetch_tags(fetch::Tags::Included), daemon.as_ref(), None, ); assert_eq!( repo.shallow_commits()?.expect("shallow clone").as_slice(), [ hex_to_id("2d9d136fb0765f2e24c44a0f91984318d580d03b"), hex_to_id("dfd0954dabef3b64f458321ef15571cc1a46d552"), hex_to_id("dfd0954dabef3b64f458321ef15571cc1a46d552") ] ); let prev_commits = repo.head_id()?.ancestors().all()?.count(); let changes = remote .connect(Fetch) .await? .prepare_fetch(gix::progress::Discard, Default::default()) .await? .with_shallow(fetch::Shallow::Deepen(1)) .receive(gix::progress::Discard, &AtomicBool::default()) .await?; assert!( matches!(changes.status, Status::Change {write_pack_bundle, ..} if write_pack_bundle.index.num_objects == 0), "we get an empty pack as there is nothing to do" ); assert_eq!( repo.shallow_commits()?.expect("shallow clone").as_slice(), [ hex_to_id("2d9d136fb0765f2e24c44a0f91984318d580d03b"), hex_to_id("dfd0954dabef3b64f458321ef15571cc1a46d552"), hex_to_id("dfd0954dabef3b64f458321ef15571cc1a46d552") ], "the base is shallow, and so is the clone, and we can't extend further" ); assert_eq!( repo.head_id()?.ancestors().all()?.count(), prev_commits, "no more commits are available - there simply isn't more information" ); Ok(()) } #[maybe_async::test( feature = "blocking-network-client", async(feature = "async-network-client-async-std", async_std::test) )] async fn fetch_empty_pack() -> crate::Result { let daemon = spawn_git_daemon_if_async(repo_path("base"))?; for (fetch_tags, expected_ref_count) in [ (fetch::Tags::None, 1), (fetch::Tags::Included, 7), (fetch::Tags::All, 7), ] { let (repo, _tmp) = repo_rw("two-origins"); let mut remote = into_daemon_remote_if_async( repo.head()? .into_remote(Fetch) .expect("present")? .with_fetch_tags(fetch_tags), daemon.as_ref(), None, ); remote.replace_refspecs(Some("HEAD:refs/remotes/origin/does-not-yet-exist"), Fetch)?; let res = remote .connect(Fetch) .await? .prepare_fetch(gix::progress::Discard, Default::default()) .await? .receive(gix::progress::Discard, &AtomicBool::default()) .await?; match res.status { fetch::Status::NoPackReceived { update_refs } => { assert_eq!(update_refs.edits.len(), expected_ref_count); } _ => unreachable!( "default negotiation is able to realize nothing is required and doesn't get to receiving a pack" ), } } Ok(()) } #[maybe_async::test( feature = "blocking-network-client", async(feature = "async-network-client-async-std", async_std::test) )] async fn fetch_pack_without_local_destination() -> crate::Result { let daemon = spawn_git_daemon_if_async(repo_path("clone-as-base-with-changes"))?; for (fetch_tags, expected_data_hash, num_objects_offset, expected_ref_edits) in [ ( gix::remote::fetch::Tags::None, "de303ef102bd5705a40a0c42ae2972eb1a668455", 0, 0, ), ( gix::remote::fetch::Tags::Included, "edc8cc8a25e64e73aacea469fc765564dd2c3f65", 1, 7, ), ( gix::remote::fetch::Tags::All, "edc8cc8a25e64e73aacea469fc765564dd2c3f65", 1, 7, ), ] { let (repo, _tmp) = repo_rw("two-origins"); let mut remote = into_daemon_remote_if_async( repo.find_remote("changes-on-top-of-origin")? .with_fetch_tags(fetch_tags), daemon.as_ref(), None, ); remote.replace_refspecs(Some("HEAD"), Fetch)?; let res: gix::remote::fetch::Outcome = remote .connect(Fetch) .await? .prepare_fetch(gix::progress::Discard, Default::default()) .await? .receive(gix::progress::Discard, &AtomicBool::default()) .await?; match res.status { gix::remote::fetch::Status::Change { write_pack_bundle, update_refs, negotiation_rounds } => { assert_eq!(negotiation_rounds, 1); assert_eq!(write_pack_bundle.index.data_hash, hex_to_id(expected_data_hash), ); assert_eq!(write_pack_bundle.index.num_objects, 3 + num_objects_offset, "{fetch_tags:?}"); assert!(write_pack_bundle.data_path.as_deref().map_or(false, |p| p.is_file())); assert!(write_pack_bundle.index_path.as_deref().map_or(false, |p| p.is_file())); assert_eq!(update_refs.edits.len(), expected_ref_edits, "{fetch_tags:?}"); assert_eq!(write_pack_bundle.keep_path.as_deref().map_or(false, |p| p.is_file()), update_refs.edits.is_empty(),".keep are kept if there was no edit to prevent `git gc` from clearing out the pack as it's not referred to necessarily"); }, _ => unreachable!("Naive negotiation sends the same have and wants, resulting in an empty pack (technically no change, but we don't detect it) - empty packs are fine") } } Ok(()) } #[maybe_async::test( feature = "blocking-network-client", async(feature = "async-network-client-async-std", async_std::test) )] async fn fetch_pack() -> crate::Result { let daemon = spawn_git_daemon_if_async({ let mut p = repo_path("base"); p.pop(); p })?; for version in [ None, Some(gix::protocol::transport::Protocol::V2), Some(gix::protocol::transport::Protocol::V1), ] { let (mut repo, _tmp) = repo_rw("two-origins"); if let Some(version) = version { repo.config_snapshot_mut().set_raw_value( "protocol", None, "version", (version as u8).to_string().as_str(), )?; } // No updates { let remote = into_daemon_remote_if_async(repo.find_remote("origin")?, daemon.as_ref(), "base"); { remote .connect(Fetch) .await? .prepare_fetch(progress::Discard, Default::default()) .await?; // early drops are fine and won't block. } let outcome = remote .connect(Fetch) .await? .prepare_fetch(progress::Discard, Default::default()) .await? .receive(progress::Discard, &AtomicBool::default()) .await?; assert!(matches!( outcome.status, gix::remote::fetch::Status::NoPackReceived { .. } )); } // Some updates to be fetched for dry_run in [true, false] { let remote = into_daemon_remote_if_async( repo.find_remote("changes-on-top-of-origin")? .with_fetch_tags(gix::remote::fetch::Tags::None), daemon.as_ref(), "clone-as-base-with-changes", ); let outcome: gix::remote::fetch::Outcome = remote .connect(Fetch) .await? .prepare_fetch(progress::Discard, Default::default()) .await? .with_dry_run(dry_run) .receive(progress::Discard, &AtomicBool::default()) .await?; let refs = match outcome.status { fetch::Status::Change {