Skip to content

Commit

Permalink
Merge branch 'feat/checkout-other-refs'
Browse files Browse the repository at this point in the history
  • Loading branch information
Byron committed Jun 20, 2024
2 parents 2f9f0ac + f36b9bd commit ecfde07
Show file tree
Hide file tree
Showing 18 changed files with 297 additions and 58 deletions.
3 changes: 3 additions & 0 deletions gitoxide-core/src/repository/clone.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pub struct Options {
pub handshake_info: bool,
pub no_tags: bool,
pub shallow: gix::remote::fetch::Shallow,
pub ref_name: Option<gix::refs::PartialName>,
}

pub const PROGRESS_RANGE: std::ops::RangeInclusive<u8> = 1..=3;
Expand All @@ -31,6 +32,7 @@ pub(crate) mod function {
handshake_info,
bare,
no_tags,
ref_name,
shallow,
}: Options,
) -> anyhow::Result<()>
Expand Down Expand Up @@ -75,6 +77,7 @@ pub(crate) mod function {
}
let (mut checkout, fetch_outcome) = prepare
.with_shallow(shallow)
.with_ref_name(ref_name.as_ref())?
.fetch_then_checkout(&mut progress, &gix::interrupt::IS_INTERRUPTED)?;

let (repo, outcome) = if bare {
Expand Down
7 changes: 2 additions & 5 deletions gix-attributes/tests/search/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -307,11 +307,8 @@ mod baseline {

let mut buf = Vec::new();
let mut collection = MetadataCollection::default();
let group = gix_attributes::Search::new_globals(
&mut [base.join("user.attributes")].into_iter(),
&mut buf,
&mut collection,
)?;
let group =
gix_attributes::Search::new_globals([base.join("user.attributes")].into_iter(), &mut buf, &mut collection)?;

Ok((group, collection, base, input))
}
Expand Down
2 changes: 1 addition & 1 deletion gix-odb/tests/odb/find/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ fn can_find(db: impl gix_object::Find, hex_id: &str) {

#[test]
fn loose_object() {
can_find(&db(), "37d4e6c5c48ba0d245164c4e10d5f41140cab980");
can_find(db(), "37d4e6c5c48ba0d245164c4e10d5f41140cab980");
}

#[test]
Expand Down
2 changes: 1 addition & 1 deletion gix-odb/tests/odb/header/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ fn find_header(db: impl gix_odb::Header, hex_id: &str) -> gix_odb::find::Header

#[test]
fn loose_object() {
find_header(&db(), "37d4e6c5c48ba0d245164c4e10d5f41140cab980");
find_header(db(), "37d4e6c5c48ba0d245164c4e10d5f41140cab980");
}

#[test]
Expand Down
6 changes: 3 additions & 3 deletions gix-pack/tests/pack/index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -363,8 +363,8 @@ fn pack_lookup() -> Result<(), Box<dyn std::error::Error>> {
},
),
] {
let idx = index::File::at(&fixture_path(index_path), gix_hash::Kind::Sha1)?;
let pack = pack::data::File::at(&fixture_path(pack_path), gix_hash::Kind::Sha1)?;
let idx = index::File::at(fixture_path(index_path), gix_hash::Kind::Sha1)?;
let pack = pack::data::File::at(fixture_path(pack_path), gix_hash::Kind::Sha1)?;

assert_eq!(pack.version(), pack::data::Version::V2);
assert_eq!(pack.num_objects(), idx.num_objects());
Expand Down Expand Up @@ -471,7 +471,7 @@ fn iter() -> Result<(), Box<dyn std::error::Error>> {
"0f3ea84cd1bba10c2a03d736a460635082833e59",
),
] {
let idx = index::File::at(&fixture_path(path), gix_hash::Kind::Sha1)?;
let idx = index::File::at(fixture_path(path), gix_hash::Kind::Sha1)?;
assert_eq!(idx.version(), *kind);
assert_eq!(idx.num_objects(), *num_objects);
assert_eq!(
Expand Down
15 changes: 14 additions & 1 deletion gix/src/clone/access.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ impl PrepareFetch {
///
/// It can also be used to configure additional options, like those for fetching tags. Note that
/// [`with_fetch_tags()`](crate::Remote::with_fetch_tags()) should be called here to configure the clone as desired.
/// Otherwise a clone is configured to be complete and fetches all tags, not only those reachable from all branches.
/// Otherwise, a clone is configured to be complete and fetches all tags, not only those reachable from all branches.
pub fn configure_remote(
mut self,
f: impl FnMut(crate::Remote<'_>) -> Result<crate::Remote<'_>, Box<dyn std::error::Error + Send + Sync>> + 'static,
Expand Down Expand Up @@ -42,6 +42,19 @@ impl PrepareFetch {
self.config_overrides = values.into_iter().map(Into::into).collect();
self
}

/// Set the `name` of the reference to check out, instead of the remote `HEAD`.
/// If `None`, the `HEAD` will be used, which is the default.
///
/// Note that `name` should be a partial name like `main` or `feat/one`, but can be a full ref name.
/// If a branch on the remote matches, it will automatically be retrieved even without a refspec.
pub fn with_ref_name<'a, Name, E>(mut self, name: Option<Name>) -> Result<Self, E>
where
Name: TryInto<&'a gix_ref::PartialNameRef, Error = E>,
{
self.ref_name = name.map(TryInto::try_into).transpose()?.map(ToOwned::to_owned);
Ok(self)
}
}

/// Consumption
Expand Down
25 changes: 20 additions & 5 deletions gix/src/clone/checkout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ pub mod main_worktree {
CheckoutOptions(#[from] crate::config::checkout_options::Error),
#[error(transparent)]
IndexCheckout(#[from] gix_worktree_state::checkout::Error),
#[error(transparent)]
Peel(#[from] crate::reference::peel::Error),
#[error("Failed to reopen object database as Arc (only if thread-safety wasn't compiled in)")]
OpenArcOdb(#[from] std::io::Error),
#[error("The HEAD reference could not be located")]
Expand Down Expand Up @@ -62,7 +64,13 @@ pub mod main_worktree {
/// on thread per logical core.
///
/// Note that this is a no-op if the remote was empty, leaving this repository empty as well. This can be validated by checking
/// if the `head()` of the returned repository is not unborn.
/// if the `head()` of the returned repository is *not* unborn.
///
/// # Panics
///
/// If called after it was successful. The reason here is that it auto-deletes the contained repository,
/// and keeps track of this by means of keeping just one repository instance, which is passed to the user
/// after success.
pub fn main_worktree<P>(
&mut self,
mut progress: P,
Expand All @@ -84,19 +92,26 @@ pub mod main_worktree {
let repo = self
.repo
.as_ref()
.expect("still present as we never succeeded the worktree checkout yet");
.expect("BUG: this method may only be called until it is successful");
let workdir = repo.work_dir().ok_or_else(|| Error::BareRepository {
git_dir: repo.git_dir().to_owned(),
})?;
let root_tree = match repo.head()?.try_peel_to_id_in_place()? {

let root_tree_id = match &self.ref_name {
Some(reference_val) => Some(repo.find_reference(reference_val)?.peel_to_id_in_place()?),
None => repo.head()?.try_peel_to_id_in_place()?,
};

let root_tree = match root_tree_id {
Some(id) => id.object().expect("downloaded from remote").peel_to_tree()?.id,
None => {
return Ok((
self.repo.take().expect("still present"),
gix_worktree_state::checkout::Outcome::default(),
))
));
}
};

let index = gix_index::State::from_tree(&root_tree, &repo.objects, repo.config.protect_options()?)
.map_err(|err| Error::IndexFromTree {
id: root_tree,
Expand Down Expand Up @@ -129,7 +144,7 @@ pub mod main_worktree {
bytes.show_throughput(start);

index.write(Default::default())?;
Ok((self.repo.take().expect("still present"), outcome))
Ok((self.repo.take().expect("still present").clone(), outcome))
}
}
}
Expand Down
34 changes: 31 additions & 3 deletions gix/src/clone/fetch/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use crate::bstr::BString;
use crate::bstr::ByteSlice;
use crate::clone::PrepareFetch;

/// The error returned by [`PrepareFetch::fetch_only()`].
Expand Down Expand Up @@ -35,6 +37,13 @@ pub enum Error {
},
#[error("Failed to update HEAD with values from remote")]
HeadUpdate(#[from] crate::reference::edit::Error),
#[error("The remote didn't have any ref that matched '{}'", wanted.as_ref().as_bstr())]
RefNameMissing { wanted: gix_ref::PartialName },
#[error("The remote has {} refs for '{}', try to use a specific name: {}", candidates.len(), wanted.as_ref().as_bstr(), candidates.iter().filter_map(|n| n.to_str().ok()).collect::<Vec<_>>().join(", "))]
RefNameAmbiguous {
wanted: gix_ref::PartialName,
candidates: Vec<BString>,
},
}

/// Modification
Expand Down Expand Up @@ -117,7 +126,7 @@ impl PrepareFetch {
remote = remote.with_fetch_tags(fetch_tags);
}

// Add HEAD after the remote was written to config, we need it to know what to checkout later, and assure
// Add HEAD after the remote was written to config, we need it to know what to check out later, and assure
// the ref that HEAD points to is present no matter what.
let head_refspec = gix_refspec::parse(
format!("HEAD:refs/remotes/{remote_name}/HEAD").as_str().into(),
Expand All @@ -136,10 +145,22 @@ impl PrepareFetch {
if !opts.extra_refspecs.contains(&head_refspec) {
opts.extra_refspecs.push(head_refspec)
}
if let Some(ref_name) = &self.ref_name {
opts.extra_refspecs.push(
gix_refspec::parse(ref_name.as_ref().as_bstr(), gix_refspec::parse::Operation::Fetch)
.expect("partial names are valid refspecs")
.to_owned(),
);
}
opts
})
.await?
};

// Assure problems with custom branch names fail early, not after getting the pack or during negotiation.
if let Some(ref_name) = &self.ref_name {
util::find_custom_refname(pending_pack.ref_map(), ref_name)?;
}
if pending_pack.ref_map().object_hash != repo.object_hash() {
unimplemented!("configure repository to expect a different object hash as advertised by the server")
}
Expand All @@ -160,9 +181,10 @@ impl PrepareFetch {
util::append_config_to_repo_config(repo, config);
util::update_head(
repo,
&outcome.ref_map.remote_refs,
&outcome.ref_map,
reflog_message.as_ref(),
remote_name.as_ref(),
self.ref_name.as_ref(),
)?;

Ok((self.repo.take().expect("still present"), outcome))
Expand All @@ -180,7 +202,13 @@ impl PrepareFetch {
P::SubProgress: 'static,
{
let (repo, fetch_outcome) = self.fetch_only(progress, should_interrupt)?;
Ok((crate::clone::PrepareCheckout { repo: repo.into() }, fetch_outcome))
Ok((
crate::clone::PrepareCheckout {
repo: repo.into(),
ref_name: self.ref_name.clone(),
},
fetch_outcome,
))
}
}

Expand Down
97 changes: 75 additions & 22 deletions gix/src/clone/fetch/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::{borrow::Cow, io::Write};

use gix_ref::{
transaction::{LogChange, RefLog},
FullNameRef,
FullNameRef, PartialName,
};

use super::Error;
Expand Down Expand Up @@ -60,35 +60,40 @@ pub fn append_config_to_repo_config(repo: &mut Repository, config: gix_config::F

/// HEAD cannot be written by means of refspec by design, so we have to do it manually here. Also create the pointed-to ref
/// if we have to, as it might not have been naturally included in the ref-specs.
/// Lastly, use `ref_name` if it was provided instead, and let `HEAD` point to it.
pub fn update_head(
repo: &mut Repository,
remote_refs: &[gix_protocol::handshake::Ref],
ref_map: &crate::remote::fetch::RefMap,
reflog_message: &BStr,
remote_name: &BStr,
ref_name: Option<&PartialName>,
) -> Result<(), Error> {
use gix_ref::{
transaction::{PreviousValue, RefEdit},
Target,
};
let (head_peeled_id, head_ref) = match remote_refs.iter().find_map(|r| {
Some(match r {
gix_protocol::handshake::Ref::Symbolic {
full_ref_name,
target,
tag: _,
object,
} if full_ref_name == "HEAD" => (Some(object.as_ref()), Some(target)),
gix_protocol::handshake::Ref::Direct { full_ref_name, object } if full_ref_name == "HEAD" => {
(Some(object.as_ref()), None)
}
gix_protocol::handshake::Ref::Unborn { full_ref_name, target } if full_ref_name == "HEAD" => {
(None, Some(target))
}
_ => return None,
})
}) {
Some(t) => t,
None => return Ok(()),
let head_info = match ref_name {
Some(ref_name) => Some(find_custom_refname(ref_map, ref_name)?),
None => ref_map.remote_refs.iter().find_map(|r| {
Some(match r {
gix_protocol::handshake::Ref::Symbolic {
full_ref_name,
target,
tag: _,
object,
} if full_ref_name == "HEAD" => (Some(object.as_ref()), Some(target.as_bstr())),
gix_protocol::handshake::Ref::Direct { full_ref_name, object } if full_ref_name == "HEAD" => {
(Some(object.as_ref()), None)
}
gix_protocol::handshake::Ref::Unborn { full_ref_name, target } if full_ref_name == "HEAD" => {
(None, Some(target.as_bstr()))
}
_ => return None,
})
}),
};
let Some((head_peeled_id, head_ref)) = head_info else {
return Ok(());
};

let head: gix_ref::FullName = "HEAD".try_into().expect("valid");
Expand Down Expand Up @@ -178,7 +183,55 @@ pub fn update_head(
Ok(())
}

/// Setup the remote configuration for `branch` so that it points to itself, but on the remote, if and only if currently
pub(super) fn find_custom_refname<'a>(
ref_map: &'a crate::remote::fetch::RefMap,
ref_name: &PartialName,
) -> Result<(Option<&'a gix_hash::oid>, Option<&'a BStr>), Error> {
let group = gix_refspec::MatchGroup::from_fetch_specs(Some(
gix_refspec::parse(ref_name.as_ref().as_bstr(), gix_refspec::parse::Operation::Fetch)
.expect("partial names are valid refs"),
));
// TODO: to fix ambiguity, implement priority system
let filtered_items: Vec<_> = ref_map
.mappings
.iter()
.filter_map(|m| {
m.remote
.as_name()
.and_then(|name| m.remote.as_id().map(|id| (name, id)))
})
.map(|(full_ref_name, target)| gix_refspec::match_group::Item {
full_ref_name,
target,
object: None,
})
.collect();
let res = group.match_remotes(filtered_items.iter().copied());
match res.mappings.len() {
0 => Err(Error::RefNameMissing {
wanted: ref_name.clone(),
}),
1 => {
let item = filtered_items[res.mappings[0]
.item_index
.expect("we map by name only and have no object-id in refspec")];
Ok((Some(item.target), Some(item.full_ref_name)))
}
_ => Err(Error::RefNameAmbiguous {
wanted: ref_name.clone(),
candidates: res
.mappings
.iter()
.filter_map(|m| match m.lhs {
gix_refspec::match_group::SourceRef::FullName(name) => Some(name.to_owned()),
gix_refspec::match_group::SourceRef::ObjectId(_) => None,
})
.collect(),
}),
}
}

/// Set up the remote configuration for `branch` so that it points to itself, but on the remote, if and only if currently
/// saved refspecs are able to match it.
/// For that we reload the remote of `remote_name` and use its `ref_specs` for match.
fn setup_branch_config(
Expand Down
Loading

0 comments on commit ecfde07

Please sign in to comment.