History

Merge branch 'feat/checkout-other-refs'

Sebastian Thiel committed 2 years ago

Commit ecfde072 parents 2f9f0ac f36b9bd18 files · +297 −58Browse files

Unified · Split

18 files
  1. gitoxide-core/src/repository/clone.rs +3 −0
  2. gix-attributes/tests/search/mod.rs +2 −5
  3. gix-odb/tests/odb/find/mod.rs +1 −1
  4. gix-odb/tests/odb/header/mod.rs +1 −1
  5. gix-pack/tests/pack/index.rs +3 −3
  6. gix/src/clone/access.rs +14 −1
  7. gix/src/clone/checkout.rs +20 −5
  8. gix/src/clone/fetch/mod.rs +31 −3
  9. gix/src/clone/fetch/util.rs +75 −22
  10. gix/src/clone/mod.rs +10 −3
  11. gix/src/remote/connection/fetch/negotiate.rs +2 −2
  12. gix/src/remote/connection/fetch/receive_pack.rs +1 −1
  13. gix/src/remote/connection/ref_map.rs +3 −3
  14. gix/tests/clone/mod.rs +121 −4
  15. gix/tests/repository/object.rs +3 −3
  16. gix/tests/repository/worktree.rs +1 −1
  17. src/plumbing/main.rs +2 −0
  18. src/plumbing/options/mod.rs +4 −0
