Unified · Split
| @@ -6,6 +6,7 @@ pub struct Options { · 5 unmodified lines | ||
| 6 | 6 | pub handshake_info: bool, |
| 7 | 7 | pub no_tags: bool, |
| 8 | 8 | pub shallow: gix::remote::fetch::Shallow, |
| 9 | + pub ref_name: Option<gix::refs::PartialName>, | |
| 9 | 10 | } |
| 10 | 11 | |
| 11 | 12 | pub const PROGRESS_RANGE: std::ops::RangeInclusive<u8> = 1..=3; |
| @@ -31,6 +32,7 @@ pub(crate) mod function { · 19 unmodified lines | ||
| 31 | 32 | handshake_info, |
| 32 | 33 | bare, |
| 33 | 34 | no_tags, |
| 35 | + ref_name, | |
| 34 | 36 | shallow, |
| 35 | 37 | }: Options, |
| 36 | 38 | ) -> anyhow::Result<()> |
| @@ -75,6 +77,7 @@ pub(crate) mod function { · 38 unmodified lines | ||
| 75 | 77 | } |
| 76 | 78 | let (mut checkout, fetch_outcome) = prepare |
| 77 | 79 | .with_shallow(shallow) |
| 80 | + .with_ref_name(ref_name.as_ref())? | |
| 78 | 81 | .fetch_then_checkout(&mut progress, &gix::interrupt::IS_INTERRUPTED)?; |
| 79 | 82 | |
| 80 | 83 | let (repo, outcome) = if bare { |
| @@ -307,11 +307,8 @@ mod baseline { · 306 unmodified lines | ||
| 307 | 307 | |
| 308 | 308 | let mut buf = Vec::new(); |
| 309 | 309 | 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)?; | |
| 315 | 312 | |
| 316 | 313 | Ok((group, collection, base, input)) |
| 317 | 314 | } |
| @@ -16,7 +16,7 @@ fn can_find(db: impl gix_object::Find, hex_id: &str) { · 15 unmodified lines | ||
| 16 | 16 | |
| 17 | 17 | #[test] |
| 18 | 18 | fn loose_object() { |
| 19 | − can_find(&db(), "37d4e6c5c48ba0d245164c4e10d5f41140cab980"); | |
| 19 | + can_find(db(), "37d4e6c5c48ba0d245164c4e10d5f41140cab980"); | |
| 20 | 20 | } |
| 21 | 21 | |
| 22 | 22 | #[test] |
| @@ -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 | 9 | #[test] |
| 10 | 10 | fn loose_object() { |
| 11 | − find_header(&db(), "37d4e6c5c48ba0d245164c4e10d5f41140cab980"); | |
| 11 | + find_header(db(), "37d4e6c5c48ba0d245164c4e10d5f41140cab980"); | |
| 12 | 12 | } |
| 13 | 13 | |
| 14 | 14 | #[test] |
| @@ -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)?; | |
| 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)?; | |
| 368 | 368 | |
| 369 | 369 | assert_eq!(pack.version(), pack::data::Version::V2); |
| 370 | 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 | 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 | 475 | assert_eq!(idx.version(), *kind); |
| 476 | 476 | assert_eq!(idx.num_objects(), *num_objects); |
| 477 | 477 | assert_eq!( |
| @@ -11,7 +11,7 @@ impl PrepareFetch { · 10 unmodified lines | ||
| 11 | 11 | /// |
| 12 | 12 | /// It can also be used to configure additional options, like those for fetching tags. Note that |
| 13 | 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 | 15 | pub fn configure_remote( |
| 16 | 16 | mut self, |
| 17 | 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 | 42 | self.config_overrides = values.into_iter().map(Into::into).collect(); |
| 43 | 43 | 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 | 60 | /// Consumption |
| @@ -28,6 +28,8 @@ pub mod main_worktree { · 27 unmodified lines | ||
| 28 | 28 | CheckoutOptions(#[from] crate::config::checkout_options::Error), |
| 29 | 29 | #[error(transparent)] |
| 30 | 30 | IndexCheckout(#[from] gix_worktree_state::checkout::Error), |
| 31 | + #[error(transparent)] | |
| 32 | + Peel(#[from] crate::reference::peel::Error), | |
| 31 | 33 | #[error("Failed to reopen object database as Arc (only if thread-safety wasn't compiled in)")] |
| 32 | 34 | OpenArcOdb(#[from] std::io::Error), |
| 33 | 35 | #[error("The HEAD reference could not be located")] |
| @@ -62,7 +64,13 @@ pub mod main_worktree { · 28 unmodified lines | ||
| 62 | 64 | /// on thread per logical core. |
| 63 | 65 | /// |
| 64 | 66 | /// 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 | 74 | pub fn main_worktree<P>( |
| 67 | 75 | &mut self, |
| 68 | 76 | mut progress: P, |
| @@ -84,19 +92,26 @@ pub mod main_worktree { · 15 unmodified lines | ||
| 84 | 92 | let repo = self |
| 85 | 93 | .repo |
| 86 | 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 | 96 | let workdir = repo.work_dir().ok_or_else(|| Error::BareRepository { |
| 89 | 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 | 106 | Some(id) => id.object().expect("downloaded from remote").peel_to_tree()?.id, |
| 93 | 107 | None => { |
| 94 | 108 | return Ok(( |
| 95 | 109 | self.repo.take().expect("still present"), |
| 96 | 110 | gix_worktree_state::checkout::Outcome::default(), |
| 97 | − )) | |
| 111 | + )); | |
| 98 | 112 | } |
| 99 | 113 | }; |
| 114 | + | |
| 100 | 115 | let index = gix_index::State::from_tree(&root_tree, &repo.objects, repo.config.protect_options()?) |
| 101 | 116 | .map_err(|err| Error::IndexFromTree { |
| 102 | 117 | id: root_tree, |
| @@ -129,7 +144,7 @@ pub mod main_worktree { · 26 unmodified lines | ||
| 129 | 144 | bytes.show_throughput(start); |
| 130 | 145 | |
| 131 | 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 | } |
| @@ -1,3 +1,5 @@ | ||
| 1 | +use crate::bstr::BString; | |
| 2 | +use crate::bstr::ByteSlice; | |
| 1 | 3 | use crate::clone::PrepareFetch; |
| 2 | 4 | |
| 3 | 5 | /// The error returned by [`PrepareFetch::fetch_only()`]. |
| @@ -35,6 +37,13 @@ pub enum Error { · 31 unmodified lines | ||
| 35 | 37 | }, |
| 36 | 38 | #[error("Failed to update HEAD with values from remote")] |
| 37 | 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 | 49 | /// Modification |
| @@ -117,7 +126,7 @@ impl PrepareFetch { · 76 unmodified lines | ||
| 117 | 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 assure | |
| 129 | + // Add HEAD after the remote was written to config, we need it to know what to check out later, and assure | |
| 121 | 130 | // the ref that HEAD points to is present no matter what. |
| 122 | 131 | let head_refspec = gix_refspec::parse( |
| 123 | 132 | format!("HEAD:refs/remotes/{remote_name}/HEAD").as_str().into(), |
| @@ -136,10 +145,22 @@ impl PrepareFetch { · 12 unmodified lines | ||
| 136 | 145 | if !opts.extra_refspecs.contains(&head_refspec) { |
| 137 | 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 | 155 | opts |
| 140 | 156 | }) |
| 141 | 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 | 164 | if pending_pack.ref_map().object_hash != repo.object_hash() { |
| 144 | 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 | 181 | util::append_config_to_repo_config(repo, config); |
| 161 | 182 | util::update_head( |
| 162 | 183 | repo, |
| 163 | − &outcome.ref_map.remote_refs, | |
| 184 | + &outcome.ref_map, | |
| 164 | 185 | reflog_message.as_ref(), |
| 165 | 186 | remote_name.as_ref(), |
| 187 | + self.ref_name.as_ref(), | |
| 166 | 188 | )?; |
| 167 | 189 | |
| 168 | 190 | Ok((self.repo.take().expect("still present"), outcome)) |
| @@ -180,7 +202,13 @@ impl PrepareFetch { · 11 unmodified lines | ||
| 180 | 202 | P::SubProgress: 'static, |
| 181 | 203 | { |
| 182 | 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 | |
| @@ -2,7 +2,7 @@ use std::{borrow::Cow, io::Write}; · 1 unmodified line | ||
| 2 | 2 | |
| 3 | 3 | use gix_ref::{ |
| 4 | 4 | transaction::{LogChange, RefLog}, |
| 5 | − FullNameRef, | |
| 5 | + FullNameRef, PartialName, | |
| 6 | 6 | }; |
| 7 | 7 | |
| 8 | 8 | use 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 | 61 | /// 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 | 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. | |
| 63 | 64 | pub fn update_head( |
| 64 | 65 | repo: &mut Repository, |
| 65 | − remote_refs: &[gix_protocol::handshake::Ref], | |
| 66 | + ref_map: &crate::remote::fetch::RefMap, | |
| 66 | 67 | reflog_message: &BStr, |
| 67 | 68 | remote_name: &BStr, |
| 69 | + ref_name: Option<&PartialName>, | |
| 68 | 70 | ) -> Result<(), Error> { |
| 69 | 71 | use gix_ref::{ |
| 70 | 72 | transaction::{PreviousValue, RefEdit}, |
| 71 | 73 | Target, |
| 72 | 74 | }; |
| 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(()); | |
| 92 | 97 | }; |
| 93 | 98 | |
| 94 | 99 | let head: gix_ref::FullName = "HEAD".try_into().expect("valid"); |
| @@ -178,7 +183,55 @@ pub fn update_head( · 83 unmodified lines | ||
| 178 | 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 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 | |
| 182 | 235 | /// saved refspecs are able to match it. |
| 183 | 236 | /// For that we reload the remote of `remote_name` and use its `ref_specs` for match. |
| 184 | 237 | fn setup_branch_config( |
| @@ -34,6 +34,9 @@ pub struct PrepareFetch { · 33 unmodified lines | ||
| 34 | 34 | /// How to handle shallow clones |
| 35 | 35 | #[cfg_attr(not(feature = "blocking-network-client"), allow(dead_code))] |
| 36 | 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 | 42 | /// The error returned by [`PrepareFetch::new()`]. |
| @@ -132,17 +135,21 @@ impl PrepareFetch { · 92 unmodified lines | ||
| 132 | 135 | #[cfg(any(feature = "async-network-client", feature = "blocking-network-client"))] |
| 133 | 136 | configure_connection: None, |
| 134 | 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 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. | |
| 141 | 145 | #[must_use] |
| 142 | 146 | #[cfg(feature = "worktree-mutation")] |
| 147 | +#[derive(Debug)] | |
| 143 | 148 | 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. | |
| 145 | 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 | 155 | // This module encapsulates functionality that works with both feature toggles. Can be combined with `fetch` |
| @@ -38,7 +38,7 @@ pub(crate) enum Action { · 37 unmodified lines | ||
| 38 | 38 | SkipToRefUpdate, |
| 39 | 39 | /// We can't know for sure if fetching *is not* needed, so we go ahead and negotiate. |
| 40 | 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 | 42 | remote_ref_target_known: Vec<bool>, |
| 43 | 43 | }, |
| 44 | 44 | } |
| @@ -221,7 +221,7 @@ pub(crate) fn add_wants( · 176 unmodified lines | ||
| 221 | 221 | shallow: &fetch::Shallow, |
| 222 | 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 everything | |
| 224 | + // When using shallow, we can't exclude `wants` as the remote won't send anything then. Thus, we have to resend everything | |
| 225 | 225 | // we have as want instead to get exactly the same graph, but possibly deepened. |
| 226 | 226 | let is_shallow = !matches!(shallow, fetch::Shallow::NoChange); |
| 227 | 227 | let wants = ref_map |
| @@ -114,7 +114,7 @@ where · 113 unmodified lines | ||
| 114 | 114 | gix_protocol::fetch::Response::check_required_features(protocol_version, &fetch_features)?; |
| 115 | 115 | let sideband_all = fetch_features.iter().any(|(n, _)| *n == "sideband-all"); |
| 116 | 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 | 118 | if !arguments.can_use_include_tag() { |
| 119 | 119 | return Err(Error::MissingServerFeature { |
| 120 | 120 | feature: "include-tag", |
| @@ -79,14 +79,14 @@ where · 78 unmodified lines | ||
| 79 | 79 | /// for _fetching_. |
| 80 | 80 | /// |
| 81 | 81 | /// 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 | 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 | 86 | /// # 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 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. | |
| 90 | 90 | /// |
| 91 | 91 | /// ### Configuration |
| 92 | 92 | /// |
| @@ -1,7 +1,8 @@ | ||
| 1 | 1 | use crate::{remote, util::restricted}; |
| 2 | 2 | |
| 3 | −#[cfg(feature = "blocking-network-client")] | |
| 3 | +#[cfg(all(feature = "worktree-mutation", feature = "blocking-network-client"))] | |
| 4 | 4 | mod blocking_io { |
| 5 | + use std::path::Path; | |
| 5 | 6 | use std::{borrow::Cow, sync::atomic::AtomicBool}; |
| 6 | 7 | |
| 7 | 8 | use gix::{ |
| @@ -500,12 +501,128 @@ mod blocking_io { · 492 unmodified lines | ||
| 500 | 501 | let index = repo.index()?; |
| 501 | 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 | 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 | 624 | assert!(entry_path.is_file(), "{entry_path:?} not found on disk") |
| 507 | 625 | } |
| 508 | − Ok(()) | |
| 509 | 626 | } |
| 510 | 627 | |
| 511 | 628 | #[test] |
| @@ -6,7 +6,7 @@ mod write_object { · 5 unmodified lines | ||
| 6 | 6 | #[test] |
| 7 | 7 | fn empty_tree() -> crate::Result { |
| 8 | 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 | 10 | assert_eq!( |
| 11 | 11 | oid, |
| 12 | 12 | gix::hash::ObjectId::empty_tree(repo.object_hash()), |
| @@ -280,7 +280,7 @@ mod commit { · 267 unmodified lines | ||
| 280 | 280 | crate::restricted(), |
| 281 | 281 | )? |
| 282 | 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 | 284 | let err = repo |
| 285 | 285 | .commit("HEAD", "initial", empty_tree_id, [empty_tree_id]) |
| 286 | 286 | .unwrap_err(); |
| @@ -304,7 +304,7 @@ mod commit { · 17 unmodified lines | ||
| 304 | 304 | restricted_and_git(), |
| 305 | 305 | )? |
| 306 | 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 | 308 | let commit_id = repo.commit("HEAD", "initial", empty_tree_id, gix::commit::NO_PARENT_IDS)?; |
| 309 | 309 | assert_eq!( |
| 310 | 310 | commit_id, |
| @@ -49,7 +49,7 @@ mod with_core_worktree_config { · 48 unmodified lines | ||
| 49 | 49 | } else { |
| 50 | 50 | assert_eq!( |
| 51 | 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 | 53 | "absolute workdirs are left untouched" |
| 54 | 54 | ); |
| 55 | 55 | } |
| @@ -404,6 +404,7 @@ pub fn main() -> Result<()> { · 403 unmodified lines | ||
| 404 | 404 | handshake_info, |
| 405 | 405 | bare, |
| 406 | 406 | no_tags, |
| 407 | + ref_name, | |
| 407 | 408 | remote, |
| 408 | 409 | shallow, |
| 409 | 410 | directory, |
| @@ -413,6 +414,7 @@ pub fn main() -> Result<()> { · 3 unmodified lines | ||
| 413 | 414 | bare, |
| 414 | 415 | handshake_info, |
| 415 | 416 | no_tags, |
| 417 | + ref_name, | |
| 416 | 418 | shallow: shallow.into(), |
| 417 | 419 | }; |
| 418 | 420 | prepare_and_run( |
| @@ -434,6 +434,10 @@ pub mod clone { · 433 unmodified lines | ||
| 434 | 434 | /// The url of the remote to connect to, like `https://github.com/byron/gitoxide`. |
| 435 | 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 | 441 | /// The directory to initialize with the new repository and to which all data should be written. |
| 438 | 442 | pub directory: Option<PathBuf>, |
| 439 | 443 | } |