Skip to content

Commit ecfde07

Browse files
committed
Merge branch 'feat/checkout-other-refs'
2 parents 2f9f0ac + f36b9bd commit ecfde07

File tree

18 files changed

+297
-58
lines changed

18 files changed

+297
-58
lines changed

gitoxide-core/src/repository/clone.rs

+3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ pub struct Options {
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 {
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 {
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
Original file line numberDiff line numberDiff line change
@@ -307,11 +307,8 @@ mod baseline {
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
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ fn can_find(db: impl gix_object::Find, hex_id: &str) {
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
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ fn find_header(db: impl gix_odb::Header, hex_id: &str) -> gix_odb::find::Header
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
Original file line numberDiff line numberDiff line change
@@ -363,8 +363,8 @@ fn pack_lookup() -> Result<(), Box<dyn std::error::Error>> {
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>> {
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
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

+20-5
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ pub mod main_worktree {
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 {
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 {
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 {
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
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

+75-22
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(

0 commit comments

Comments
 (0)