Unified · Split
| @@ -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 | ||
| 11 | pub const PROGRESS_RANGE: std::ops::RangeInclusive<u8> = 1..=3; | 12 | pub 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) = prepare | 78 | 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 { |
| @@ -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 | } |
| @@ -16,7 +16,7 @@ fn can_find(db: impl gix_object::Find, hex_id: &str) { · 15 unmodified lines | |||
| 16 | 16 | ||
| 17 | #[test] | 17 | #[test] |
| 18 | fn loose_object() { | 18 | fn loose_object() { |
| 19 | can_find(&db(), "37d4e6c5c48ba0d245164c4e10d5f41140cab980"); | 19 | can_find(db(), "37d4e6c5c48ba0d245164c4e10d5f41140cab980"); |
| 20 | } | 20 | } |
| 21 | 21 | ||
| 22 | #[test] | 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 | #[test] | 9 | #[test] |
| 10 | fn loose_object() { | 10 | fn loose_object() { |
| 11 | find_header(&db(), "37d4e6c5c48ba0d245164c4e10d5f41140cab980"); | 11 | find_header(db(), "37d4e6c5c48ba0d245164c4e10d5f41140cab980"); |
| 12 | } | 12 | } |
| 13 | 13 | ||
| 14 | #[test] | 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)?; | 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!( |
| @@ -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 that | 12 | /// 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 | self | 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 | /// Consumption | 60 | /// Consumption |
| @@ -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 checking | 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 | 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 = self | 92 | let repo = self |
| 85 | .repo | 93 | .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 | } |
| @@ -1,3 +1,5 @@ | |||
| 1 | use crate::bstr::BString; | ||
| 2 | use crate::bstr::ByteSlice; | ||
| 1 | use crate::clone::PrepareFetch; | 3 | use 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 | /// Modification | 49 | /// 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 assure | 129 | // 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 | opts | 155 | 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 | ||
| @@ -2,7 +2,7 @@ use std::{borrow::Cow, io::Write}; · 1 unmodified line | |||
| 2 | 2 | ||
| 3 | use gix_ref::{ | 3 | use gix_ref::{ |
| 4 | transaction::{LogChange, RefLog}, | 4 | transaction::{LogChange, RefLog}, |
| 5 | FullNameRef, | 5 | FullNameRef, PartialName, |
| 6 | }; | 6 | }; |
| 7 | 7 | ||
| 8 | use super::Error; | 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 | /// HEAD cannot be written by means of refspec by design, so we have to do it manually here. Also create the pointed-to ref | 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 | /// 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. | ||
| 63 | pub fn update_head( | 64 | pub 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 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 | /// 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. |
| 184 | fn setup_branch_config( | 237 | fn setup_branch_config( |
| @@ -34,6 +34,9 @@ pub struct PrepareFetch { · 33 unmodified lines | |||
| 34 | /// How to handle shallow clones | 34 | /// 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 successfully | 143 | /// 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)] | ||
| 143 | pub struct PrepareCheckout { | 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 | 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` |
| @@ -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 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 | // 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_map | 227 | let wants = ref_map |
| @@ -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", |
| @@ -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 with | 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 | /// 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 | /// # Consumption | 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 | 88 | /// 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 | /// ### Configuration | 91 | /// ### Configuration |
| 92 | /// | 92 | /// |
| @@ -1,7 +1,8 @@ | |||
| 1 | use crate::{remote, util::restricted}; | 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 | mod blocking_io { | 4 | mod 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] |
| @@ -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 = repo | 284 | 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, |
| @@ -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 | } |
| @@ -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( |
| @@ -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 | } |