#[cfg(any(feature = "blocking-network-client", feature = "async-network-client-async-std"))] mod blocking_and_async_io { use std::sync::atomic::AtomicBool; use git_features::progress; use git_protocol::maybe_async; use git_repository as git; use git_repository::remote::{fetch, Direction::Fetch}; use crate::{ remote::{into_daemon_remote_if_async, spawn_git_daemon_if_async}, util::hex_to_id, }; pub(crate) fn base_repo_path() -> String { git::path::realpath( git_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 = git_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<(git::Repository, git_testtools::tempfile::TempDir), git::open::Error> { let dir = git_testtools::scripted_fixture_writable_with_args( "make_fetch_repos.sh", [base_repo_path()], git_testtools::Creation::ExecuteScript, ) .unwrap(); let repo = git::open_opts(dir.path().join(name), crate::restricted())?; Ok((repo, dir)) } pub(crate) fn repo_rw(name: &str) -> (git::Repository, git_testtools::tempfile::TempDir) { try_repo_rw(name).unwrap() } #[test] #[cfg(feature = "blocking-network-client")] fn collate_fetch_error() -> Result<(), git::env::collate::fetch::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, git::progress::Discard)? .prepare_fetch(Default::default())? .receive(&AtomicBool::default())?; assert!( repo.path() .join("HEAD") .metadata() .map_err(git::env::collate::fetch::Error::Other)? .is_file(), "just to show off the 'Other' error type" ); 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, git::progress::Discard) .await? .prepare_fetch(Default::default()) .await? .receive(&AtomicBool::default()) .await?; match res.status { fetch::Status::Change { write_pack_bundle, update_refs } => { assert_eq!(write_pack_bundle.index.data_hash, hex_to_id("029d08823bd8a8eab510ad6ac75c823cfd3ed31e")); assert_eq!(write_pack_bundle.index.num_objects, 0, "empty pack"); 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_count); assert!(!write_pack_bundle.keep_path.as_deref().map_or(false, |p| p.is_file()), ".keep files are deleted if at least one ref-edit was made or the pack is empty"); }, _ => 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_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 [ ( git::remote::fetch::Tags::None, "de303ef102bd5705a40a0c42ae2972eb1a668455", 0, 0, ), ( git::remote::fetch::Tags::Included, "edc8cc8a25e64e73aacea469fc765564dd2c3f65", 1, 7, ), ( git::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: git::remote::fetch::Outcome = remote .connect(Fetch, git::progress::Discard) .await? .prepare_fetch(Default::default()) .await? .receive(&AtomicBool::default()) .await?; match res.status { git::remote::fetch::Status::Change { write_pack_bundle, update_refs } => { 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(git::protocol::transport::Protocol::V2), Some(git::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, progress::Discard) .await? .prepare_fetch(Default::default()) .await?; // early drops are fine and won't block. } let outcome = remote .connect(Fetch, progress::Discard) .await? .prepare_fetch(Default::default()) .await? .receive(&AtomicBool::default()) .await?; assert!(matches!( outcome.status, git::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(git::remote::fetch::Tags::None), daemon.as_ref(), "clone-as-base-with-changes", ); let outcome: git::remote::fetch::Outcome = remote .connect(Fetch, progress::Discard) .await? .prepare_fetch(Default::default()) .await? .with_dry_run(dry_run) .receive(&AtomicBool::default()) .await?; let refs = match outcome.status { fetch::Status::Change { write_pack_bundle, update_refs, } => { assert_eq!(write_pack_bundle.pack_version, git::odb::pack::data::Version::V2); assert_eq!(write_pack_bundle.object_hash, repo.object_hash()); assert_eq!(write_pack_bundle.index.num_objects, 4, "{dry_run}: this value is 4 when git does it with 'consecutive' negotiation style, but could be 33 if completely naive."); assert_eq!( write_pack_bundle.index.index_version, git::odb::pack::index::Version::V2 ); assert_eq!( write_pack_bundle.index.index_hash, hex_to_id("d07c527cf14e524a8494ce6d5d08e28079f5c6ea") ); assert!(write_pack_bundle.data_path.map_or(false, |f| f.is_file())); assert!(write_pack_bundle.index_path.map_or(false, |f| f.is_file())); assert_eq!(update_refs.edits.len(), 2); let edit = &update_refs.edits[0]; assert_eq!(edit.name.as_bstr(), "refs/remotes/changes-on-top-of-origin/main"); assert!( edit.change.new_value().expect("no deletion").try_id().is_some(), "a simple peeled ref" ); let edit = &update_refs.edits[1]; assert_eq!(edit.name.as_bstr(), "refs/remotes/changes-on-top-of-origin/symbolic"); assert!( edit.change.new_value().expect("no deletion").try_id().is_some(), "on the remote this is a symbolic ref, we just write its destination object id though" ); assert!( !write_pack_bundle.keep_path.map_or(false, |f| f.is_file()), ".keep files are deleted if there is one edit" ); update_refs } fetch::Status::DryRun { update_refs } => update_refs, fetch::Status::NoPackReceived { .. } => unreachable!("we firmly expect changes here"), }; assert_eq!( refs.updates, vec![ fetch::refs::Update { mode: fetch::refs::update::Mode::New, edit_index: Some(0), }, fetch::refs::Update { mode: fetch::refs::update::Mode::New, edit_index: Some(1), } ] ); for (_update, mapping, _spec, edit) in refs.iter_mapping_updates( &outcome.ref_map.mappings, remote.refspecs(Fetch), &outcome.ref_map.extra_refspecs, ) { let edit = edit.expect("refedit present even if it's a no-op"); if dry_run { assert_eq!( edit.change.new_value().expect("no deletions").id(), mapping.remote.as_id().expect("no unborn") ); assert!( repo.try_find_reference(edit.name.as_ref())?.is_none(), "no ref created in dry-run mode" ); } else { let r = repo.find_reference(edit.name.as_ref()).unwrap(); assert_eq!( r.id(), *mapping.remote.as_id().expect("no unborn"), "local reference should point to remote id" ); } } } } Ok(()) } }