From 0912a4649134c33251fee18e2c030c68a10c19bd Mon Sep 17 00:00:00 2001 From: FernTheDev <15272073+Fernthedev@users.noreply.github.com> Date: Thu, 20 Jun 2024 08:57:55 +0200 Subject: [PATCH 1/5] Squash 11 commits that get started with allowing to checkout a particular branch --- gix/src/clone/checkout.rs | 33 ++++++++++++++++++++++++++++++--- gix/tests/clone/mod.rs | 39 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 68 insertions(+), 4 deletions(-) diff --git a/gix/src/clone/checkout.rs b/gix/src/clone/checkout.rs index e04a32fc484..6c0816531cf 100644 --- a/gix/src/clone/checkout.rs +++ b/gix/src/clone/checkout.rs @@ -5,6 +5,8 @@ use crate::{clone::PrepareCheckout, Repository}; pub mod main_worktree { use std::{path::PathBuf, sync::atomic::AtomicBool}; + use gix_ref::bstr::BStr; + use crate::{clone::PrepareCheckout, Progress, Repository}; /// The error returned by [`PrepareCheckout::main_worktree()`]. @@ -28,6 +30,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")] @@ -72,13 +76,29 @@ pub mod main_worktree { P: gix_features::progress::NestedProgress, P::SubProgress: gix_features::progress::NestedProgress + 'static, { - self.main_worktree_inner(&mut progress, should_interrupt) + self.main_worktree_inner(&mut progress, should_interrupt, None) + } + + /// Checkout the a worktree, determining how many threads to use by looking at `checkout.workers`, defaulting to using + /// on thread per logical core. + pub fn worktree

( + &mut self, + mut progress: P, + should_interrupt: &AtomicBool, + reference: Option<&BStr>, + ) -> Result<(Repository, gix_worktree_state::checkout::Outcome), Error> + where + P: gix_features::progress::NestedProgress, + P::SubProgress: gix_features::progress::NestedProgress + 'static, + { + self.main_worktree_inner(&mut progress, should_interrupt, reference) } fn main_worktree_inner( &mut self, progress: &mut dyn gix_features::progress::DynNestedProgress, should_interrupt: &AtomicBool, + reference: Option<&BStr>, ) -> Result<(Repository, gix_worktree_state::checkout::Outcome), Error> { let _span = gix_trace::coarse!("gix::clone::PrepareCheckout::main_worktree()"); let repo = self @@ -88,15 +108,22 @@ pub mod main_worktree { 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 reference { + 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, diff --git a/gix/tests/clone/mod.rs b/gix/tests/clone/mod.rs index e35b71e5dbe..c38a5735525 100644 --- a/gix/tests/clone/mod.rs +++ b/gix/tests/clone/mod.rs @@ -5,7 +5,7 @@ mod blocking_io { use std::{borrow::Cow, sync::atomic::AtomicBool}; use gix::{ - bstr::BString, + bstr::{BStr, BString}, config::tree::{Clone, Core, Init, Key}, remote::{ fetch::{Shallow, SpecIndex}, @@ -507,6 +507,43 @@ mod blocking_io { } Ok(()) } + #[test] + fn fetch_and_checkout_branch() -> crate::Result { + let tmp = gix_testtools::tempfile::TempDir::new()?; + let mut prepare = gix::clone::PrepareFetch::new( + remote::repo("base").path(), + tmp.path(), + gix::create::Kind::WithWorktree, + Default::default(), + restricted(), + )?; + let (mut checkout, _out) = + prepare.fetch_then_checkout(gix::progress::Discard, &std::sync::atomic::AtomicBool::default())?; + + let branch_names = checkout.repo().branch_names(); + let target_branch: &BStr = "a".into(); + let branch_result = checkout.repo().find_reference(target_branch); + + assert!( + branch_result.is_ok(), + "branch {target_branch} not found: {branch_result:?}. Available branches: {branch_names:?}" + ); + let (repo, _) = checkout.worktree( + gix::progress::Discard, + &std::sync::atomic::AtomicBool::default(), + Some("a".into()), + )?; + + 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"); + for entry in index.entries() { + 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] fn fetch_and_checkout_empty_remote_repo() -> crate::Result { From acbfa6fb5f749e84e6c9f34c3c97b02f97db5f68 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 20 Jun 2024 09:07:56 +0200 Subject: [PATCH 2/5] feat: add `PrepareFetch::with_ref_name()` to control which ref is checked out. --- gix/src/clone/access.rs | 15 ++- gix/src/clone/checkout.rs | 24 +--- gix/src/clone/fetch/mod.rs | 34 ++++- gix/src/clone/fetch/util.rs | 97 +++++++++++--- gix/src/clone/mod.rs | 7 + gix/src/remote/connection/fetch/negotiate.rs | 4 +- .../remote/connection/fetch/receive_pack.rs | 2 +- gix/src/remote/connection/ref_map.rs | 6 +- gix/tests/clone/mod.rs | 126 ++++++++++++++---- 9 files changed, 239 insertions(+), 76 deletions(-) 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 6c0816531cf..7d69681a47b 100644 --- a/gix/src/clone/checkout.rs +++ b/gix/src/clone/checkout.rs @@ -5,8 +5,6 @@ use crate::{clone::PrepareCheckout, Repository}; pub mod main_worktree { use std::{path::PathBuf, sync::atomic::AtomicBool}; - use gix_ref::bstr::BStr; - use crate::{clone::PrepareCheckout, Progress, Repository}; /// The error returned by [`PrepareCheckout::main_worktree()`]. @@ -66,7 +64,7 @@ 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. pub fn main_worktree

( &mut self, mut progress: P, @@ -76,29 +74,13 @@ pub mod main_worktree { P: gix_features::progress::NestedProgress, P::SubProgress: gix_features::progress::NestedProgress + 'static, { - self.main_worktree_inner(&mut progress, should_interrupt, None) - } - - /// Checkout the a worktree, determining how many threads to use by looking at `checkout.workers`, defaulting to using - /// on thread per logical core. - pub fn worktree

( - &mut self, - mut progress: P, - should_interrupt: &AtomicBool, - reference: Option<&BStr>, - ) -> Result<(Repository, gix_worktree_state::checkout::Outcome), Error> - where - P: gix_features::progress::NestedProgress, - P::SubProgress: gix_features::progress::NestedProgress + 'static, - { - self.main_worktree_inner(&mut progress, should_interrupt, reference) + self.main_worktree_inner(&mut progress, should_interrupt) } fn main_worktree_inner( &mut self, progress: &mut dyn gix_features::progress::DynNestedProgress, should_interrupt: &AtomicBool, - reference: Option<&BStr>, ) -> Result<(Repository, gix_worktree_state::checkout::Outcome), Error> { let _span = gix_trace::coarse!("gix::clone::PrepareCheckout::main_worktree()"); let repo = self @@ -109,7 +91,7 @@ pub mod main_worktree { git_dir: repo.git_dir().to_owned(), })?; - let root_tree_id = match reference { + 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()?, }; 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..ea225678437 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,6 +135,7 @@ impl PrepareFetch { #[cfg(any(feature = "async-network-client", feature = "blocking-network-client"))] configure_connection: None, shallow: remote::fetch::Shallow::NoChange, + ref_name: None, }) } } @@ -140,9 +144,12 @@ impl PrepareFetch { /// the fetched repository will be dropped. #[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 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 c38a5735525..4fc7c089072 100644 --- a/gix/tests/clone/mod.rs +++ b/gix/tests/clone/mod.rs @@ -1,11 +1,12 @@ 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::{ - bstr::{BStr, BString}, + bstr::BString, config::tree::{Clone, Core, Init, Key}, remote::{ fetch::{Shallow, SpecIndex}, @@ -500,49 +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"); - for entry in index.entries() { - let entry_path = work_dir.join(gix_path::from_bstr(entry.path(&index))); - assert!(entry_path.is_file(), "{entry_path:?} not found on disk") - } + assure_index_entries_on_disk(&index, repo.work_dir().expect("non-bare")); Ok(()) } #[test] - fn fetch_and_checkout_branch() -> crate::Result { + 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("base").path(), + 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 branch_names = checkout.repo().branch_names(); - let target_branch: &BStr = "a".into(); - let branch_result = checkout.repo().find_reference(target_branch); + let (repo, _) = checkout.main_worktree(gix::progress::Discard, &std::sync::atomic::AtomicBool::default())?; - assert!( - branch_result.is_ok(), - "branch {target_branch} not found: {branch_result:?}. Available branches: {branch_names:?}" + 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 (repo, _) = checkout.worktree( - gix::progress::Discard, - &std::sync::atomic::AtomicBool::default(), - Some("a".into()), - )?; 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_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))); assert!(entry_path.is_file(), "{entry_path:?} not found on disk") } - Ok(()) } #[test] From 9bf01e42b8d8964dfd1e099d645082c10bdabcdf Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 20 Jun 2024 15:50:08 +0200 Subject: [PATCH 3/5] feat: `gix clone` with `--ref` support. `--ref` is similar to `--branch`, but was renamed as it also supports tags for example. --- gitoxide-core/src/repository/clone.rs | 3 +++ src/plumbing/main.rs | 2 ++ src/plumbing/options/mod.rs | 4 ++++ 3 files changed, 9 insertions(+) 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/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, } From 39180b4602745678f9204fe9e11c0facbdd23f40 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 20 Jun 2024 10:38:50 +0200 Subject: [PATCH 4/5] improve documentation of `PrepareCheckout` and make it easier to use Now it's clear why it does what it does with the internal repository, even though admittedly it could also be made so that it can be called multiple times (despite making no sense). --- gix/src/clone/checkout.rs | 10 ++++++++-- gix/src/clone/mod.rs | 6 +++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/gix/src/clone/checkout.rs b/gix/src/clone/checkout.rs index 7d69681a47b..d980e02a873 100644 --- a/gix/src/clone/checkout.rs +++ b/gix/src/clone/checkout.rs @@ -65,6 +65,12 @@ pub mod main_worktree { /// /// 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. + /// + /// # 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, @@ -86,7 +92,7 @@ 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(), })?; @@ -138,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/mod.rs b/gix/src/clone/mod.rs index ea225678437..0dd49c12f5a 100644 --- a/gix/src/clone/mod.rs +++ b/gix/src/clone/mod.rs @@ -140,13 +140,13 @@ impl PrepareFetch { } } -/// 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, From f36b9bd28052131401d048b5aa55c5ae1f9248db Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 20 Jun 2024 19:28:26 +0200 Subject: [PATCH 5/5] thanks clippy --- gix-attributes/tests/search/mod.rs | 7 ++----- gix-odb/tests/odb/find/mod.rs | 2 +- gix-odb/tests/odb/header/mod.rs | 2 +- gix-pack/tests/pack/index.rs | 6 +++--- gix/tests/clone/mod.rs | 2 +- gix/tests/repository/object.rs | 6 +++--- gix/tests/repository/worktree.rs | 2 +- 7 files changed, 12 insertions(+), 15 deletions(-) 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/tests/clone/mod.rs b/gix/tests/clone/mod.rs index 4fc7c089072..3c3c4741d77 100644 --- a/gix/tests/clone/mod.rs +++ b/gix/tests/clone/mod.rs @@ -620,7 +620,7 @@ mod blocking_io { 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") } } 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" ); }