gitoxide-core/src/repository/clone.rs+3−0
@@ -6,6 +6,7 @@ pub struct Options { · 5 unmodified lines
66 pub handshake_info: bool,
77 pub no_tags: bool,
88 pub shallow: gix::remote::fetch::Shallow,
9+ pub ref_name: Option<gix::refs::PartialName>,
910 }
1011
1112 pub const PROGRESS_RANGE: std::ops::RangeInclusive<u8> = 1..=3;
@@ -31,6 +32,7 @@ pub(crate) mod function { · 19 unmodified lines
3132 handshake_info,
3233 bare,
3334 no_tags,
35+ ref_name,
3436 shallow,
3537 }: Options,
3638 ) -> anyhow::Result<()>
@@ -75,6 +77,7 @@ pub(crate) mod function { · 38 unmodified lines
7577 }
7678 let (mut checkout, fetch_outcome) = prepare
7779 .with_shallow(shallow)
80+ .with_ref_name(ref_name.as_ref())?
7881 .fetch_then_checkout(&mut progress, &gix::interrupt::IS_INTERRUPTED)?;
7982
8083 let (repo, outcome) = if bare {
gix-attributes/tests/search/mod.rs+2−5
@@ -307,11 +307,8 @@ mod baseline { · 306 unmodified lines
307307
308308 let mut buf = Vec::new();
309309 let mut collection = MetadataCollection::default();
310− let group = gix_attributes::Search::new_globals(
311− &mut [base.join("user.attributes")].into_iter(),
312− &mut buf,
313− &mut collection,
314− )?;
310+ let group =
311+ gix_attributes::Search::new_globals([base.join("user.attributes")].into_iter(), &mut buf, &mut collection)?;
315312
316313 Ok((group, collection, base, input))
317314 }
gix-odb/tests/odb/find/mod.rs+1−1
@@ -16,7 +16,7 @@ fn can_find(db: impl gix_object::Find, hex_id: &str) { · 15 unmodified lines
1616
1717 #[test]
1818 fn loose_object() {
19− can_find(&db(), "37d4e6c5c48ba0d245164c4e10d5f41140cab980");
19+ can_find(db(), "37d4e6c5c48ba0d245164c4e10d5f41140cab980");
2020 }
2121
2222 #[test]
gix-odb/tests/odb/header/mod.rs+1−1
@@ -8,7 +8,7 @@ fn find_header(db: impl gix_odb::Header, hex_id: &str) -> gix_odb::find::Header · 7 unmodified lines
88
99 #[test]
1010 fn loose_object() {
11− find_header(&db(), "37d4e6c5c48ba0d245164c4e10d5f41140cab980");
11+ find_header(db(), "37d4e6c5c48ba0d245164c4e10d5f41140cab980");
1212 }
1313
1414 #[test]
gix-pack/tests/pack/index.rs+3−3
@@ -363,8 +363,8 @@ fn pack_lookup() -> Result<(), Box<dyn std::error::Error>> { · 362 unmodified lines
363363 },
364364 ),
365365 ] {
366− let idx = index::File::at(&fixture_path(index_path), gix_hash::Kind::Sha1)?;
367− let pack = pack::data::File::at(&fixture_path(pack_path), gix_hash::Kind::Sha1)?;
366+ let idx = index::File::at(fixture_path(index_path), gix_hash::Kind::Sha1)?;
367+ let pack = pack::data::File::at(fixture_path(pack_path), gix_hash::Kind::Sha1)?;
368368
369369 assert_eq!(pack.version(), pack::data::Version::V2);
370370 assert_eq!(pack.num_objects(), idx.num_objects());
@@ -471,7 +471,7 @@ fn iter() -> Result<(), Box<dyn std::error::Error>> { · 100 unmodified lines
471471 "0f3ea84cd1bba10c2a03d736a460635082833e59",
472472 ),
473473 ] {
474− let idx = index::File::at(&fixture_path(path), gix_hash::Kind::Sha1)?;
474+ let idx = index::File::at(fixture_path(path), gix_hash::Kind::Sha1)?;
475475 assert_eq!(idx.version(), *kind);
476476 assert_eq!(idx.num_objects(), *num_objects);
477477 assert_eq!(
gix/src/clone/access.rs+14−1
@@ -11,7 +11,7 @@ impl PrepareFetch { · 10 unmodified lines
1111 ///
1212 /// It can also be used to configure additional options, like those for fetching tags. Note that
1313 /// [`with_fetch_tags()`](crate::Remote::with_fetch_tags()) should be called here to configure the clone as desired.
14− /// Otherwise a clone is configured to be complete and fetches all tags, not only those reachable from all branches.
14+ /// Otherwise, a clone is configured to be complete and fetches all tags, not only those reachable from all branches.
1515 pub fn configure_remote(
1616 mut self,
1717 f: impl FnMut(crate::Remote<'_>) -> Result<crate::Remote<'_>, Box<dyn std::error::Error + Send + Sync>> + 'static,
@@ -42,6 +42,19 @@ impl PrepareFetch { · 24 unmodified lines
4242 self.config_overrides = values.into_iter().map(Into::into).collect();
4343 self
4444 }
45+
46+ /// Set the `name` of the reference to check out, instead of the remote `HEAD`.
47+ /// If `None`, the `HEAD` will be used, which is the default.
48+ ///
49+ /// Note that `name` should be a partial name like `main` or `feat/one`, but can be a full ref name.
50+ /// If a branch on the remote matches, it will automatically be retrieved even without a refspec.
51+ pub fn with_ref_name<'a, Name, E>(mut self, name: Option<Name>) -> Result<Self, E>
52+ where
53+ Name: TryInto<&'a gix_ref::PartialNameRef, Error = E>,
54+ {
55+ self.ref_name = name.map(TryInto::try_into).transpose()?.map(ToOwned::to_owned);
56+ Ok(self)
57+ }
4558 }
4659
4760 /// Consumption
gix/src/clone/checkout.rs+20−5
@@ -28,6 +28,8 @@ pub mod main_worktree { · 27 unmodified lines
2828 CheckoutOptions(#[from] crate::config::checkout_options::Error),
2929 #[error(transparent)]
3030 IndexCheckout(#[from] gix_worktree_state::checkout::Error),
31+ #[error(transparent)]
32+ Peel(#[from] crate::reference::peel::Error),
3133 #[error("Failed to reopen object database as Arc (only if thread-safety wasn't compiled in)")]
3234 OpenArcOdb(#[from] std::io::Error),
3335 #[error("The HEAD reference could not be located")]
@@ -62,7 +64,13 @@ pub mod main_worktree { · 28 unmodified lines
6264 /// on thread per logical core.
6365 ///
6466 /// Note that this is a no-op if the remote was empty, leaving this repository empty as well. This can be validated by checking
65− /// if the `head()` of the returned repository is not unborn.
67+ /// if the `head()` of the returned repository is *not* unborn.
68+ ///
69+ /// # Panics
70+ ///
71+ /// If called after it was successful. The reason here is that it auto-deletes the contained repository,
72+ /// and keeps track of this by means of keeping just one repository instance, which is passed to the user
73+ /// after success.
6674 pub fn main_worktree<P>(
6775 &mut self,
6876 mut progress: P,
@@ -84,19 +92,26 @@ pub mod main_worktree { · 15 unmodified lines
8492 let repo = self
8593 .repo
8694 .as_ref()
87− .expect("still present as we never succeeded the worktree checkout yet");
95+ .expect("BUG: this method may only be called until it is successful");
8896 let workdir = repo.work_dir().ok_or_else(|| Error::BareRepository {
8997 git_dir: repo.git_dir().to_owned(),
9098 })?;
91− let root_tree = match repo.head()?.try_peel_to_id_in_place()? {
99+
100+ let root_tree_id = match &self.ref_name {
101+ Some(reference_val) => Some(repo.find_reference(reference_val)?.peel_to_id_in_place()?),
102+ None => repo.head()?.try_peel_to_id_in_place()?,
103+ };
104+
105+ let root_tree = match root_tree_id {
92106 Some(id) => id.object().expect("downloaded from remote").peel_to_tree()?.id,
93107 None => {
94108 return Ok((
95109 self.repo.take().expect("still present"),
96110 gix_worktree_state::checkout::Outcome::default(),
97− ))
111+ ));
98112 }
99113 };
114+
100115 let index = gix_index::State::from_tree(&root_tree, &repo.objects, repo.config.protect_options()?)
101116 .map_err(|err| Error::IndexFromTree {
102117 id: root_tree,
@@ -129,7 +144,7 @@ pub mod main_worktree { · 26 unmodified lines
129144 bytes.show_throughput(start);
130145
131146 index.write(Default::default())?;
132− Ok((self.repo.take().expect("still present"), outcome))
147+ Ok((self.repo.take().expect("still present").clone(), outcome))
133148 }
134149 }
135150 }
gix/src/clone/fetch/mod.rs+31−3
@@ -1,3 +1,5 @@
1+use crate::bstr::BString;
2+use crate::bstr::ByteSlice;
13 use crate::clone::PrepareFetch;
24
35 /// The error returned by [`PrepareFetch::fetch_only()`].
@@ -35,6 +37,13 @@ pub enum Error { · 31 unmodified lines
3537 },
3638 #[error("Failed to update HEAD with values from remote")]
3739 HeadUpdate(#[from] crate::reference::edit::Error),
40+ #[error("The remote didn't have any ref that matched '{}'", wanted.as_ref().as_bstr())]
41+ RefNameMissing { wanted: gix_ref::PartialName },
42+ #[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::<Vec<_>>().join(", "))]
43+ RefNameAmbiguous {
44+ wanted: gix_ref::PartialName,
45+ candidates: Vec<BString>,
46+ },
3847 }
3948
4049 /// Modification
@@ -117,7 +126,7 @@ impl PrepareFetch { · 76 unmodified lines
117126 remote = remote.with_fetch_tags(fetch_tags);
118127 }
119128
120− // Add HEAD after the remote was written to config, we need it to know what to checkout later, and assure
129+ // Add HEAD after the remote was written to config, we need it to know what to check out later, and assure
121130 // the ref that HEAD points to is present no matter what.
122131 let head_refspec = gix_refspec::parse(
123132 format!("HEAD:refs/remotes/{remote_name}/HEAD").as_str().into(),
@@ -136,10 +145,22 @@ impl PrepareFetch { · 12 unmodified lines
136145 if !opts.extra_refspecs.contains(&head_refspec) {
137146 opts.extra_refspecs.push(head_refspec)
138147 }
148+ if let Some(ref_name) = &self.ref_name {
149+ opts.extra_refspecs.push(
150+ gix_refspec::parse(ref_name.as_ref().as_bstr(), gix_refspec::parse::Operation::Fetch)
151+ .expect("partial names are valid refspecs")
152+ .to_owned(),
153+ );
154+ }
139155 opts
140156 })
141157 .await?
142158 };
159+
160+ // Assure problems with custom branch names fail early, not after getting the pack or during negotiation.
161+ if let Some(ref_name) = &self.ref_name {
162+ util::find_custom_refname(pending_pack.ref_map(), ref_name)?;
163+ }
143164 if pending_pack.ref_map().object_hash != repo.object_hash() {
144165 unimplemented!("configure repository to expect a different object hash as advertised by the server")
145166 }
@@ -160,9 +181,10 @@ impl PrepareFetch { · 14 unmodified lines
160181 util::append_config_to_repo_config(repo, config);
161182 util::update_head(
162183 repo,
163− &outcome.ref_map.remote_refs,
184+ &outcome.ref_map,
164185 reflog_message.as_ref(),
165186 remote_name.as_ref(),
187+ self.ref_name.as_ref(),
166188 )?;
167189
168190 Ok((self.repo.take().expect("still present"), outcome))
@@ -180,7 +202,13 @@ impl PrepareFetch { · 11 unmodified lines
180202 P::SubProgress: 'static,
181203 {
182204 let (repo, fetch_outcome) = self.fetch_only(progress, should_interrupt)?;
183− Ok((crate::clone::PrepareCheckout { repo: repo.into() }, fetch_outcome))
205+ Ok((
206+ crate::clone::PrepareCheckout {
207+ repo: repo.into(),
208+ ref_name: self.ref_name.clone(),
209+ },
210+ fetch_outcome,
211+ ))
184212 }
185213 }
186214
gix/src/clone/fetch/util.rs+75−22
@@ -2,7 +2,7 @@ use std::{borrow::Cow, io::Write}; · 1 unmodified line
22
33 use gix_ref::{
44 transaction::{LogChange, RefLog},
5− FullNameRef,
5+ FullNameRef, PartialName,
66 };
77
88 use super::Error;
@@ -60,35 +60,40 @@ pub fn append_config_to_repo_config(repo: &mut Repository, config: gix_config::F · 51 unmodified lines
6060
6161 /// HEAD cannot be written by means of refspec by design, so we have to do it manually here. Also create the pointed-to ref
6262 /// if we have to, as it might not have been naturally included in the ref-specs.
63+/// Lastly, use `ref_name` if it was provided instead, and let `HEAD` point to it.
6364 pub fn update_head(
6465 repo: &mut Repository,
65− remote_refs: &[gix_protocol::handshake::Ref],
66+ ref_map: &crate::remote::fetch::RefMap,
6667 reflog_message: &BStr,
6768 remote_name: &BStr,
69+ ref_name: Option<&PartialName>,
6870 ) -> Result<(), Error> {
6971 use gix_ref::{
7072 transaction::{PreviousValue, RefEdit},
7173 Target,
7274 };
73− let (head_peeled_id, head_ref) = match remote_refs.iter().find_map(|r| {
74− Some(match r {
75− gix_protocol::handshake::Ref::Symbolic {
76− full_ref_name,
77− target,
78− tag: _,
79− object,
80− } if full_ref_name == "HEAD" => (Some(object.as_ref()), Some(target)),
81− gix_protocol::handshake::Ref::Direct { full_ref_name, object } if full_ref_name == "HEAD" => {
82− (Some(object.as_ref()), None)
83− }
84− gix_protocol::handshake::Ref::Unborn { full_ref_name, target } if full_ref_name == "HEAD" => {
85− (None, Some(target))
86− }
87− _ => return None,
88− })
89− }) {
90− Some(t) => t,
91− None => return Ok(()),
75+ let head_info = match ref_name {
76+ Some(ref_name) => Some(find_custom_refname(ref_map, ref_name)?),
77+ None => ref_map.remote_refs.iter().find_map(|r| {
78+ Some(match r {
79+ gix_protocol::handshake::Ref::Symbolic {
80+ full_ref_name,
81+ target,
82+ tag: _,
83+ object,
84+ } if full_ref_name == "HEAD" => (Some(object.as_ref()), Some(target.as_bstr())),
85+ gix_protocol::handshake::Ref::Direct { full_ref_name, object } if full_ref_name == "HEAD" => {
86+ (Some(object.as_ref()), None)
87+ }
88+ gix_protocol::handshake::Ref::Unborn { full_ref_name, target } if full_ref_name == "HEAD" => {
89+ (None, Some(target.as_bstr()))
90+ }
91+ _ => return None,
92+ })
93+ }),
94+ };
95+ let Some((head_peeled_id, head_ref)) = head_info else {
96+ return Ok(());
9297 };
9398
9499 let head: gix_ref::FullName = "HEAD".try_into().expect("valid");
@@ -178,7 +183,55 @@ pub fn update_head( · 83 unmodified lines
178183 Ok(())
179184 }
180185
181−/// Setup the remote configuration for `branch` so that it points to itself, but on the remote, if and only if currently
186+pub(super) fn find_custom_refname<'a>(
187+ ref_map: &'a crate::remote::fetch::RefMap,
188+ ref_name: &PartialName,
189+) -> Result<(Option<&'a gix_hash::oid>, Option<&'a BStr>), Error> {
190+ let group = gix_refspec::MatchGroup::from_fetch_specs(Some(
191+ gix_refspec::parse(ref_name.as_ref().as_bstr(), gix_refspec::parse::Operation::Fetch)
192+ .expect("partial names are valid refs"),
193+ ));
194+ // TODO: to fix ambiguity, implement priority system
195+ let filtered_items: Vec<_> = ref_map
196+ .mappings
197+ .iter()
198+ .filter_map(|m| {
199+ m.remote
200+ .as_name()
201+ .and_then(|name| m.remote.as_id().map(|id| (name, id)))
202+ })
203+ .map(|(full_ref_name, target)| gix_refspec::match_group::Item {
204+ full_ref_name,
205+ target,
206+ object: None,
207+ })
208+ .collect();
209+ let res = group.match_remotes(filtered_items.iter().copied());
210+ match res.mappings.len() {
211+ 0 => Err(Error::RefNameMissing {
212+ wanted: ref_name.clone(),
213+ }),
214+ 1 => {
215+ let item = filtered_items[res.mappings[0]
216+ .item_index
217+ .expect("we map by name only and have no object-id in refspec")];
218+ Ok((Some(item.target), Some(item.full_ref_name)))
219+ }
220+ _ => Err(Error::RefNameAmbiguous {
221+ wanted: ref_name.clone(),
222+ candidates: res
223+ .mappings
224+ .iter()
225+ .filter_map(|m| match m.lhs {
226+ gix_refspec::match_group::SourceRef::FullName(name) => Some(name.to_owned()),
227+ gix_refspec::match_group::SourceRef::ObjectId(_) => None,
228+ })
229+ .collect(),
230+ }),
231+ }
232+}
233+
234+/// Set up the remote configuration for `branch` so that it points to itself, but on the remote, if and only if currently
182235 /// saved refspecs are able to match it.
183236 /// For that we reload the remote of `remote_name` and use its `ref_specs` for match.
184237 fn setup_branch_config(
gix/src/clone/mod.rs+10−3
@@ -34,6 +34,9 @@ pub struct PrepareFetch { · 33 unmodified lines
3434 /// How to handle shallow clones
3535 #[cfg_attr(not(feature = "blocking-network-client"), allow(dead_code))]
3636 shallow: remote::fetch::Shallow,
37+ /// The name of the reference to fetch. If `None`, the reference pointed to by `HEAD` will be checked out.
38+ #[cfg_attr(not(feature = "blocking-network-client"), allow(dead_code))]
39+ ref_name: Option<gix_ref::PartialName>,
3740 }
3841
3942 /// The error returned by [`PrepareFetch::new()`].
@@ -132,17 +135,21 @@ impl PrepareFetch { · 92 unmodified lines
132135 #[cfg(any(feature = "async-network-client", feature = "blocking-network-client"))]
133136 configure_connection: None,
134137 shallow: remote::fetch::Shallow::NoChange,
138+ ref_name: None,
135139 })
136140 }
137141 }
138142
139−/// A utility to collect configuration on how to perform a checkout into a working tree, and when dropped without checking out successfully
140−/// the fetched repository will be dropped.
143+/// A utility to collect configuration on how to perform a checkout into a working tree,
144+/// and when dropped without checking out successfully the fetched repository will be deleted from disk.
141145 #[must_use]
142146 #[cfg(feature = "worktree-mutation")]
147+#[derive(Debug)]
143148 pub struct PrepareCheckout {
144− /// A freshly initialized repository which is owned by us, or `None` if it was handed to the user
149+ /// A freshly initialized repository which is owned by us, or `None` if it was successfully checked out.
145150 pub(self) repo: Option<crate::Repository>,
151+ /// The name of the reference to check out. If `None`, the reference pointed to by `HEAD` will be checked out.
152+ pub(self) ref_name: Option<gix_ref::PartialName>,
146153 }
147154
148155 // This module encapsulates functionality that works with both feature toggles. Can be combined with `fetch`
gix/src/remote/connection/fetch/negotiate.rs+2−2
@@ -38,7 +38,7 @@ pub(crate) enum Action { · 37 unmodified lines
3838 SkipToRefUpdate,
3939 /// We can't know for sure if fetching *is not* needed, so we go ahead and negotiate.
4040 MustNegotiate {
41− /// Each `ref_map.mapping` has a slot here which is `true` if we have the object the remote ref points to locally.
41+ /// Each `ref_map.mapping` has a slot here which is `true` if we have the object the remote ref points to, locally.
4242 remote_ref_target_known: Vec<bool>,
4343 },
4444 }
@@ -221,7 +221,7 @@ pub(crate) fn add_wants( · 176 unmodified lines
221221 shallow: &fetch::Shallow,
222222 mapping_is_ignored: impl Fn(&fetch::Mapping) -> bool,
223223 ) {
224− // When using shallow, we can't exclude `wants` as the remote won't send anything then. Thus we have to resend everything
224+ // When using shallow, we can't exclude `wants` as the remote won't send anything then. Thus, we have to resend everything
225225 // we have as want instead to get exactly the same graph, but possibly deepened.
226226 let is_shallow = !matches!(shallow, fetch::Shallow::NoChange);
227227 let wants = ref_map
gix/src/remote/connection/fetch/receive_pack.rs+1−1
@@ -114,7 +114,7 @@ where · 113 unmodified lines
114114 gix_protocol::fetch::Response::check_required_features(protocol_version, &fetch_features)?;
115115 let sideband_all = fetch_features.iter().any(|(n, _)| *n == "sideband-all");
116116 let mut arguments = gix_protocol::fetch::Arguments::new(protocol_version, fetch_features, con.trace);
117− if matches!(con.remote.fetch_tags, crate::remote::fetch::Tags::Included) {
117+ if matches!(con.remote.fetch_tags, fetch::Tags::Included) {
118118 if !arguments.can_use_include_tag() {
119119 return Err(Error::MissingServerFeature {
120120 feature: "include-tag",
gix/src/remote/connection/ref_map.rs+3−3
@@ -79,14 +79,14 @@ where · 78 unmodified lines
7979 /// for _fetching_.
8080 ///
8181 /// This comes in the form of all matching tips on the remote and the object they point to, along with
82− /// with the local tracking branch of these tips (if available).
82+ /// the local tracking branch of these tips (if available).
8383 ///
8484 /// Note that this doesn't fetch the objects mentioned in the tips nor does it make any change to underlying repository.
8585 ///
8686 /// # Consumption
8787 ///
88− /// Due to management of the transport, it's cleanest to only use it for a single interaction. Thus it's consumed along with
89− /// the connection.
88+ /// Due to management of the transport, it's cleanest to only use it for a single interaction. Thus, it's consumed
89+ /// along with the connection.
9090 ///
9191 /// ### Configuration
9292 ///
gix/tests/clone/mod.rs+121−4
@@ -1,7 +1,8 @@
11 use crate::{remote, util::restricted};
22
3−#[cfg(feature = "blocking-network-client")]
3+#[cfg(all(feature = "worktree-mutation", feature = "blocking-network-client"))]
44 mod blocking_io {
5+ use std::path::Path;
56 use std::{borrow::Cow, sync::atomic::AtomicBool};
67
78 use gix::{
@@ -500,12 +501,128 @@ mod blocking_io { · 492 unmodified lines
500501 let index = repo.index()?;
501502 assert_eq!(index.entries().len(), 1, "All entries are known as per HEAD tree");
502503
503− let work_dir = repo.work_dir().expect("non-bare");
504+ assure_index_entries_on_disk(&index, repo.work_dir().expect("non-bare"));
505+ Ok(())
506+ }
507+ #[test]
508+ fn fetch_and_checkout_specific_ref() -> crate::Result {
509+ let tmp = gix_testtools::tempfile::TempDir::new()?;
510+ let remote_repo = remote::repo("base");
511+ let ref_to_checkout = "a";
512+ let mut prepare = gix::clone::PrepareFetch::new(
513+ remote_repo.path(),
514+ tmp.path(),
515+ gix::create::Kind::WithWorktree,
516+ Default::default(),
517+ restricted(),
518+ )?
519+ .with_ref_name(Some(ref_to_checkout))?;
520+ let (mut checkout, _out) =
521+ prepare.fetch_then_checkout(gix::progress::Discard, &std::sync::atomic::AtomicBool::default())?;
522+
523+ let (repo, _) = checkout.main_worktree(gix::progress::Discard, &std::sync::atomic::AtomicBool::default())?;
524+
525+ assert_eq!(
526+ repo.references()?.all()?.count() - 2,
527+ remote_repo.references()?.all()?.count(),
528+ "all references have been cloned, + remote HEAD + remote main (not listed in remote_repo)"
529+ );
530+ let checked_out_ref = repo.head_ref()?.expect("head points to ref");
531+ let remote_ref_name = format!("refs/heads/{ref_to_checkout}");
532+ assert_eq!(
533+ checked_out_ref.name().as_bstr(),
534+ remote_ref_name,
535+ "it's possible to checkout anything with that name, but here we have an ordinary branch"
536+ );
537+
538+ assert_eq!(
539+ checked_out_ref
540+ .remote_ref_name(gix::remote::Direction::Fetch)
541+ .transpose()?
542+ .unwrap()
543+ .as_bstr(),
544+ remote_ref_name,
545+ "the merge configuration is using the given name"
546+ );
547+
548+ let index = repo.index()?;
549+ assert_eq!(index.entries().len(), 1, "All entries are known as per HEAD tree");
550+
551+ assure_index_entries_on_disk(&index, repo.work_dir().expect("non-bare"));
552+ Ok(())
553+ }
554+
555+ #[test]
556+ fn fetch_and_checkout_specific_non_existing() -> crate::Result {
557+ let tmp = gix_testtools::tempfile::TempDir::new()?;
558+ let remote_repo = remote::repo("base");
559+ let ref_to_checkout = "does-not-exist";
560+ let mut prepare = gix::clone::PrepareFetch::new(
561+ remote_repo.path(),
562+ tmp.path(),
563+ gix::create::Kind::WithWorktree,
564+ Default::default(),
565+ restricted(),
566+ )?
567+ .with_ref_name(Some(ref_to_checkout))?;
568+
569+ let err = prepare
570+ .fetch_then_checkout(gix::progress::Discard, &std::sync::atomic::AtomicBool::default())
571+ .unwrap_err();
572+ assert_eq!(
573+ err.to_string(),
574+ "The remote didn't have any ref that matched 'does-not-exist'",
575+ "we don't test this, but it's important that it determines this before receiving a pack"
576+ );
577+ Ok(())
578+ }
579+
580+ #[test]
581+ fn fetch_and_checkout_specific_annotated_tag() -> crate::Result {
582+ let tmp = gix_testtools::tempfile::TempDir::new()?;
583+ let remote_repo = remote::repo("base");
584+ let ref_to_checkout = "annotated-detached-tag";
585+ let mut prepare = gix::clone::PrepareFetch::new(
586+ remote_repo.path(),
587+ tmp.path(),
588+ gix::create::Kind::WithWorktree,
589+ Default::default(),
590+ restricted(),
591+ )?
592+ .with_ref_name(Some(ref_to_checkout))?;
593+ let (mut checkout, _out) =
594+ prepare.fetch_then_checkout(gix::progress::Discard, &std::sync::atomic::AtomicBool::default())?;
595+
596+ let (repo, _) = checkout.main_worktree(gix::progress::Discard, &std::sync::atomic::AtomicBool::default())?;
597+
598+ assert_eq!(
599+ repo.references()?.all()?.count() - 1,
600+ remote_repo.references()?.all()?.count(),
601+ "all references have been cloned, + remote HEAD (not listed in remote_repo)"
602+ );
603+ let checked_out_ref = repo.head_ref()?.expect("head points to ref");
604+ let remote_ref_name = format!("refs/tags/{ref_to_checkout}");
605+ assert_eq!(
606+ checked_out_ref.name().as_bstr(),
607+ remote_ref_name,
608+ "it also works with tags"
609+ );
610+
611+ assert_eq!(
612+ checked_out_ref
613+ .remote_ref_name(gix::remote::Direction::Fetch)
614+ .transpose()?,
615+ None,
616+ "there is no merge configuration for tags"
617+ );
618+ Ok(())
619+ }
620+
621+ fn assure_index_entries_on_disk(index: &gix::worktree::Index, work_dir: &Path) {
504622 for entry in index.entries() {
505− let entry_path = work_dir.join(gix_path::from_bstr(entry.path(&index)));
623+ let entry_path = work_dir.join(gix_path::from_bstr(entry.path(index)));
506624 assert!(entry_path.is_file(), "{entry_path:?} not found on disk")
507625 }
508− Ok(())
509626 }
510627
511628 #[test]
gix/tests/repository/object.rs+3−3
@@ -6,7 +6,7 @@ mod write_object { · 5 unmodified lines
66 #[test]
77 fn empty_tree() -> crate::Result {
88 let (_tmp, repo) = empty_bare_repo()?;
9− let oid = repo.write_object(&gix::objs::TreeRef::empty())?;
9+ let oid = repo.write_object(gix::objs::TreeRef::empty())?;
1010 assert_eq!(
1111 oid,
1212 gix::hash::ObjectId::empty_tree(repo.object_hash()),
@@ -280,7 +280,7 @@ mod commit { · 267 unmodified lines
280280 crate::restricted(),
281281 )?
282282 .to_thread_local();
283− let empty_tree_id = repo.write_object(&gix::objs::Tree::empty())?.detach();
283+ let empty_tree_id = repo.write_object(gix::objs::Tree::empty())?.detach();
284284 let err = repo
285285 .commit("HEAD", "initial", empty_tree_id, [empty_tree_id])
286286 .unwrap_err();
@@ -304,7 +304,7 @@ mod commit { · 17 unmodified lines
304304 restricted_and_git(),
305305 )?
306306 .to_thread_local();
307− let empty_tree_id = repo.write_object(&gix::objs::Tree::empty())?;
307+ let empty_tree_id = repo.write_object(gix::objs::Tree::empty())?;
308308 let commit_id = repo.commit("HEAD", "initial", empty_tree_id, gix::commit::NO_PARENT_IDS)?;
309309 assert_eq!(
310310 commit_id,
gix/tests/repository/worktree.rs+1−1
@@ -49,7 +49,7 @@ mod with_core_worktree_config { · 48 unmodified lines
4949 } else {
5050 assert_eq!(
5151 repo.work_dir().unwrap(),
52− gix_path::realpath(&repo.git_dir().parent().unwrap().parent().unwrap().join("worktree"))?,
52+ gix_path::realpath(repo.git_dir().parent().unwrap().parent().unwrap().join("worktree"))?,
5353 "absolute workdirs are left untouched"
5454 );
5555 }
src/plumbing/main.rs+2−0
@@ -404,6 +404,7 @@ pub fn main() -> Result<()> { · 403 unmodified lines
404404 handshake_info,
405405 bare,
406406 no_tags,
407+ ref_name,
407408 remote,
408409 shallow,
409410 directory,
@@ -413,6 +414,7 @@ pub fn main() -> Result<()> { · 3 unmodified lines
413414 bare,
414415 handshake_info,
415416 no_tags,
417+ ref_name,
416418 shallow: shallow.into(),
417419 };
418420 prepare_and_run(
src/plumbing/options/mod.rs+4−0
@@ -434,6 +434,10 @@ pub mod clone { · 433 unmodified lines
434434 /// The url of the remote to connect to, like `https://github.com/byron/gitoxide`.
435435 pub remote: OsString,
436436
437+ /// The name of the reference to check out.
438+ #[clap(long = "ref", value_parser = crate::shared::AsPartialRefName, value_name = "REF_NAME")]
439+ pub ref_name: Option<gix::refs::PartialName>,
440+
437441 /// The directory to initialize with the new repository and to which all data should be written.
438442 pub directory: Option<PathBuf>,
439443 }