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
6 pub handshake_info: bool,6 pub handshake_info: bool,
7 pub no_tags: bool,7 pub no_tags: bool,
8 pub shallow: gix::remote::fetch::Shallow,8 pub shallow: gix::remote::fetch::Shallow,
9 pub ref_name: Option<gix::refs::PartialName>,
9}10}
10 11
11pub const PROGRESS_RANGE: std::ops::RangeInclusive<u8> = 1..=3;12pub const PROGRESS_RANGE: std::ops::RangeInclusive<u8> = 1..=3;
@@ -31,6 +32,7 @@ pub(crate) mod function { · 19 unmodified lines
31 handshake_info,32 handshake_info,
32 bare,33 bare,
33 no_tags,34 no_tags,
35 ref_name,
34 shallow,36 shallow,
35 }: Options,37 }: Options,
36 ) -> anyhow::Result<()>38 ) -> anyhow::Result<()>
@@ -75,6 +77,7 @@ pub(crate) mod function { · 38 unmodified lines
75 }77 }
76 let (mut checkout, fetch_outcome) = prepare78 let (mut checkout, fetch_outcome) = prepare
77 .with_shallow(shallow)79 .with_shallow(shallow)
80 .with_ref_name(ref_name.as_ref())?
78 .fetch_then_checkout(&mut progress, &gix::interrupt::IS_INTERRUPTED)?;81 .fetch_then_checkout(&mut progress, &gix::interrupt::IS_INTERRUPTED)?;
79 82
80 let (repo, outcome) = if bare {83 let (repo, outcome) = if bare {
gix-attributes/tests/search/mod.rs+2−5
@@ -307,11 +307,8 @@ mod baseline { · 306 unmodified lines
307 307
308 let mut buf = Vec::new();308 let mut buf = Vec::new();
309 let mut collection = MetadataCollection::default();309 let mut collection = MetadataCollection::default();
310 let group = gix_attributes::Search::new_globals(310 let group =
311 &mut [base.join("user.attributes")].into_iter(),311 gix_attributes::Search::new_globals([base.join("user.attributes")].into_iter(), &mut buf, &mut collection)?;
312 &mut buf,
313 &mut collection,
314 )?;
315 312
316 Ok((group, collection, base, input))313 Ok((group, collection, base, input))
317 }314 }
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
16 16
17#[test]17#[test]
18fn loose_object() {18fn loose_object() {
19 can_find(&db(), "37d4e6c5c48ba0d245164c4e10d5f41140cab980");19 can_find(db(), "37d4e6c5c48ba0d245164c4e10d5f41140cab980");
20}20}
21 21
22#[test]22#[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
8 8
9#[test]9#[test]
10fn loose_object() {10fn loose_object() {
11 find_header(&db(), "37d4e6c5c48ba0d245164c4e10d5f41140cab980");11 find_header(db(), "37d4e6c5c48ba0d245164c4e10d5f41140cab980");
12}12}
13 13
14#[test]14#[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
363 },363 },
364 ),364 ),
365 ] {365 ] {
366 let idx = index::File::at(&fixture_path(index_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)?;367 let pack = pack::data::File::at(fixture_path(pack_path), gix_hash::Kind::Sha1)?;
368 368
369 assert_eq!(pack.version(), pack::data::Version::V2);369 assert_eq!(pack.version(), pack::data::Version::V2);
370 assert_eq!(pack.num_objects(), idx.num_objects());370 assert_eq!(pack.num_objects(), idx.num_objects());
@@ -471,7 +471,7 @@ fn iter() -> Result<(), Box<dyn std::error::Error>> { · 100 unmodified lines
471 "0f3ea84cd1bba10c2a03d736a460635082833e59",471 "0f3ea84cd1bba10c2a03d736a460635082833e59",
472 ),472 ),
473 ] {473 ] {
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)?;
475 assert_eq!(idx.version(), *kind);475 assert_eq!(idx.version(), *kind);
476 assert_eq!(idx.num_objects(), *num_objects);476 assert_eq!(idx.num_objects(), *num_objects);
477 assert_eq!(477 assert_eq!(
gix/src/clone/access.rs+14−1
@@ -11,7 +11,7 @@ impl PrepareFetch { · 10 unmodified lines
11 ///11 ///
12 /// It can also be used to configure additional options, like those for fetching tags. Note that12 /// It can also be used to configure additional options, like those for fetching tags. Note that
13 /// [`with_fetch_tags()`](crate::Remote::with_fetch_tags()) should be called here to configure the clone as desired.13 /// [`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.
15 pub fn configure_remote(15 pub fn configure_remote(
16 mut self,16 mut self,
17 f: impl FnMut(crate::Remote<'_>) -> Result<crate::Remote<'_>, Box<dyn std::error::Error + Send + Sync>> + 'static,17 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
42 self.config_overrides = values.into_iter().map(Into::into).collect();42 self.config_overrides = values.into_iter().map(Into::into).collect();
43 self43 self
44 }44 }
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 }
45}58}
46 59
47/// Consumption60/// Consumption
gix/src/clone/checkout.rs+20−5
@@ -28,6 +28,8 @@ pub mod main_worktree { · 27 unmodified lines
28 CheckoutOptions(#[from] crate::config::checkout_options::Error),28 CheckoutOptions(#[from] crate::config::checkout_options::Error),
29 #[error(transparent)]29 #[error(transparent)]
30 IndexCheckout(#[from] gix_worktree_state::checkout::Error),30 IndexCheckout(#[from] gix_worktree_state::checkout::Error),
31 #[error(transparent)]
32 Peel(#[from] crate::reference::peel::Error),
31 #[error("Failed to reopen object database as Arc (only if thread-safety wasn't compiled in)")]33 #[error("Failed to reopen object database as Arc (only if thread-safety wasn't compiled in)")]
32 OpenArcOdb(#[from] std::io::Error),34 OpenArcOdb(#[from] std::io::Error),
33 #[error("The HEAD reference could not be located")]35 #[error("The HEAD reference could not be located")]
@@ -62,7 +64,13 @@ pub mod main_worktree { · 28 unmodified lines
62 /// on thread per logical core.64 /// on thread per logical core.
63 ///65 ///
64 /// Note that this is a no-op if the remote was empty, leaving this repository empty as well. This can be validated by checking66 /// 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.
66 pub fn main_worktree<P>(74 pub fn main_worktree<P>(
67 &mut self,75 &mut self,
68 mut progress: P,76 mut progress: P,
@@ -84,19 +92,26 @@ pub mod main_worktree { · 15 unmodified lines
84 let repo = self92 let repo = self
85 .repo93 .repo
86 .as_ref()94 .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");
88 let workdir = repo.work_dir().ok_or_else(|| Error::BareRepository {96 let workdir = repo.work_dir().ok_or_else(|| Error::BareRepository {
89 git_dir: repo.git_dir().to_owned(),97 git_dir: repo.git_dir().to_owned(),
90 })?;98 })?;
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 {
92 Some(id) => id.object().expect("downloaded from remote").peel_to_tree()?.id,106 Some(id) => id.object().expect("downloaded from remote").peel_to_tree()?.id,
93 None => {107 None => {
94 return Ok((108 return Ok((
95 self.repo.take().expect("still present"),109 self.repo.take().expect("still present"),
96 gix_worktree_state::checkout::Outcome::default(),110 gix_worktree_state::checkout::Outcome::default(),
97 ))111 ));
98 }112 }
99 };113 };
114
100 let index = gix_index::State::from_tree(&root_tree, &repo.objects, repo.config.protect_options()?)115 let index = gix_index::State::from_tree(&root_tree, &repo.objects, repo.config.protect_options()?)
101 .map_err(|err| Error::IndexFromTree {116 .map_err(|err| Error::IndexFromTree {
102 id: root_tree,117 id: root_tree,
@@ -129,7 +144,7 @@ pub mod main_worktree { · 26 unmodified lines
129 bytes.show_throughput(start);144 bytes.show_throughput(start);
130 145
131 index.write(Default::default())?;146 index.write(Default::default())?;
132 Ok((self.repo.take().expect("still present"), outcome))147 Ok((self.repo.take().expect("still present").clone(), outcome))
133 }148 }
134 }149 }
135}150}
gix/src/clone/fetch/mod.rs+31−3
@@ -1,3 +1,5 @@
1use crate::bstr::BString;
2use crate::bstr::ByteSlice;
1use crate::clone::PrepareFetch;3use crate::clone::PrepareFetch;
2 4
3/// The error returned by [`PrepareFetch::fetch_only()`].5/// The error returned by [`PrepareFetch::fetch_only()`].
@@ -35,6 +37,13 @@ pub enum Error { · 31 unmodified lines
35 },37 },
36 #[error("Failed to update HEAD with values from remote")]38 #[error("Failed to update HEAD with values from remote")]
37 HeadUpdate(#[from] crate::reference::edit::Error),39 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 },
38}47}
39 48
40/// Modification49/// Modification
@@ -117,7 +126,7 @@ impl PrepareFetch { · 76 unmodified lines
117 remote = remote.with_fetch_tags(fetch_tags);126 remote = remote.with_fetch_tags(fetch_tags);
118 }127 }
119 128
120 // Add HEAD after the remote was written to config, we need it to know what to checkout later, and assure129 // Add HEAD after the remote was written to config, we need it to know what to check out later, and assure
121 // the ref that HEAD points to is present no matter what.130 // the ref that HEAD points to is present no matter what.
122 let head_refspec = gix_refspec::parse(131 let head_refspec = gix_refspec::parse(
123 format!("HEAD:refs/remotes/{remote_name}/HEAD").as_str().into(),132 format!("HEAD:refs/remotes/{remote_name}/HEAD").as_str().into(),
@@ -136,10 +145,22 @@ impl PrepareFetch { · 12 unmodified lines
136 if !opts.extra_refspecs.contains(&head_refspec) {145 if !opts.extra_refspecs.contains(&head_refspec) {
137 opts.extra_refspecs.push(head_refspec)146 opts.extra_refspecs.push(head_refspec)
138 }147 }
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 }
139 opts155 opts
140 })156 })
141 .await?157 .await?
142 };158 };
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 }
143 if pending_pack.ref_map().object_hash != repo.object_hash() {164 if pending_pack.ref_map().object_hash != repo.object_hash() {
144 unimplemented!("configure repository to expect a different object hash as advertised by the server")165 unimplemented!("configure repository to expect a different object hash as advertised by the server")
145 }166 }
@@ -160,9 +181,10 @@ impl PrepareFetch { · 14 unmodified lines
160 util::append_config_to_repo_config(repo, config);181 util::append_config_to_repo_config(repo, config);
161 util::update_head(182 util::update_head(
162 repo,183 repo,
163 &outcome.ref_map.remote_refs,184 &outcome.ref_map,
164 reflog_message.as_ref(),185 reflog_message.as_ref(),
165 remote_name.as_ref(),186 remote_name.as_ref(),
187 self.ref_name.as_ref(),
166 )?;188 )?;
167 189
168 Ok((self.repo.take().expect("still present"), outcome))190 Ok((self.repo.take().expect("still present"), outcome))
@@ -180,7 +202,13 @@ impl PrepareFetch { · 11 unmodified lines
180 P::SubProgress: 'static,202 P::SubProgress: 'static,
181 {203 {
182 let (repo, fetch_outcome) = self.fetch_only(progress, should_interrupt)?;204 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 ))
184 }212 }
185}213}
186 214
gix/src/clone/fetch/util.rs+75−22
@@ -2,7 +2,7 @@ use std::{borrow::Cow, io::Write}; · 1 unmodified line
2 2
3use gix_ref::{3use gix_ref::{
4 transaction::{LogChange, RefLog},4 transaction::{LogChange, RefLog},
5 FullNameRef,5 FullNameRef, PartialName,
6};6};
7 7
8use super::Error;8use super::Error;
@@ -60,35 +60,40 @@ pub fn append_config_to_repo_config(repo: &mut Repository, config: gix_config::F · 51 unmodified lines
60 60
61/// HEAD cannot be written by means of refspec by design, so we have to do it manually here. Also create the pointed-to ref61/// HEAD cannot be written by means of refspec by design, so we have to do it manually here. Also create the pointed-to ref
62/// if we have to, as it might not have been naturally included in the ref-specs.62/// 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.
63pub fn update_head(64pub fn update_head(
64 repo: &mut Repository,65 repo: &mut Repository,
65 remote_refs: &[gix_protocol::handshake::Ref],66 ref_map: &crate::remote::fetch::RefMap,
66 reflog_message: &BStr,67 reflog_message: &BStr,
67 remote_name: &BStr,68 remote_name: &BStr,
69 ref_name: Option<&PartialName>,
68) -> Result<(), Error> {70) -> Result<(), Error> {
69 use gix_ref::{71 use gix_ref::{
70 transaction::{PreviousValue, RefEdit},72 transaction::{PreviousValue, RefEdit},
71 Target,73 Target,
72 };74 };
73 let (head_peeled_id, head_ref) = match remote_refs.iter().find_map(|r| {75 let head_info = match ref_name {
74 Some(match r {76 Some(ref_name) => Some(find_custom_refname(ref_map, ref_name)?),
75 gix_protocol::handshake::Ref::Symbolic {77 None => ref_map.remote_refs.iter().find_map(|r| {
76 full_ref_name,78 Some(match r {
77 target,79 gix_protocol::handshake::Ref::Symbolic {
78 tag: _,80 full_ref_name,
79 object,81 target,
80 } if full_ref_name == "HEAD" => (Some(object.as_ref()), Some(target)),82 tag: _,
81 gix_protocol::handshake::Ref::Direct { full_ref_name, object } if full_ref_name == "HEAD" => {83 object,
82 (Some(object.as_ref()), None)84 } if full_ref_name == "HEAD" => (Some(object.as_ref()), Some(target.as_bstr())),
83 }85 gix_protocol::handshake::Ref::Direct { full_ref_name, object } if full_ref_name == "HEAD" => {
84 gix_protocol::handshake::Ref::Unborn { full_ref_name, target } if full_ref_name == "HEAD" => {86 (Some(object.as_ref()), None)
85 (None, Some(target))87 }
86 }88 gix_protocol::handshake::Ref::Unborn { full_ref_name, target } if full_ref_name == "HEAD" => {
87 _ => return None,89 (None, Some(target.as_bstr()))
88 })90 }
89 }) {91 _ => return None,
90 Some(t) => t,92 })
91 None => return Ok(()),93 }),
94 };
95 let Some((head_peeled_id, head_ref)) = head_info else {
96 return Ok(());
92 };97 };
93 98
94 let head: gix_ref::FullName = "HEAD".try_into().expect("valid");99 let head: gix_ref::FullName = "HEAD".try_into().expect("valid");
@@ -178,7 +183,55 @@ pub fn update_head( · 83 unmodified lines
178 Ok(())183 Ok(())
179}184}
180 185
181/// Setup the remote configuration for `branch` so that it points to itself, but on the remote, if and only if currently186pub(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
182/// saved refspecs are able to match it.235/// saved refspecs are able to match it.
183/// For that we reload the remote of `remote_name` and use its `ref_specs` for match.236/// For that we reload the remote of `remote_name` and use its `ref_specs` for match.
184fn setup_branch_config(237fn setup_branch_config(
gix/src/clone/mod.rs+10−3
@@ -34,6 +34,9 @@ pub struct PrepareFetch { · 33 unmodified lines
34 /// How to handle shallow clones34 /// How to handle shallow clones
35 #[cfg_attr(not(feature = "blocking-network-client"), allow(dead_code))]35 #[cfg_attr(not(feature = "blocking-network-client"), allow(dead_code))]
36 shallow: remote::fetch::Shallow,36 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>,
37}40}
38 41
39/// The error returned by [`PrepareFetch::new()`].42/// The error returned by [`PrepareFetch::new()`].
@@ -132,17 +135,21 @@ impl PrepareFetch { · 92 unmodified lines
132 #[cfg(any(feature = "async-network-client", feature = "blocking-network-client"))]135 #[cfg(any(feature = "async-network-client", feature = "blocking-network-client"))]
133 configure_connection: None,136 configure_connection: None,
134 shallow: remote::fetch::Shallow::NoChange,137 shallow: remote::fetch::Shallow::NoChange,
138 ref_name: None,
135 })139 })
136 }140 }
137}141}
138 142
139/// A utility to collect configuration on how to perform a checkout into a working tree, and when dropped without checking out successfully143/// A utility to collect configuration on how to perform a checkout into a working tree,
140/// the fetched repository will be dropped.144/// and when dropped without checking out successfully the fetched repository will be deleted from disk.
141#[must_use]145#[must_use]
142#[cfg(feature = "worktree-mutation")]146#[cfg(feature = "worktree-mutation")]
147#[derive(Debug)]
143pub struct PrepareCheckout {148pub struct PrepareCheckout {
144 /// A freshly initialized repository which is owned by us, or `None` if it was handed to the user149 /// A freshly initialized repository which is owned by us, or `None` if it was successfully checked out.
145 pub(self) repo: Option<crate::Repository>,150 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>,
146}153}
147 154
148// This module encapsulates functionality that works with both feature toggles. Can be combined with `fetch`155// 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
38 SkipToRefUpdate,38 SkipToRefUpdate,
39 /// We can't know for sure if fetching *is not* needed, so we go ahead and negotiate.39 /// We can't know for sure if fetching *is not* needed, so we go ahead and negotiate.
40 MustNegotiate {40 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.
42 remote_ref_target_known: Vec<bool>,42 remote_ref_target_known: Vec<bool>,
43 },43 },
44}44}
@@ -221,7 +221,7 @@ pub(crate) fn add_wants( · 176 unmodified lines
221 shallow: &fetch::Shallow,221 shallow: &fetch::Shallow,
222 mapping_is_ignored: impl Fn(&fetch::Mapping) -> bool,222 mapping_is_ignored: impl Fn(&fetch::Mapping) -> bool,
223) {223) {
224 // When using shallow, we can't exclude `wants` as the remote won't send anything then. Thus we have to resend everything224 // When using shallow, we can't exclude `wants` as the remote won't send anything then. Thus, we have to resend everything
225 // we have as want instead to get exactly the same graph, but possibly deepened.225 // we have as want instead to get exactly the same graph, but possibly deepened.
226 let is_shallow = !matches!(shallow, fetch::Shallow::NoChange);226 let is_shallow = !matches!(shallow, fetch::Shallow::NoChange);
227 let wants = ref_map227 let wants = ref_map
gix/src/remote/connection/fetch/receive_pack.rs+1−1
@@ -114,7 +114,7 @@ where · 113 unmodified lines
114 gix_protocol::fetch::Response::check_required_features(protocol_version, &fetch_features)?;114 gix_protocol::fetch::Response::check_required_features(protocol_version, &fetch_features)?;
115 let sideband_all = fetch_features.iter().any(|(n, _)| *n == "sideband-all");115 let sideband_all = fetch_features.iter().any(|(n, _)| *n == "sideband-all");
116 let mut arguments = gix_protocol::fetch::Arguments::new(protocol_version, fetch_features, con.trace);116 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) {
118 if !arguments.can_use_include_tag() {118 if !arguments.can_use_include_tag() {
119 return Err(Error::MissingServerFeature {119 return Err(Error::MissingServerFeature {
120 feature: "include-tag",120 feature: "include-tag",
gix/src/remote/connection/ref_map.rs+3−3
@@ -79,14 +79,14 @@ where · 78 unmodified lines
79 /// for _fetching_.79 /// for _fetching_.
80 ///80 ///
81 /// This comes in the form of all matching tips on the remote and the object they point to, along with81 /// 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).
83 ///83 ///
84 /// Note that this doesn't fetch the objects mentioned in the tips nor does it make any change to underlying repository.84 /// Note that this doesn't fetch the objects mentioned in the tips nor does it make any change to underlying repository.
85 ///85 ///
86 /// # Consumption86 /// # Consumption
87 ///87 ///
88 /// Due to management of the transport, it's cleanest to only use it for a single interaction. Thus it's consumed along with88 /// Due to management of the transport, it's cleanest to only use it for a single interaction. Thus, it's consumed
89 /// the connection.89 /// along with the connection.
90 ///90 ///
91 /// ### Configuration91 /// ### Configuration
92 ///92 ///
gix/tests/clone/mod.rs+121−4
@@ -1,7 +1,8 @@
1use crate::{remote, util::restricted};1use crate::{remote, util::restricted};
2 2
3#[cfg(feature = "blocking-network-client")]3#[cfg(all(feature = "worktree-mutation", feature = "blocking-network-client"))]
4mod blocking_io {4mod blocking_io {
5 use std::path::Path;
5 use std::{borrow::Cow, sync::atomic::AtomicBool};6 use std::{borrow::Cow, sync::atomic::AtomicBool};
6 7
7 use gix::{8 use gix::{
@@ -500,12 +501,128 @@ mod blocking_io { · 492 unmodified lines
500 let index = repo.index()?;501 let index = repo.index()?;
501 assert_eq!(index.entries().len(), 1, "All entries are known as per HEAD tree");502 assert_eq!(index.entries().len(), 1, "All entries are known as per HEAD tree");
502 503
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) {
504 for entry in index.entries() {622 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)));
506 assert!(entry_path.is_file(), "{entry_path:?} not found on disk")624 assert!(entry_path.is_file(), "{entry_path:?} not found on disk")
507 }625 }
508 Ok(())
509 }626 }
510 627
511 #[test]628 #[test]
gix/tests/repository/object.rs+3−3
@@ -6,7 +6,7 @@ mod write_object { · 5 unmodified lines
6 #[test]6 #[test]
7 fn empty_tree() -> crate::Result {7 fn empty_tree() -> crate::Result {
8 let (_tmp, repo) = empty_bare_repo()?;8 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())?;
10 assert_eq!(10 assert_eq!(
11 oid,11 oid,
12 gix::hash::ObjectId::empty_tree(repo.object_hash()),12 gix::hash::ObjectId::empty_tree(repo.object_hash()),
@@ -280,7 +280,7 @@ mod commit { · 267 unmodified lines
280 crate::restricted(),280 crate::restricted(),
281 )?281 )?
282 .to_thread_local();282 .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();
284 let err = repo284 let err = repo
285 .commit("HEAD", "initial", empty_tree_id, [empty_tree_id])285 .commit("HEAD", "initial", empty_tree_id, [empty_tree_id])
286 .unwrap_err();286 .unwrap_err();
@@ -304,7 +304,7 @@ mod commit { · 17 unmodified lines
304 restricted_and_git(),304 restricted_and_git(),
305 )?305 )?
306 .to_thread_local();306 .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())?;
308 let commit_id = repo.commit("HEAD", "initial", empty_tree_id, gix::commit::NO_PARENT_IDS)?;308 let commit_id = repo.commit("HEAD", "initial", empty_tree_id, gix::commit::NO_PARENT_IDS)?;
309 assert_eq!(309 assert_eq!(
310 commit_id,310 commit_id,
gix/tests/repository/worktree.rs+1−1
@@ -49,7 +49,7 @@ mod with_core_worktree_config { · 48 unmodified lines
49 } else {49 } else {
50 assert_eq!(50 assert_eq!(
51 repo.work_dir().unwrap(),51 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"))?,
53 "absolute workdirs are left untouched"53 "absolute workdirs are left untouched"
54 );54 );
55 }55 }
src/plumbing/main.rs+2−0
@@ -404,6 +404,7 @@ pub fn main() -> Result<()> { · 403 unmodified lines
404 handshake_info,404 handshake_info,
405 bare,405 bare,
406 no_tags,406 no_tags,
407 ref_name,
407 remote,408 remote,
408 shallow,409 shallow,
409 directory,410 directory,
@@ -413,6 +414,7 @@ pub fn main() -> Result<()> { · 3 unmodified lines
413 bare,414 bare,
414 handshake_info,415 handshake_info,
415 no_tags,416 no_tags,
417 ref_name,
416 shallow: shallow.into(),418 shallow: shallow.into(),
417 };419 };
418 prepare_and_run(420 prepare_and_run(
src/plumbing/options/mod.rs+4−0
@@ -434,6 +434,10 @@ pub mod clone { · 433 unmodified lines
434 /// The url of the remote to connect to, like `https://github.com/byron/gitoxide`.434 /// The url of the remote to connect to, like `https://github.com/byron/gitoxide`.
435 pub remote: OsString,435 pub remote: OsString,
436 436
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
437 /// The directory to initialize with the new repository and to which all data should be written.441 /// The directory to initialize with the new repository and to which all data should be written.
438 pub directory: Option<PathBuf>,442 pub directory: Option<PathBuf>,
439 }443 }