Skip to content

Commit acbfa6f

Browse files
committed
feat: add PrepareFetch::with_ref_name() to control which ref is checked out.
1 parent 0912a46 commit acbfa6f

File tree

9 files changed

+239
-76
lines changed

9 files changed

+239
-76
lines changed

gix/src/clone/access.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ impl PrepareFetch {
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 {
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

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@ use crate::{clone::PrepareCheckout, Repository};
55
pub mod main_worktree {
66
use std::{path::PathBuf, sync::atomic::AtomicBool};
77

8-
use gix_ref::bstr::BStr;
9-
108
use crate::{clone::PrepareCheckout, Progress, Repository};
119

1210
/// The error returned by [`PrepareCheckout::main_worktree()`].
@@ -66,7 +64,7 @@ pub mod main_worktree {
6664
/// on thread per logical core.
6765
///
6866
/// Note that this is a no-op if the remote was empty, leaving this repository empty as well. This can be validated by checking
69-
/// if the `head()` of the returned repository is not unborn.
67+
/// if the `head()` of the returned repository is *not* unborn.
7068
pub fn main_worktree<P>(
7169
&mut self,
7270
mut progress: P,
@@ -76,29 +74,13 @@ pub mod main_worktree {
7674
P: gix_features::progress::NestedProgress,
7775
P::SubProgress: gix_features::progress::NestedProgress + 'static,
7876
{
79-
self.main_worktree_inner(&mut progress, should_interrupt, None)
80-
}
81-
82-
/// Checkout the a worktree, determining how many threads to use by looking at `checkout.workers`, defaulting to using
83-
/// on thread per logical core.
84-
pub fn worktree<P>(
85-
&mut self,
86-
mut progress: P,
87-
should_interrupt: &AtomicBool,
88-
reference: Option<&BStr>,
89-
) -> Result<(Repository, gix_worktree_state::checkout::Outcome), Error>
90-
where
91-
P: gix_features::progress::NestedProgress,
92-
P::SubProgress: gix_features::progress::NestedProgress + 'static,
93-
{
94-
self.main_worktree_inner(&mut progress, should_interrupt, reference)
77+
self.main_worktree_inner(&mut progress, should_interrupt)
9578
}
9679

9780
fn main_worktree_inner(
9881
&mut self,
9982
progress: &mut dyn gix_features::progress::DynNestedProgress,
10083
should_interrupt: &AtomicBool,
101-
reference: Option<&BStr>,
10284
) -> Result<(Repository, gix_worktree_state::checkout::Outcome), Error> {
10385
let _span = gix_trace::coarse!("gix::clone::PrepareCheckout::main_worktree()");
10486
let repo = self
@@ -109,7 +91,7 @@ pub mod main_worktree {
10991
git_dir: repo.git_dir().to_owned(),
11092
})?;
11193

112-
let root_tree_id = match reference {
94+
let root_tree_id = match &self.ref_name {
11395
Some(reference_val) => Some(repo.find_reference(reference_val)?.peel_to_id_in_place()?),
11496
None => repo.head()?.try_peel_to_id_in_place()?,
11597
};

gix/src/clone/fetch/mod.rs

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -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 {
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 {
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 {
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 {
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 {
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

Lines changed: 75 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use std::{borrow::Cow, io::Write};
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
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(
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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ pub struct PrepareFetch {
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,6 +135,7 @@ impl PrepareFetch {
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
}
@@ -140,9 +144,12 @@ impl PrepareFetch {
140144
/// the fetched repository will be dropped.
141145
#[must_use]
142146
#[cfg(feature = "worktree-mutation")]
147+
#[derive(Debug)]
143148
pub struct PrepareCheckout {
144149
/// A freshly initialized repository which is owned by us, or `None` if it was handed to the user
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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ pub(crate) enum Action {
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(
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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ where
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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,14 +79,14 @@ where
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
///

0 commit comments

Comments
 (0)