diff --git a/gitoxide-core/src/repository/clone.rs b/gitoxide-core/src/repository/clone.rs index 9f09b71d55e..cf810deb389 100644 --- a/gitoxide-core/src/repository/clone.rs +++ b/gitoxide-core/src/repository/clone.rs @@ -6,6 +6,7 @@ pub struct Options { pub handshake_info: bool, pub no_tags: bool, pub shallow: gix::remote::fetch::Shallow, + pub ref_name: Option, } pub const PROGRESS_RANGE: std::ops::RangeInclusive = 1..=3; @@ -31,6 +32,7 @@ pub(crate) mod function { handshake_info, bare, no_tags, + ref_name, shallow, }: Options, ) -> anyhow::Result<()> @@ -75,6 +77,7 @@ pub(crate) mod function { } let (mut checkout, fetch_outcome) = prepare .with_shallow(shallow) + .with_ref_name(ref_name.as_ref())? .fetch_then_checkout(&mut progress, &gix::interrupt::IS_INTERRUPTED)?; let (repo, outcome) = if bare { diff --git a/gix-attributes/tests/search/mod.rs b/gix-attributes/tests/search/mod.rs index 36e6286c8ed..6ce6f7b3d3d 100644 --- a/gix-attributes/tests/search/mod.rs +++ b/gix-attributes/tests/search/mod.rs @@ -307,11 +307,8 @@ mod baseline { let mut buf = Vec::new(); let mut collection = MetadataCollection::default(); - let group = gix_attributes::Search::new_globals( - &mut [base.join("user.attributes")].into_iter(), - &mut buf, - &mut collection, - )?; + let group = + gix_attributes::Search::new_globals([base.join("user.attributes")].into_iter(), &mut buf, &mut collection)?; Ok((group, collection, base, input)) } diff --git a/gix-odb/tests/odb/find/mod.rs b/gix-odb/tests/odb/find/mod.rs index 3a93a03f1af..77d515e9505 100644 --- a/gix-odb/tests/odb/find/mod.rs +++ b/gix-odb/tests/odb/find/mod.rs @@ -16,7 +16,7 @@ fn can_find(db: impl gix_object::Find, hex_id: &str) { #[test] fn loose_object() { - can_find(&db(), "37d4e6c5c48ba0d245164c4e10d5f41140cab980"); + can_find(db(), "37d4e6c5c48ba0d245164c4e10d5f41140cab980"); } #[test] diff --git a/gix-odb/tests/odb/header/mod.rs b/gix-odb/tests/odb/header/mod.rs index 4094a7399c8..8e059729316 100644 --- a/gix-odb/tests/odb/header/mod.rs +++ b/gix-odb/tests/odb/header/mod.rs @@ -8,7 +8,7 @@ fn find_header(db: impl gix_odb::Header, hex_id: &str) -> gix_odb::find::Header #[test] fn loose_object() { - find_header(&db(), "37d4e6c5c48ba0d245164c4e10d5f41140cab980"); + find_header(db(), "37d4e6c5c48ba0d245164c4e10d5f41140cab980"); } #[test] diff --git a/gix-pack/tests/pack/index.rs b/gix-pack/tests/pack/index.rs index 67f991d0f8d..bb440b76ced 100644 --- a/gix-pack/tests/pack/index.rs +++ b/gix-pack/tests/pack/index.rs @@ -363,8 +363,8 @@ fn pack_lookup() -> Result<(), Box> { }, ), ] { - let idx = index::File::at(&fixture_path(index_path), gix_hash::Kind::Sha1)?; - let pack = pack::data::File::at(&fixture_path(pack_path), gix_hash::Kind::Sha1)?; + let idx = index::File::at(fixture_path(index_path), gix_hash::Kind::Sha1)?; + let pack = pack::data::File::at(fixture_path(pack_path), gix_hash::Kind::Sha1)?; assert_eq!(pack.version(), pack::data::Version::V2); assert_eq!(pack.num_objects(), idx.num_objects()); @@ -471,7 +471,7 @@ fn iter() -> Result<(), Box> { "0f3ea84cd1bba10c2a03d736a460635082833e59", ), ] { - let idx = index::File::at(&fixture_path(path), gix_hash::Kind::Sha1)?; + let idx = index::File::at(fixture_path(path), gix_hash::Kind::Sha1)?; assert_eq!(idx.version(), *kind); assert_eq!(idx.num_objects(), *num_objects); assert_eq!( diff --git a/gix/src/clone/access.rs b/gix/src/clone/access.rs index 966c54b6671..ceada84472b 100644 --- a/gix/src/clone/access.rs +++ b/gix/src/clone/access.rs @@ -11,7 +11,7 @@ impl PrepareFetch { /// /// It can also be used to configure additional options, like those for fetching tags. Note that /// [`with_fetch_tags()`](crate::Remote::with_fetch_tags()) should be called here to configure the clone as desired. - /// Otherwise a clone is configured to be complete and fetches all tags, not only those reachable from all branches. + /// Otherwise, a clone is configured to be complete and fetches all tags, not only those reachable from all branches. pub fn configure_remote( mut self, f: impl FnMut(crate::Remote<'_>) -> Result, Box> + 'static, @@ -42,6 +42,19 @@ impl PrepareFetch { self.config_overrides = values.into_iter().map(Into::into).collect(); self } + + /// Set the `name` of the reference to check out, instead of the remote `HEAD`. + /// If `None`, the `HEAD` will be used, which is the default. + /// + /// Note that `name` should be a partial name like `main` or `feat/one`, but can be a full ref name. + /// If a branch on the remote matches, it will automatically be retrieved even without a refspec. + pub fn with_ref_name<'a, Name, E>(mut self, name: Option) -> Result + where + Name: TryInto<&'a gix_ref::PartialNameRef, Error = E>, + { + self.ref_name = name.map(TryInto::try_into).transpose()?.map(ToOwned::to_owned); + Ok(self) + } } /// Consumption diff --git a/gix/src/clone/checkout.rs b/gix/src/clone/checkout.rs index e04a32fc484..d980e02a873 100644 --- a/gix/src/clone/checkout.rs +++ b/gix/src/clone/checkout.rs @@ -28,6 +28,8 @@ pub mod main_worktree { CheckoutOptions(#[from] crate::config::checkout_options::Error), #[error(transparent)] IndexCheckout(#[from] gix_worktree_state::checkout::Error), + #[error(transparent)] + Peel(#[from] crate::reference::peel::Error), #[error("Failed to reopen object database as Arc (only if thread-safety wasn't compiled in)")] OpenArcOdb(#[from] std::io::Error), #[error("The HEAD reference could not be located")] @@ -62,7 +64,13 @@ pub mod main_worktree { /// on thread per logical core. /// /// Note that this is a no-op if the remote was empty, leaving this repository empty as well. This can be validated by checking - /// if the `head()` of the returned repository is not unborn. + /// if the `head()` of the returned repository is *not* unborn. + /// + /// # Panics + /// + /// If called after it was successful. The reason here is that it auto-deletes the contained repository, + /// and keeps track of this by means of keeping just one repository instance, which is passed to the user + /// after success. pub fn main_worktree

( &mut self, mut progress: P, @@ -84,19 +92,26 @@ pub mod main_worktree { let repo = self .repo .as_ref() - .expect("still present as we never succeeded the worktree checkout yet"); + .expect("BUG: this method may only be called until it is successful"); let workdir = repo.work_dir().ok_or_else(|| Error::BareRepository { git_dir: repo.git_dir().to_owned(), })?; - let root_tree = match repo.head()?.try_peel_to_id_in_place()? { + + let root_tree_id = match &self.ref_name { + Some(reference_val) => Some(repo.find_reference(reference_val)?.peel_to_id_in_place()?), + None => repo.head()?.try_peel_to_id_in_place()?, + }; + + let root_tree = match root_tree_id { Some(id) => id.object().expect("downloaded from remote").peel_to_tree()?.id, None => { return Ok(( self.repo.take().expect("still present"), gix_worktree_state::checkout::Outcome::default(), - )) + )); } }; + let index = gix_index::State::from_tree(&root_tree, &repo.objects, repo.config.protect_options()?) .map_err(|err| Error::IndexFromTree { id: root_tree, @@ -129,7 +144,7 @@ pub mod main_worktree { bytes.show_throughput(start); index.write(Default::default())?; - Ok((self.repo.take().expect("still present"), outcome)) + Ok((self.repo.take().expect("still present").clone(), outcome)) } } } diff --git a/gix/src/clone/fetch/mod.rs b/gix/src/clone/fetch/mod.rs index 8fdee98edba..e5f18b3b2e7 100644 --- a/gix/src/clone/fetch/mod.rs +++ b/gix/src/clone/fetch/mod.rs @@ -1,3 +1,5 @@ +use crate::bstr::BString; +use crate::bstr::ByteSlice; use crate::clone::PrepareFetch; /// The error returned by [`PrepareFetch::fetch_only()`]. @@ -35,6 +37,13 @@ pub enum Error { }, #[error("Failed to update HEAD with values from remote")] HeadUpdate(#[from] crate::reference::edit::Error), + #[error("The remote didn't have any ref that matched '{}'", wanted.as_ref().as_bstr())] + RefNameMissing { wanted: gix_ref::PartialName }, + #[error("The remote has {} refs for '{}', try to use a specific name: {}", candidates.len(), wanted.as_ref().as_bstr(), candidates.iter().filter_map(|n| n.to_str().ok()).collect::>().join(", "))] + RefNameAmbiguous { + wanted: gix_ref::PartialName, + candidates: Vec, + }, } /// Modification @@ -117,7 +126,7 @@ impl PrepareFetch { remote = remote.with_fetch_tags(fetch_tags); } - // Add HEAD after the remote was written to config, we need it to know what to checkout later, and assure + // Add HEAD after the remote was written to config, we need it to know what to check out later, and assure // the ref that HEAD points to is present no matter what. let head_refspec = gix_refspec::parse( format!("HEAD:refs/remotes/{remote_name}/HEAD").as_str().into(), @@ -136,10 +145,22 @@ impl PrepareFetch { if !opts.extra_refspecs.contains(&head_refspec) { opts.extra_refspecs.push(head_refspec) } + if let Some(ref_name) = &self.ref_name { + opts.extra_refspecs.push( + gix_refspec::parse(ref_name.as_ref().as_bstr(), gix_refspec::parse::Operation::Fetch) + .expect("partial names are valid refspecs") + .to_owned(), + ); + } opts }) .await? }; + + // Assure problems with custom branch names fail early, not after getting the pack or during negotiation. + if let Some(ref_name) = &self.ref_name { + util::find_custom_refname(pending_pack.ref_map(), ref_name)?; + } if pending_pack.ref_map().object_hash != repo.object_hash() { unimplemented!("configure repository to expect a different object hash as advertised by the server") } @@ -160,9 +181,10 @@ impl PrepareFetch { util::append_config_to_repo_config(repo, config); util::update_head( repo, - &outcome.ref_map.remote_refs, + &outcome.ref_map, reflog_message.as_ref(), remote_name.as_ref(), + self.ref_name.as_ref(), )?; Ok((self.repo.take().expect("still present"), outcome)) @@ -180,7 +202,13 @@ impl PrepareFetch { P::SubProgress: 'static, { let (repo, fetch_outcome) = self.fetch_only(progress, should_interrupt)?; - Ok((crate::clone::PrepareCheckout { repo: repo.into() }, fetch_outcome)) + Ok(( + crate::clone::PrepareCheckout { + repo: repo.into(), + ref_name: self.ref_name.clone(), + }, + fetch_outcome, + )) } } diff --git a/gix/src/clone/fetch/util.rs b/gix/src/clone/fetch/util.rs index e50ec7509d0..fde5241edcd 100644 --- a/gix/src/clone/fetch/util.rs +++ b/gix/src/clone/fetch/util.rs @@ -2,7 +2,7 @@ use std::{borrow::Cow, io::Write}; use gix_ref::{ transaction::{LogChange, RefLog}, - FullNameRef, + FullNameRef, PartialName, }; use super::Error; @@ -60,35 +60,40 @@ pub fn append_config_to_repo_config(repo: &mut Repository, config: gix_config::F /// HEAD cannot be written by means of refspec by design, so we have to do it manually here. Also create the pointed-to ref /// if we have to, as it might not have been naturally included in the ref-specs. +/// Lastly, use `ref_name` if it was provided instead, and let `HEAD` point to it. pub fn update_head( repo: &mut Repository, - remote_refs: &[gix_protocol::handshake::Ref], + ref_map: &crate::remote::fetch::RefMap, reflog_message: &BStr, remote_name: &BStr, + ref_name: Option<&PartialName>, ) -> Result<(), Error> { use gix_ref::{ transaction::{PreviousValue, RefEdit}, Target, }; - let (head_peeled_id, head_ref) = match remote_refs.iter().find_map(|r| { - Some(match r { - gix_protocol::handshake::Ref::Symbolic { - full_ref_name, - target, - tag: _, - object, - } if full_ref_name == "HEAD" => (Some(object.as_ref()), Some(target)), - gix_protocol::handshake::Ref::Direct { full_ref_name, object } if full_ref_name == "HEAD" => { - (Some(object.as_ref()), None) - } - gix_protocol::handshake::Ref::Unborn { full_ref_name, target } if full_ref_name == "HEAD" => { - (None, Some(target)) - } - _ => return None, - }) - }) { - Some(t) => t, - None => return Ok(()), + let head_info = match ref_name { + Some(ref_name) => Some(find_custom_refname(ref_map, ref_name)?), + None => ref_map.remote_refs.iter().find_map(|r| { + Some(match r { + gix_protocol::handshake::Ref::Symbolic { + full_ref_name, + target, + tag: _, + object, + } if full_ref_name == "HEAD" => (Some(object.as_ref()), Some(target.as_bstr())), + gix_protocol::handshake::Ref::Direct { full_ref_name, object } if full_ref_name == "HEAD" => { + (Some(object.as_ref()), None) + } + gix_protocol::handshake::Ref::Unborn { full_ref_name, target } if full_ref_name == "HEAD" => { + (None, Some(target.as_bstr())) + } + _ => return None, + }) + }), + }; + let Some((head_peeled_id, head_ref)) = head_info else { + return Ok(()); }; let head: gix_ref::FullName = "HEAD".try_into().expect("valid"); @@ -178,7 +183,55 @@ pub fn update_head( Ok(()) } -/// Setup the remote configuration for `branch` so that it points to itself, but on the remote, if and only if currently +pub(super) fn find_custom_refname<'a>( + ref_map: &'a crate::remote::fetch::RefMap, + ref_name: &PartialName, +) -> Result<(Option<&'a gix_hash::oid>, Option<&'a BStr>), Error> { + let group = gix_refspec::MatchGroup::from_fetch_specs(Some( + gix_refspec::parse(ref_name.as_ref().as_bstr(), gix_refspec::parse::Operation::Fetch) + .expect("partial names are valid refs"), + )); + // TODO: to fix ambiguity, implement priority system + let filtered_items: Vec<_> = ref_map + .mappings + .iter() + .filter_map(|m| { + m.remote + .as_name() + .and_then(|name| m.remote.as_id().map(|id| (name, id))) + }) + .map(|(full_ref_name, target)| gix_refspec::match_group::Item { + full_ref_name, + target, + object: None, + }) + .collect(); + let res = group.match_remotes(filtered_items.iter().copied()); + match res.mappings.len() { + 0 => Err(Error::RefNameMissing { + wanted: ref_name.clone(), + }), + 1 => { + let item = filtered_items[res.mappings[0] + .item_index + .expect("we map by name only and have no object-id in refspec")]; + Ok((Some(item.target), Some(item.full_ref_name))) + } + _ => Err(Error::RefNameAmbiguous { + wanted: ref_name.clone(), + candidates: res + .mappings + .iter() + .filter_map(|m| match m.lhs { + gix_refspec::match_group::SourceRef::FullName(name) => Some(name.to_owned()), + gix_refspec::match_group::SourceRef::ObjectId(_) => None, + }) + .collect(), + }), + } +} + +/// Set up the remote configuration for `branch` so that it points to itself, but on the remote, if and only if currently /// saved refspecs are able to match it. /// For that we reload the remote of `remote_name` and use its `ref_specs` for match. fn setup_branch_config( diff --git a/gix/src/clone/mod.rs b/gix/src/clone/mod.rs index 03328253e78..0dd49c12f5a 100644 --- a/gix/src/clone/mod.rs +++ b/gix/src/clone/mod.rs @@ -34,6 +34,9 @@ pub struct PrepareFetch { /// How to handle shallow clones #[cfg_attr(not(feature = "blocking-network-client"), allow(dead_code))] shallow: remote::fetch::Shallow, + /// The name of the reference to fetch. If `None`, the reference pointed to by `HEAD` will be checked out. + #[cfg_attr(not(feature = "blocking-network-client"), allow(dead_code))] + ref_name: Option, } /// The error returned by [`PrepareFetch::new()`]. @@ -132,17 +135,21 @@ impl PrepareFetch { #[cfg(any(feature = "async-network-client", feature = "blocking-network-client"))] configure_connection: None, shallow: remote::fetch::Shallow::NoChange, + ref_name: None, }) } } -/// A utility to collect configuration on how to perform a checkout into a working tree, and when dropped without checking out successfully -/// the fetched repository will be dropped. +/// A utility to collect configuration on how to perform a checkout into a working tree, +/// and when dropped without checking out successfully the fetched repository will be deleted from disk. #[must_use] #[cfg(feature = "worktree-mutation")] +#[derive(Debug)] pub struct PrepareCheckout { - /// A freshly initialized repository which is owned by us, or `None` if it was handed to the user + /// A freshly initialized repository which is owned by us, or `None` if it was successfully checked out. pub(self) repo: Option, + /// The name of the reference to check out. If `None`, the reference pointed to by `HEAD` will be checked out. + pub(self) ref_name: Option, } // This module encapsulates functionality that works with both feature toggles. Can be combined with `fetch` diff --git a/gix/src/remote/connection/fetch/negotiate.rs b/gix/src/remote/connection/fetch/negotiate.rs index 42b578ee490..d9e73fe7dbc 100644 --- a/gix/src/remote/connection/fetch/negotiate.rs +++ b/gix/src/remote/connection/fetch/negotiate.rs @@ -38,7 +38,7 @@ pub(crate) enum Action { SkipToRefUpdate, /// We can't know for sure if fetching *is not* needed, so we go ahead and negotiate. MustNegotiate { - /// Each `ref_map.mapping` has a slot here which is `true` if we have the object the remote ref points to locally. + /// Each `ref_map.mapping` has a slot here which is `true` if we have the object the remote ref points to, locally. remote_ref_target_known: Vec, }, } @@ -221,7 +221,7 @@ pub(crate) fn add_wants( shallow: &fetch::Shallow, mapping_is_ignored: impl Fn(&fetch::Mapping) -> bool, ) { - // When using shallow, we can't exclude `wants` as the remote won't send anything then. Thus we have to resend everything + // When using shallow, we can't exclude `wants` as the remote won't send anything then. Thus, we have to resend everything // we have as want instead to get exactly the same graph, but possibly deepened. let is_shallow = !matches!(shallow, fetch::Shallow::NoChange); let wants = ref_map diff --git a/gix/src/remote/connection/fetch/receive_pack.rs b/gix/src/remote/connection/fetch/receive_pack.rs index ac48473e1bc..f49ff6dcce1 100644 --- a/gix/src/remote/connection/fetch/receive_pack.rs +++ b/gix/src/remote/connection/fetch/receive_pack.rs @@ -114,7 +114,7 @@ where gix_protocol::fetch::Response::check_required_features(protocol_version, &fetch_features)?; let sideband_all = fetch_features.iter().any(|(n, _)| *n == "sideband-all"); let mut arguments = gix_protocol::fetch::Arguments::new(protocol_version, fetch_features, con.trace); - if matches!(con.remote.fetch_tags, crate::remote::fetch::Tags::Included) { + if matches!(con.remote.fetch_tags, fetch::Tags::Included) { if !arguments.can_use_include_tag() { return Err(Error::MissingServerFeature { feature: "include-tag", diff --git a/gix/src/remote/connection/ref_map.rs b/gix/src/remote/connection/ref_map.rs index c33471d5416..264984ecb8a 100644 --- a/gix/src/remote/connection/ref_map.rs +++ b/gix/src/remote/connection/ref_map.rs @@ -79,14 +79,14 @@ where /// for _fetching_. /// /// This comes in the form of all matching tips on the remote and the object they point to, along with - /// with the local tracking branch of these tips (if available). + /// the local tracking branch of these tips (if available). /// /// Note that this doesn't fetch the objects mentioned in the tips nor does it make any change to underlying repository. /// /// # Consumption /// - /// Due to management of the transport, it's cleanest to only use it for a single interaction. Thus it's consumed along with - /// the connection. + /// Due to management of the transport, it's cleanest to only use it for a single interaction. Thus, it's consumed + /// along with the connection. /// /// ### Configuration /// diff --git a/gix/tests/clone/mod.rs b/gix/tests/clone/mod.rs index e35b71e5dbe..3c3c4741d77 100644 --- a/gix/tests/clone/mod.rs +++ b/gix/tests/clone/mod.rs @@ -1,7 +1,8 @@ use crate::{remote, util::restricted}; -#[cfg(feature = "blocking-network-client")] +#[cfg(all(feature = "worktree-mutation", feature = "blocking-network-client"))] mod blocking_io { + use std::path::Path; use std::{borrow::Cow, sync::atomic::AtomicBool}; use gix::{ @@ -500,12 +501,128 @@ mod blocking_io { let index = repo.index()?; assert_eq!(index.entries().len(), 1, "All entries are known as per HEAD tree"); - let work_dir = repo.work_dir().expect("non-bare"); + assure_index_entries_on_disk(&index, repo.work_dir().expect("non-bare")); + Ok(()) + } + #[test] + fn fetch_and_checkout_specific_ref() -> crate::Result { + let tmp = gix_testtools::tempfile::TempDir::new()?; + let remote_repo = remote::repo("base"); + let ref_to_checkout = "a"; + let mut prepare = gix::clone::PrepareFetch::new( + remote_repo.path(), + tmp.path(), + gix::create::Kind::WithWorktree, + Default::default(), + restricted(), + )? + .with_ref_name(Some(ref_to_checkout))?; + let (mut checkout, _out) = + prepare.fetch_then_checkout(gix::progress::Discard, &std::sync::atomic::AtomicBool::default())?; + + let (repo, _) = checkout.main_worktree(gix::progress::Discard, &std::sync::atomic::AtomicBool::default())?; + + assert_eq!( + repo.references()?.all()?.count() - 2, + remote_repo.references()?.all()?.count(), + "all references have been cloned, + remote HEAD + remote main (not listed in remote_repo)" + ); + let checked_out_ref = repo.head_ref()?.expect("head points to ref"); + let remote_ref_name = format!("refs/heads/{ref_to_checkout}"); + assert_eq!( + checked_out_ref.name().as_bstr(), + remote_ref_name, + "it's possible to checkout anything with that name, but here we have an ordinary branch" + ); + + assert_eq!( + checked_out_ref + .remote_ref_name(gix::remote::Direction::Fetch) + .transpose()? + .unwrap() + .as_bstr(), + remote_ref_name, + "the merge configuration is using the given name" + ); + + let index = repo.index()?; + assert_eq!(index.entries().len(), 1, "All entries are known as per HEAD tree"); + + assure_index_entries_on_disk(&index, repo.work_dir().expect("non-bare")); + Ok(()) + } + + #[test] + fn fetch_and_checkout_specific_non_existing() -> crate::Result { + let tmp = gix_testtools::tempfile::TempDir::new()?; + let remote_repo = remote::repo("base"); + let ref_to_checkout = "does-not-exist"; + let mut prepare = gix::clone::PrepareFetch::new( + remote_repo.path(), + tmp.path(), + gix::create::Kind::WithWorktree, + Default::default(), + restricted(), + )? + .with_ref_name(Some(ref_to_checkout))?; + + let err = prepare + .fetch_then_checkout(gix::progress::Discard, &std::sync::atomic::AtomicBool::default()) + .unwrap_err(); + assert_eq!( + err.to_string(), + "The remote didn't have any ref that matched 'does-not-exist'", + "we don't test this, but it's important that it determines this before receiving a pack" + ); + Ok(()) + } + + #[test] + fn fetch_and_checkout_specific_annotated_tag() -> crate::Result { + let tmp = gix_testtools::tempfile::TempDir::new()?; + let remote_repo = remote::repo("base"); + let ref_to_checkout = "annotated-detached-tag"; + let mut prepare = gix::clone::PrepareFetch::new( + remote_repo.path(), + tmp.path(), + gix::create::Kind::WithWorktree, + Default::default(), + restricted(), + )? + .with_ref_name(Some(ref_to_checkout))?; + let (mut checkout, _out) = + prepare.fetch_then_checkout(gix::progress::Discard, &std::sync::atomic::AtomicBool::default())?; + + let (repo, _) = checkout.main_worktree(gix::progress::Discard, &std::sync::atomic::AtomicBool::default())?; + + assert_eq!( + repo.references()?.all()?.count() - 1, + remote_repo.references()?.all()?.count(), + "all references have been cloned, + remote HEAD (not listed in remote_repo)" + ); + let checked_out_ref = repo.head_ref()?.expect("head points to ref"); + let remote_ref_name = format!("refs/tags/{ref_to_checkout}"); + assert_eq!( + checked_out_ref.name().as_bstr(), + remote_ref_name, + "it also works with tags" + ); + + assert_eq!( + checked_out_ref + .remote_ref_name(gix::remote::Direction::Fetch) + .transpose()?, + None, + "there is no merge configuration for tags" + ); + Ok(()) + } + + fn assure_index_entries_on_disk(index: &gix::worktree::Index, work_dir: &Path) { for entry in index.entries() { - let entry_path = work_dir.join(gix_path::from_bstr(entry.path(&index))); + let entry_path = work_dir.join(gix_path::from_bstr(entry.path(index))); assert!(entry_path.is_file(), "{entry_path:?} not found on disk") } - Ok(()) } #[test] diff --git a/gix/tests/repository/object.rs b/gix/tests/repository/object.rs index 3761c0e8209..2aa2607cc69 100644 --- a/gix/tests/repository/object.rs +++ b/gix/tests/repository/object.rs @@ -6,7 +6,7 @@ mod write_object { #[test] fn empty_tree() -> crate::Result { let (_tmp, repo) = empty_bare_repo()?; - let oid = repo.write_object(&gix::objs::TreeRef::empty())?; + let oid = repo.write_object(gix::objs::TreeRef::empty())?; assert_eq!( oid, gix::hash::ObjectId::empty_tree(repo.object_hash()), @@ -280,7 +280,7 @@ mod commit { crate::restricted(), )? .to_thread_local(); - let empty_tree_id = repo.write_object(&gix::objs::Tree::empty())?.detach(); + let empty_tree_id = repo.write_object(gix::objs::Tree::empty())?.detach(); let err = repo .commit("HEAD", "initial", empty_tree_id, [empty_tree_id]) .unwrap_err(); @@ -304,7 +304,7 @@ mod commit { restricted_and_git(), )? .to_thread_local(); - let empty_tree_id = repo.write_object(&gix::objs::Tree::empty())?; + let empty_tree_id = repo.write_object(gix::objs::Tree::empty())?; let commit_id = repo.commit("HEAD", "initial", empty_tree_id, gix::commit::NO_PARENT_IDS)?; assert_eq!( commit_id, diff --git a/gix/tests/repository/worktree.rs b/gix/tests/repository/worktree.rs index 16dfb35073d..19bce459584 100644 --- a/gix/tests/repository/worktree.rs +++ b/gix/tests/repository/worktree.rs @@ -49,7 +49,7 @@ mod with_core_worktree_config { } else { assert_eq!( repo.work_dir().unwrap(), - gix_path::realpath(&repo.git_dir().parent().unwrap().parent().unwrap().join("worktree"))?, + gix_path::realpath(repo.git_dir().parent().unwrap().parent().unwrap().join("worktree"))?, "absolute workdirs are left untouched" ); } diff --git a/src/plumbing/main.rs b/src/plumbing/main.rs index de177127e44..b4c788952f1 100644 --- a/src/plumbing/main.rs +++ b/src/plumbing/main.rs @@ -404,6 +404,7 @@ pub fn main() -> Result<()> { handshake_info, bare, no_tags, + ref_name, remote, shallow, directory, @@ -413,6 +414,7 @@ pub fn main() -> Result<()> { bare, handshake_info, no_tags, + ref_name, shallow: shallow.into(), }; prepare_and_run( diff --git a/src/plumbing/options/mod.rs b/src/plumbing/options/mod.rs index 229afc4ed13..af1374241fb 100644 --- a/src/plumbing/options/mod.rs +++ b/src/plumbing/options/mod.rs @@ -434,6 +434,10 @@ pub mod clone { /// The url of the remote to connect to, like `https://github.com/byron/gitoxide`. pub remote: OsString, + /// The name of the reference to check out. + #[clap(long = "ref", value_parser = crate::shared::AsPartialRefName, value_name = "REF_NAME")] + pub ref_name: Option, + /// The directory to initialize with the new repository and to which all data should be written. pub directory: Option, }