Unified · Split
| @@ -637,10 +637,14 @@ See its [README.md](https://github.com/Byron/gitoxide/blob/main/gix-lock/README. · 636 unmodified lines | ||
| 637 | 637 | * [x] find remote itself |
| 638 | 638 | - [ ] respect `branch.<name>.merge` in the returned remote. |
| 639 | 639 | * **remotes** |
| 640 | − * [ ] clone | |
| 640 | + * [x] clone | |
| 641 | 641 | * [ ] shallow |
| 642 | + * [ ] include-tags when shallow is used (needs separate fetch) | |
| 643 | + * [ ] prune non-existing shallow commits | |
| 642 | 644 | * [ ] [bundles](https://git-scm.com/docs/git-bundle) |
| 643 | 645 | * [x] fetch |
| 646 | + * [ ] 'ref-in-want' | |
| 647 | + * [ ] standard negotiation algorithms (right now we only have a 'naive' one) | |
| 644 | 648 | * [ ] push |
| 645 | 649 | * [x] ls-refs |
| 646 | 650 | * [x] ls-refs with ref-spec filter |
| @@ -121,6 +121,7 @@ impl PrepareFetch { · 120 unmodified lines | ||
| 121 | 121 | .with_reflog_message(RefLogMessage::Override { |
| 122 | 122 | message: reflog_message.clone(), |
| 123 | 123 | }) |
| 124 | + .with_shallow(self.shallow.clone()) | |
| 124 | 125 | .receive(should_interrupt)?; |
| 125 | 126 | |
| 126 | 127 | util::append_config_to_repo_config(repo, config); |
| @@ -184,6 +185,12 @@ impl PrepareFetch { · 57 unmodified lines | ||
| 184 | 185 | self.remote_name = Some(crate::remote::name::validated(name)?); |
| 185 | 186 | Ok(self) |
| 186 | 187 | } |
| 188 | + | |
| 189 | + /// Make this clone a shallow one with the respective choice of shallow-ness. | |
| 190 | + pub fn with_shallow(mut self, shallow: crate::remote::fetch::Shallow) -> Self { | |
| 191 | + self.shallow = shallow; | |
| 192 | + self | |
| 193 | + } | |
| 187 | 194 | } |
| 188 | 195 | |
| 189 | 196 | /// Consumption |
| @@ -1,7 +1,7 @@ | ||
| 1 | 1 | #![allow(clippy::result_large_err)] |
| 2 | 2 | use std::convert::TryInto; |
| 3 | 3 | |
| 4 | −use crate::{bstr::BString, config::tree::gitoxide}; | |
| 4 | +use crate::{bstr::BString, config::tree::gitoxide, remote}; | |
| 5 | 5 | |
| 6 | 6 | type ConfigureRemoteFn = |
| 7 | 7 | Box<dyn FnMut(crate::Remote<'_>) -> Result<crate::Remote<'_>, Box<dyn std::error::Error + Send + Sync>>>; |
| @@ -22,6 +22,9 @@ pub struct PrepareFetch { · 14 unmodified lines | ||
| 22 | 22 | /// The url to clone from |
| 23 | 23 | #[cfg_attr(not(feature = "blocking-network-client"), allow(dead_code))] |
| 24 | 24 | url: gix_url::Url, |
| 25 | + /// How to handle shallow clones | |
| 26 | + #[cfg_attr(not(feature = "blocking-network-client"), allow(dead_code))] | |
| 27 | + shallow: remote::fetch::Shallow, | |
| 25 | 28 | } |
| 26 | 29 | |
| 27 | 30 | /// The error returned by [`PrepareFetch::new()`]. |
| @@ -99,6 +102,7 @@ impl PrepareFetch { · 71 unmodified lines | ||
| 99 | 102 | repo: Some(repo), |
| 100 | 103 | remote_name: None, |
| 101 | 104 | configure_remote: None, |
| 105 | + shallow: remote::fetch::Shallow::NoChange, | |
| 102 | 106 | }) |
| 103 | 107 | } |
| 104 | 108 | } |
| @@ -7,6 +7,8 @@ impl Clone { · 6 unmodified lines | ||
| 7 | 7 | /// The `clone.defaultRemoteName` key. |
| 8 | 8 | pub const DEFAULT_REMOTE_NAME: keys::RemoteName = |
| 9 | 9 | keys::RemoteName::new_remote_name("defaultRemoteName", &config::Tree::CLONE); |
| 10 | + /// The `clone.rejectShallow` key. | |
| 11 | + pub const REJECT_SHALLOW: keys::Boolean = keys::Boolean::new_boolean("rejectShallow", &config::Tree::CLONE); | |
| 10 | 12 | } |
| 11 | 13 | |
| 12 | 14 | impl Section for Clone { |
| @@ -15,6 +17,6 @@ impl Section for Clone { · 2 unmodified lines | ||
| 15 | 17 | } |
| 16 | 18 | |
| 17 | 19 | fn keys(&self) -> &[&dyn Key] { |
| 18 | − &[&Self::DEFAULT_REMOTE_NAME] | |
| 20 | + &[&Self::DEFAULT_REMOTE_NAME, &Self::REJECT_SHALLOW] | |
| 19 | 21 | } |
| 20 | 22 | } |
| @@ -28,6 +28,21 @@ pub enum Error { · 27 unmodified lines | ||
| 28 | 28 | path: std::path::PathBuf, |
| 29 | 29 | source: std::io::Error, |
| 30 | 30 | }, |
| 31 | + #[error(transparent)] | |
| 32 | + ShallowOpen(#[from] crate::shallow::open::Error), | |
| 33 | + #[error("Server lack feature {feature:?}: {description}")] | |
| 34 | + MissingServerFeature { | |
| 35 | + feature: &'static str, | |
| 36 | + description: &'static str, | |
| 37 | + }, | |
| 38 | + #[error("Could not write 'shallow' file to incorporate remote updates after fetching")] | |
| 39 | + WriteShallowFile(#[from] crate::shallow::write::Error), | |
| 40 | + #[error("'shallow' file could not be locked in preparation for writing changes")] | |
| 41 | + LockShallowFile(#[from] gix_lock::acquire::Error), | |
| 42 | + #[error("Could not obtain configuration to learn if shallow remotes should be rejected")] | |
| 43 | + RejectShallowRemoteConfig(#[from] config::boolean::Error), | |
| 44 | + #[error("Receiving objects from shallow remotes is prohibited due to the value of `clone.rejectShallow`")] | |
| 45 | + RejectShallowRemote, | |
| 31 | 46 | } |
| 32 | 47 | |
| 33 | 48 | impl gix_protocol::transport::IsSpuriousError for Error { |
| @@ -149,6 +149,7 @@ where · 148 unmodified lines | ||
| 149 | 149 | dry_run: DryRun::No, |
| 150 | 150 | reflog_message: None, |
| 151 | 151 | write_packed_refs: WritePackedRefs::Never, |
| 152 | + shallow: Default::default(), | |
| 152 | 153 | }) |
| 153 | 154 | } |
| 154 | 155 | } |
| @@ -179,6 +180,7 @@ where · 24 unmodified lines | ||
| 179 | 180 | dry_run: DryRun, |
| 180 | 181 | reflog_message: Option<RefLogMessage>, |
| 181 | 182 | write_packed_refs: WritePackedRefs, |
| 183 | + shallow: remote::fetch::Shallow, | |
| 182 | 184 | } |
| 183 | 185 | |
| 184 | 186 | /// Builder |
| @@ -212,6 +214,14 @@ where · 27 unmodified lines | ||
| 212 | 214 | self.reflog_message = reflog_message.into(); |
| 213 | 215 | self |
| 214 | 216 | } |
| 217 | + | |
| 218 | + /// Define what to do when the current repository is a shallow clone. | |
| 219 | + /// | |
| 220 | + /// *Has no effect if the current repository is not as shallow clone.* | |
| 221 | + pub fn with_shallow(mut self, shallow: remote::fetch::Shallow) -> Self { | |
| 222 | + self.shallow = shallow; | |
| 223 | + self | |
| 224 | + } | |
| 215 | 225 | } |
| 216 | 226 | |
| 217 | 227 | impl<'remote, 'repo, T, P> Drop for Prepare<'remote, 'repo, T, P> |
| @@ -1,3 +1,5 @@ | ||
| 1 | +use gix_refspec::RefSpec; | |
| 2 | + | |
| 1 | 3 | /// The way the negotiation is performed |
| 2 | 4 | #[derive(Copy, Clone)] |
| 3 | 5 | pub(crate) enum Algorithm { |
| @@ -16,6 +18,7 @@ pub enum Error { · 12 unmodified lines | ||
| 16 | 18 | /// Negotiate one round with `algo` by looking at `ref_map` and adjust `arguments` to contain the haves and wants. |
| 17 | 19 | /// If this is not the first round, the `previous_response` is set with the last recorded server response. |
| 18 | 20 | /// Returns `true` if the negotiation is done from our side so the server won't keep asking. |
| 21 | +#[allow(clippy::too_many_arguments)] | |
| 19 | 22 | pub(crate) fn one_round( |
| 20 | 23 | algo: Algorithm, |
| 21 | 24 | round: usize, |
| @@ -24,13 +27,15 @@ pub(crate) fn one_round( · 2 unmodified lines | ||
| 24 | 27 | fetch_tags: crate::remote::fetch::Tags, |
| 25 | 28 | arguments: &mut gix_protocol::fetch::Arguments, |
| 26 | 29 | _previous_response: Option<&gix_protocol::fetch::Response>, |
| 30 | + wants_shallow_change: Option<&[RefSpec]>, | |
| 27 | 31 | ) -> Result<bool, Error> { |
| 28 | 32 | let tag_refspec_to_ignore = fetch_tags |
| 29 | 33 | .to_refspec() |
| 30 | 34 | .filter(|_| matches!(fetch_tags, crate::remote::fetch::Tags::Included)); |
| 35 | + let non_wildcard_specs_only = wants_shallow_change; | |
| 31 | 36 | match algo { |
| 32 | 37 | Algorithm::Naive => { |
| 33 | − assert_eq!(round, 1, "Naive always finishes after the first round, and claims."); | |
| 38 | + assert_eq!(round, 1, "Naive always finishes after the first round, it claims."); | |
| 34 | 39 | let mut has_missing_tracking_branch = false; |
| 35 | 40 | for mapping in &ref_map.mappings { |
| 36 | 41 | if tag_refspec_to_ignore.map_or(false, |tag_spec| { |
| @@ -42,6 +47,14 @@ pub(crate) fn one_round( · 5 unmodified lines | ||
| 42 | 47 | }) { |
| 43 | 48 | continue; |
| 44 | 49 | } |
| 50 | + if non_wildcard_specs_only | |
| 51 | + .and_then(|refspecs| mapping.spec_index.get(refspecs, &ref_map.extra_refspecs)) | |
| 52 | + .map_or(false, |spec| { | |
| 53 | + spec.to_ref().local().map_or(false, |ref_| ref_.contains(&b'*')) | |
| 54 | + }) | |
| 55 | + { | |
| 56 | + continue; | |
| 57 | + } | |
| 45 | 58 | let have_id = mapping.local.as_ref().and_then(|name| { |
| 46 | 59 | repo.find_reference(name) |
| 47 | 60 | .ok() |
| @@ -50,7 +63,7 @@ pub(crate) fn one_round( · 2 unmodified lines | ||
| 50 | 63 | match have_id { |
| 51 | 64 | Some(have_id) => { |
| 52 | 65 | if let Some(want_id) = mapping.remote.as_id() { |
| 53 | − if want_id != have_id { | |
| 66 | + if want_id != have_id || wants_shallow_change.is_some() { | |
| 54 | 67 | arguments.want(want_id); |
| 55 | 68 | arguments.have(have_id); |
| 56 | 69 | } |
| @@ -65,10 +78,11 @@ pub(crate) fn one_round( · 8 unmodified lines | ||
| 65 | 78 | } |
| 66 | 79 | } |
| 67 | 80 | |
| 68 | − if has_missing_tracking_branch { | |
| 81 | + if has_missing_tracking_branch || (wants_shallow_change.is_some() && arguments.is_empty()) { | |
| 69 | 82 | if let Ok(Some(r)) = repo.head_ref() { |
| 70 | 83 | if let Some(id) = r.target().try_id() { |
| 71 | 84 | arguments.have(id); |
| 85 | + arguments.want(id); | |
| 72 | 86 | } |
| 73 | 87 | } |
| 74 | 88 | } |
| @@ -1,16 +1,18 @@ | ||
| 1 | 1 | use std::sync::atomic::AtomicBool; |
| 2 | 2 | |
| 3 | 3 | use gix_odb::FindExt; |
| 4 | +use gix_protocol::fetch::Arguments; | |
| 4 | 5 | use gix_protocol::transport::client::Transport; |
| 5 | 6 | |
| 7 | +use crate::config::tree::Clone; | |
| 6 | 8 | use crate::{ |
| 7 | 9 | remote, |
| 8 | 10 | remote::{ |
| 9 | 11 | connection::fetch::config, |
| 10 | 12 | fetch, |
| 11 | − fetch::{negotiate, refs, Error, Outcome, Prepare, ProgressId, RefLogMessage, Status}, | |
| 13 | + fetch::{negotiate, refs, Error, Outcome, Prepare, ProgressId, RefLogMessage, Shallow, Status}, | |
| 12 | 14 | }, |
| 13 | − Progress, | |
| 15 | + Progress, Repository, | |
| 14 | 16 | }; |
| 15 | 17 | |
| 16 | 18 | impl<'remote, 'repo, T, P> Prepare<'remote, 'repo, T, P> |
| @@ -82,10 +84,17 @@ where · 65 unmodified lines | ||
| 82 | 84 | let mut arguments = gix_protocol::fetch::Arguments::new(protocol_version, fetch_features); |
| 83 | 85 | if matches!(con.remote.fetch_tags, crate::remote::fetch::Tags::Included) { |
| 84 | 86 | if !arguments.can_use_include_tag() { |
| 85 | − unimplemented!("we expect servers to support 'include-tag', otherwise we have to implement another pass to fetch attached tags separately"); | |
| 87 | + return Err(Error::MissingServerFeature { | |
| 88 | + feature: "include-tag", | |
| 89 | + description: | |
| 90 | + // NOTE: if this is an issue, we could probably do what's proposed here. | |
| 91 | + "To make this work we would have to implement another pass to fetch attached tags separately", | |
| 92 | + }); | |
| 86 | 93 | } |
| 87 | 94 | arguments.use_include_tag(); |
| 88 | 95 | } |
| 96 | + let (shallow_commits, mut shallow_lock) = add_shallow_args(&mut arguments, &self.shallow, repo)?; | |
| 97 | + | |
| 89 | 98 | let mut previous_response = None::<gix_protocol::fetch::Response>; |
| 90 | 99 | let mut round = 1; |
| 91 | 100 | |
| @@ -108,6 +117,7 @@ where · 16 unmodified lines | ||
| 108 | 117 | con.remote.fetch_tags, |
| 109 | 118 | &mut arguments, |
| 110 | 119 | previous_response.as_ref(), |
| 120 | + (self.shallow != Shallow::NoChange).then(|| con.remote.refspecs(remote::Direction::Fetch)), | |
| 111 | 121 | ) { |
| 112 | 122 | Ok(_) if arguments.is_empty() => { |
| 113 | 123 | gix_protocol::indicate_end_of_interaction(&mut con.transport).await.ok(); |
| @@ -146,11 +156,26 @@ where · 32 unmodified lines | ||
| 146 | 156 | if !sideband_all { |
| 147 | 157 | setup_remote_progress(progress, &mut reader); |
| 148 | 158 | } |
| 159 | + previous_response = Some(response); | |
| 149 | 160 | break 'negotiation reader; |
| 150 | 161 | } else { |
| 151 | 162 | previous_response = Some(response); |
| 152 | 163 | } |
| 153 | 164 | }; |
| 165 | + let previous_response = previous_response.expect("knowledge of a pack means a response was received"); | |
| 166 | + if !previous_response.shallow_updates().is_empty() && shallow_lock.is_none() { | |
| 167 | + let reject_shallow_remote = repo | |
| 168 | + .config | |
| 169 | + .resolved | |
| 170 | + .boolean_filter_by_key("clone.rejectShallow", &mut repo.filter_config_section()) | |
| 171 | + .map(|val| Clone::REJECT_SHALLOW.enrich_error(val)) | |
| 172 | + .transpose()? | |
| 173 | + .unwrap_or(false); | |
| 174 | + if reject_shallow_remote { | |
| 175 | + return Err(Error::RejectShallowRemote); | |
| 176 | + } | |
| 177 | + shallow_lock = acquire_shallow_lock(repo).map(Some)?; | |
| 178 | + } | |
| 154 | 179 | |
| 155 | 180 | let options = gix_pack::bundle::write::Options { |
| 156 | 181 | thread_limit: config::index_threads(repo)?, |
| @@ -187,6 +212,12 @@ where · 30 unmodified lines | ||
| 187 | 212 | gix_protocol::indicate_end_of_interaction(&mut con.transport).await.ok(); |
| 188 | 213 | } |
| 189 | 214 | |
| 215 | + if let Some(shallow_lock) = shallow_lock { | |
| 216 | + if !previous_response.shallow_updates().is_empty() { | |
| 217 | + crate::shallow::write(shallow_lock, shallow_commits, previous_response.shallow_updates())?; | |
| 218 | + } | |
| 219 | + } | |
| 220 | + | |
| 190 | 221 | let update_refs = refs::update( |
| 191 | 222 | repo, |
| 192 | 223 | self.reflog_message |
| @@ -221,6 +252,57 @@ where · 28 unmodified lines | ||
| 221 | 252 | } |
| 222 | 253 | } |
| 223 | 254 | |
| 255 | +fn acquire_shallow_lock(repo: &Repository) -> Result<gix_lock::File, Error> { | |
| 256 | + gix_lock::File::acquire_to_update_resource(repo.shallow_file(), gix_lock::acquire::Fail::Immediately, None) | |
| 257 | + .map_err(Into::into) | |
| 258 | +} | |
| 259 | + | |
| 260 | +fn add_shallow_args( | |
| 261 | + args: &mut Arguments, | |
| 262 | + shallow: &Shallow, | |
| 263 | + repo: &Repository, | |
| 264 | +) -> Result<(Option<crate::shallow::Commits>, Option<gix_lock::File>), Error> { | |
| 265 | + let expect_change = *shallow != Shallow::NoChange; | |
| 266 | + let shallow_lock = expect_change.then(|| acquire_shallow_lock(repo)).transpose()?; | |
| 267 | + | |
| 268 | + let shallow_commits = repo.shallow_commits()?; | |
| 269 | + if (shallow_commits.is_some() || expect_change) && !args.can_use_shallow() { | |
| 270 | + // NOTE: if this is an issue, we can always unshallow the repo ourselves. | |
| 271 | + return Err(Error::MissingServerFeature { | |
| 272 | + feature: "shallow", | |
| 273 | + description: "shallow clones need server support to remain shallow, otherwise bigger than expected packs are sent effectively unshallowing the repository", | |
| 274 | + }); | |
| 275 | + } | |
| 276 | + if let Some(shallow_commits) = &shallow_commits { | |
| 277 | + for commit in shallow_commits.iter() { | |
| 278 | + args.shallow(commit); | |
| 279 | + } | |
| 280 | + } | |
| 281 | + match shallow { | |
| 282 | + Shallow::NoChange => {} | |
| 283 | + Shallow::DepthAtRemote(commits) => args.deepen(commits.get() as usize), | |
| 284 | + Shallow::Deepen(commits) => { | |
| 285 | + args.deepen(*commits as usize); | |
| 286 | + args.deepen_relative(); | |
| 287 | + } | |
| 288 | + Shallow::Since { cutoff } => { | |
| 289 | + args.deepen_since(cutoff.seconds_since_unix_epoch as usize); | |
| 290 | + } | |
| 291 | + Shallow::Exclude { | |
| 292 | + remote_refs, | |
| 293 | + since_cutoff, | |
| 294 | + } => { | |
| 295 | + if let Some(cutoff) = since_cutoff { | |
| 296 | + args.deepen_since(cutoff.seconds_since_unix_epoch as usize); | |
| 297 | + } | |
| 298 | + for ref_ in remote_refs { | |
| 299 | + args.deepen_not(ref_.as_ref().as_bstr()); | |
| 300 | + } | |
| 301 | + } | |
| 302 | + } | |
| 303 | + Ok((shallow_commits, shallow_lock)) | |
| 304 | +} | |
| 305 | + | |
| 224 | 306 | fn setup_remote_progress<P>( |
| 225 | 307 | progress: &mut P, |
| 226 | 308 | reader: &mut Box<dyn gix_protocol::transport::client::ExtendedBufRead + Unpin + '_>, |
| @@ -52,6 +52,52 @@ impl Tags { · 51 unmodified lines | ||
| 52 | 52 | } |
| 53 | 53 | } |
| 54 | 54 | |
| 55 | +/// Describe how shallow clones are handled when fetching, with variants defining how the *shallow boundary* is handled. | |
| 56 | +/// | |
| 57 | +/// The *shallow boundary* is a set of commits whose parents are not present in the repository. | |
| 58 | +#[derive(Debug, Clone, PartialEq, Eq)] | |
| 59 | +pub enum Shallow { | |
| 60 | + /// Fetch all changes from the remote without affecting the shallow boundary at all. | |
| 61 | + NoChange, | |
| 62 | + /// Receive update to `depth` commits in the history of the refs to fetch (from the viewpoint of the remote), | |
| 63 | + /// with the value of `1` meaning to receive only the commit a ref is pointing to. | |
| 64 | + /// | |
| 65 | + /// This may update the shallow boundary to increase or decrease the amount of available history. | |
| 66 | + DepthAtRemote(std::num::NonZeroU32), | |
| 67 | + /// Increase the number of commits and thus expand the shallow boundary by `depth` commits as seen from our local | |
| 68 | + /// shallow boundary, with a value of `0` having no effect. | |
| 69 | + Deepen(u32), | |
| 70 | + /// Set the shallow boundary at the `cutoff` time, meaning that there will be no commits beyond that time. | |
| 71 | + Since { | |
| 72 | + /// The date beyond which there will be no history. | |
| 73 | + cutoff: gix_date::Time, | |
| 74 | + }, | |
| 75 | + /// Receive all history excluding all commits reachable from `remote_refs`. These can be long or short | |
| 76 | + /// ref names or tag names. | |
| 77 | + Exclude { | |
| 78 | + /// The ref names to exclude, short or long. Note that ambiguous short names will cause the remote to abort | |
| 79 | + /// without an error message being transferred (because the protocol does not support it) | |
| 80 | + remote_refs: Vec<gix_ref::PartialName>, | |
| 81 | + /// If some, this field has the same meaning as [`Shallow::Since`] which can be used in combination | |
| 82 | + /// with excluded references. | |
| 83 | + since_cutoff: Option<gix_date::Time>, | |
| 84 | + }, | |
| 85 | +} | |
| 86 | + | |
| 87 | +impl Default for Shallow { | |
| 88 | + fn default() -> Self { | |
| 89 | + Shallow::NoChange | |
| 90 | + } | |
| 91 | +} | |
| 92 | + | |
| 93 | +impl Shallow { | |
| 94 | + /// Produce a variant that causes the repository to loose its shallow boundary, effectively by extending it | |
| 95 | + /// beyond all limits. | |
| 96 | + pub fn undo() -> Self { | |
| 97 | + Shallow::DepthAtRemote((i32::MAX as u32).try_into().expect("valid at compile time")) | |
| 98 | + } | |
| 99 | +} | |
| 100 | + | |
| 55 | 101 | /// Information about the relationship between our refspecs, and remote references with their local counterparts. |
| 56 | 102 | #[derive(Default, Debug, Clone)] |
| 57 | 103 | #[cfg(any(feature = "blocking-network-client", feature = "async-network-client"))] |
| @@ -12,8 +12,12 @@ impl Repository { · 11 unmodified lines | ||
| 12 | 12 | .map_or(false, |m| m.is_file() && m.len() > 0) |
| 13 | 13 | } |
| 14 | 14 | |
| 15 | − /// Return a shared list of shallow commits which is updated automatically if the in-memory snapshot has become stale as the underlying file | |
| 16 | − /// on disk has changed. | |
| 15 | + /// Return a shared list of shallow commits which is updated automatically if the in-memory snapshot has become stale | |
| 16 | + /// as the underlying file on disk has changed. | |
| 17 | + /// | |
| 18 | + /// The list of shallow commits represents the shallow boundary, beyond which we are lacking all (parent) commits. | |
| 19 | + /// Note that the list is never empty, as `Ok(None)` is returned in that case indicating the repository | |
| 20 | + /// isn't a shallow clone. | |
| 17 | 21 | /// |
| 18 | 22 | /// The shared list is shared across all clones of this repository. |
| 19 | 23 | pub fn shallow_commits(&self) -> Result<Option<crate::shallow::Commits>, crate::shallow::open::Error> { |
| @@ -4,6 +4,73 @@ pub(crate) type CommitsStorage = · 3 unmodified lines | ||
| 4 | 4 | /// sorted to allow bisecting. |
| 5 | 5 | pub type Commits = gix_features::fs::SharedSnapshot<Vec<gix_hash::ObjectId>>; |
| 6 | 6 | |
| 7 | +/// | |
| 8 | +#[cfg(any(feature = "blocking-network-client", feature = "async-network-client"))] | |
| 9 | +pub mod write { | |
| 10 | + pub(crate) mod function { | |
| 11 | + use crate::shallow::{write::Error, Commits}; | |
| 12 | + use gix_protocol::fetch::response::ShallowUpdate; | |
| 13 | + use std::io::Write; | |
| 14 | + | |
| 15 | + /// Write the previously obtained (possibly non-existing) `shallow_commits` to the shallow `file` | |
| 16 | + /// after applying all `updates`. | |
| 17 | + /// | |
| 18 | + /// If this leaves the list of shallow commits empty, the file is removed. | |
| 19 | + /// | |
| 20 | + /// ### Deviation | |
| 21 | + /// | |
| 22 | + /// Git also prunes the set of shallow commits while writing, we don't until we support some sort of pruning. | |
| 23 | + pub fn write( | |
| 24 | + mut file: gix_lock::File, | |
| 25 | + shallow_commits: Option<Commits>, | |
| 26 | + updates: &[ShallowUpdate], | |
| 27 | + ) -> Result<(), Error> { | |
| 28 | + let mut shallow_commits = shallow_commits.map(|sc| (**sc).to_owned()).unwrap_or_default(); | |
| 29 | + for update in updates { | |
| 30 | + match update { | |
| 31 | + ShallowUpdate::Shallow(id) => { | |
| 32 | + shallow_commits.push(*id); | |
| 33 | + } | |
| 34 | + ShallowUpdate::Unshallow(id) => shallow_commits.retain(|oid| oid != id), | |
| 35 | + } | |
| 36 | + } | |
| 37 | + | |
| 38 | + if shallow_commits.is_empty() { | |
| 39 | + if let Err(err) = std::fs::remove_file(file.resource_path()) { | |
| 40 | + if err.kind() != std::io::ErrorKind::NotFound { | |
| 41 | + return Err(err.into()); | |
| 42 | + } | |
| 43 | + } | |
| 44 | + } else { | |
| 45 | + shallow_commits.sort(); | |
| 46 | + let mut buf = Vec::<u8>::new(); | |
| 47 | + for commit in shallow_commits { | |
| 48 | + commit.write_hex_to(&mut buf).map_err(Error::Io)?; | |
| 49 | + buf.push(b'\n'); | |
| 50 | + } | |
| 51 | + file.write_all(&buf).map_err(Error::Io)?; | |
| 52 | + file.flush()?; | |
| 53 | + } | |
| 54 | + file.commit()?; | |
| 55 | + Ok(()) | |
| 56 | + } | |
| 57 | + } | |
| 58 | + | |
| 59 | + /// The error returned by [`write()`][crate::shallow::write()]. | |
| 60 | + #[derive(Debug, thiserror::Error)] | |
| 61 | + #[allow(missing_docs)] | |
| 62 | + pub enum Error { | |
| 63 | + #[error(transparent)] | |
| 64 | + Commit(#[from] gix_lock::commit::Error<gix_lock::File>), | |
| 65 | + #[error("Could not remove an empty shallow file")] | |
| 66 | + RemoveEmpty(#[from] std::io::Error), | |
| 67 | + #[error("Failed to write object id to shallow file")] | |
| 68 | + Io(std::io::Error), | |
| 69 | + } | |
| 70 | +} | |
| 71 | +#[cfg(any(feature = "blocking-network-client", feature = "async-network-client"))] | |
| 72 | +pub use write::function::write; | |
| 73 | + | |
| 7 | 74 | /// |
| 8 | 75 | pub mod open { |
| 9 | 76 | /// The error returned by [`Repository::shallow_commits()`][crate::Repository::shallow_commits()]. |
| @@ -2,12 +2,203 @@ use crate::{remote, util::restricted}; · 1 unmodified line | ||
| 2 | 2 | |
| 3 | 3 | #[cfg(feature = "blocking-network-client")] |
| 4 | 4 | mod blocking_io { |
| 5 | − use gix::remote::fetch::SpecIndex; | |
| 5 | + use gix::config::tree::{Clone, Core, Init, Key}; | |
| 6 | + use gix::remote::Direction; | |
| 6 | 7 | use gix_object::bstr::ByteSlice; |
| 7 | 8 | use gix_ref::TargetRef; |
| 9 | + use std::sync::atomic::AtomicBool; | |
| 8 | 10 | |
| 9 | 11 | use crate::{remote, util::restricted}; |
| 10 | 12 | |
| 13 | + use crate::util::hex_to_id; | |
| 14 | + use gix::remote::fetch::{Shallow, SpecIndex}; | |
| 15 | + | |
| 16 | + #[test] | |
| 17 | + fn fetch_shallow_no_checkout_then_unshallow() -> crate::Result { | |
| 18 | + let tmp = gix_testtools::tempfile::TempDir::new()?; | |
| 19 | + let called_configure_remote = std::sync::Arc::new(std::sync::atomic::AtomicBool::default()); | |
| 20 | + let remote_name = "special"; | |
| 21 | + let desired_fetch_tags = gix::remote::fetch::Tags::Included; | |
| 22 | + let mut prepare = gix::prepare_clone_bare(remote::repo("base").path(), tmp.path())? | |
| 23 | + .with_remote_name(remote_name)? | |
| 24 | + .configure_remote({ | |
| 25 | + let called_configure_remote = called_configure_remote; | |
| 26 | + move |r| { | |
| 27 | + called_configure_remote.store(true, std::sync::atomic::Ordering::Relaxed); | |
| 28 | + let r = r | |
| 29 | + .with_refspecs(Some("+refs/tags/b-tag:refs/tags/b-tag"), gix::remote::Direction::Fetch)? | |
| 30 | + .with_fetch_tags(desired_fetch_tags); | |
| 31 | + Ok(r) | |
| 32 | + } | |
| 33 | + }) | |
| 34 | + .with_shallow(Shallow::DepthAtRemote(2.try_into().expect("non-zero"))); | |
| 35 | + let (repo, _out) = prepare.fetch_only(gix::progress::Discard, &std::sync::atomic::AtomicBool::default())?; | |
| 36 | + drop(prepare); | |
| 37 | + | |
| 38 | + assert_eq!( | |
| 39 | + repo.shallow_commits()?.expect("shallow").as_slice(), | |
| 40 | + [ | |
| 41 | + hex_to_id("27e71576a6335294aa6073ab767f8b36bdba81d0"), | |
| 42 | + hex_to_id("2d9d136fb0765f2e24c44a0f91984318d580d03b"), | |
| 43 | + hex_to_id("82024b2ef7858273337471cbd1ca1cedbdfd5616"), | |
| 44 | + hex_to_id("b5152869aedeb21e55696bb81de71ea1bb880c85") | |
| 45 | + ], | |
| 46 | + "shallow information is written" | |
| 47 | + ); | |
| 48 | + | |
| 49 | + let shallow_commit_count = repo.head_id()?.ancestors().all()?.count(); | |
| 50 | + let remote = repo.head()?.into_remote(Direction::Fetch).expect("present")?; | |
| 51 | + | |
| 52 | + remote | |
| 53 | + .connect(Direction::Fetch, gix::progress::Discard)? | |
| 54 | + .prepare_fetch(Default::default())? | |
| 55 | + .with_shallow(Shallow::undo()) | |
| 56 | + .receive(&AtomicBool::default())?; | |
| 57 | + | |
| 58 | + assert!(repo.shallow_commits()?.is_none(), "the repo isn't shallow anymore"); | |
| 59 | + assert!( | |
| 60 | + !repo.is_shallow(), | |
| 61 | + "both methods agree - if there are no shallow commits, it shouldn't think the repo is shallow" | |
| 62 | + ); | |
| 63 | + assert!( | |
| 64 | + repo.head_id()?.ancestors().all()?.count() > shallow_commit_count, | |
| 65 | + "there are more commits now as the history is complete" | |
| 66 | + ); | |
| 67 | + | |
| 68 | + Ok(()) | |
| 69 | + } | |
| 70 | + | |
| 71 | + #[test] | |
| 72 | + fn from_shallow_prohibited_with_option() -> crate::Result { | |
| 73 | + let tmp = gix_testtools::tempfile::TempDir::new()?; | |
| 74 | + let err = gix::clone::PrepareFetch::new( | |
| 75 | + remote::repo("base.shallow").path(), | |
| 76 | + tmp.path(), | |
| 77 | + gix::create::Kind::Bare, | |
| 78 | + Default::default(), | |
| 79 | + gix::open::Options::isolated().config_overrides([Clone::REJECT_SHALLOW.validated_assignment_fmt(&true)?]), | |
| 80 | + )? | |
| 81 | + .fetch_only(gix::progress::Discard, &std::sync::atomic::AtomicBool::default()) | |
| 82 | + .unwrap_err(); | |
| 83 | + assert!( | |
| 84 | + matches!( | |
| 85 | + err, | |
| 86 | + gix::clone::fetch::Error::Fetch(gix::remote::fetch::Error::RejectShallowRemote) | |
| 87 | + ), | |
| 88 | + "we can avoid fetching from remotes with this setting" | |
| 89 | + ); | |
| 90 | + Ok(()) | |
| 91 | + } | |
| 92 | + | |
| 93 | + #[test] | |
| 94 | + fn from_shallow_allowed_by_default() -> crate::Result { | |
| 95 | + let tmp = gix_testtools::tempfile::TempDir::new()?; | |
| 96 | + let (repo, _change) = gix::prepare_clone_bare(remote::repo("base.shallow").path(), tmp.path())? | |
| 97 | + .fetch_only(gix::progress::Discard, &std::sync::atomic::AtomicBool::default())?; | |
| 98 | + assert_eq!( | |
| 99 | + repo.shallow_commits()?.expect("present").as_slice(), | |
| 100 | + vec![ | |
| 101 | + hex_to_id("2d9d136fb0765f2e24c44a0f91984318d580d03b"), | |
| 102 | + hex_to_id("dfd0954dabef3b64f458321ef15571cc1a46d552"), | |
| 103 | + hex_to_id("dfd0954dabef3b64f458321ef15571cc1a46d552"), | |
| 104 | + ] | |
| 105 | + ); | |
| 106 | + Ok(()) | |
| 107 | + } | |
| 108 | + | |
| 109 | + #[test] | |
| 110 | + fn from_non_shallow_then_deepen_then_deepen_since_to_unshallow() -> crate::Result { | |
| 111 | + let tmp = gix_testtools::tempfile::TempDir::new()?; | |
| 112 | + let (repo, _change) = gix::prepare_clone_bare(remote::repo("base").path(), tmp.path())? | |
| 113 | + .with_shallow(Shallow::DepthAtRemote(2.try_into()?)) | |
| 114 | + .fetch_only(gix::progress::Discard, &std::sync::atomic::AtomicBool::default())?; | |
| 115 | + | |
| 116 | + assert!(repo.is_shallow()); | |
| 117 | + assert_eq!( | |
| 118 | + repo.shallow_commits()?.expect("present").as_slice(), | |
| 119 | + vec![ | |
| 120 | + hex_to_id("2d9d136fb0765f2e24c44a0f91984318d580d03b"), | |
| 121 | + hex_to_id("dfd0954dabef3b64f458321ef15571cc1a46d552"), | |
| 122 | + ] | |
| 123 | + ); | |
| 124 | + | |
| 125 | + let shallow_commit_count = repo.head_id()?.ancestors().all()?.count(); | |
| 126 | + | |
| 127 | + let remote = repo.head()?.into_remote(Direction::Fetch).expect("present")?; | |
| 128 | + remote | |
| 129 | + .connect(Direction::Fetch, gix::progress::Discard)? | |
| 130 | + .prepare_fetch(Default::default())? | |
| 131 | + .with_shallow(Shallow::Deepen(1)) | |
| 132 | + .receive(&AtomicBool::default())?; | |
| 133 | + | |
| 134 | + assert_eq!( | |
| 135 | + repo.shallow_commits()?.expect("present").as_slice(), | |
| 136 | + vec![ | |
| 137 | + hex_to_id("27e71576a6335294aa6073ab767f8b36bdba81d0"), | |
| 138 | + hex_to_id("82024b2ef7858273337471cbd1ca1cedbdfd5616"), | |
| 139 | + hex_to_id("b5152869aedeb21e55696bb81de71ea1bb880c85"), | |
| 140 | + ], | |
| 141 | + "the shallow boundary was changed" | |
| 142 | + ); | |
| 143 | + assert!( | |
| 144 | + repo.head_id()?.ancestors().all()?.count() > shallow_commit_count, | |
| 145 | + "there are more commits now as the history was deepened" | |
| 146 | + ); | |
| 147 | + | |
| 148 | + let shallow_commit_count = repo.head_id()?.ancestors().all()?.count(); | |
| 149 | + remote | |
| 150 | + .connect(Direction::Fetch, gix::progress::Discard)? | |
| 151 | + .prepare_fetch(Default::default())? | |
| 152 | + .with_shallow(Shallow::Since { | |
| 153 | + cutoff: gix::date::Time::new(1112354053, 0), | |
| 154 | + }) | |
| 155 | + .receive(&AtomicBool::default())?; | |
| 156 | + | |
| 157 | + assert!( | |
| 158 | + !repo.is_shallow(), | |
| 159 | + "the cutoff date is before the first commit, effectively unshallowing" | |
| 160 | + ); | |
| 161 | + assert!( | |
| 162 | + repo.head_id()?.ancestors().all()?.count() > shallow_commit_count, | |
| 163 | + "there is even more commits than previously" | |
| 164 | + ); | |
| 165 | + Ok(()) | |
| 166 | + } | |
| 167 | + | |
| 168 | + #[test] | |
| 169 | + fn from_non_shallow_by_deepen_exclude_then_deepen_to_unshallow() -> crate::Result { | |
| 170 | + let tmp = gix_testtools::tempfile::TempDir::new()?; | |
| 171 | + let excluded_leaf_refs = ["g", "h", "j"]; | |
| 172 | + let (repo, _change) = gix::prepare_clone_bare(remote::repo("base").path(), tmp.path())? | |
| 173 | + .with_shallow(Shallow::Exclude { | |
| 174 | + remote_refs: excluded_leaf_refs | |
| 175 | + .into_iter() | |
| 176 | + .map(|n| n.try_into().expect("valid")) | |
| 177 | + .collect(), | |
| 178 | + since_cutoff: None, | |
| 179 | + }) | |
| 180 | + .fetch_only(gix::progress::Discard, &std::sync::atomic::AtomicBool::default())?; | |
| 181 | + | |
| 182 | + assert!(repo.is_shallow()); | |
| 183 | + assert_eq!( | |
| 184 | + repo.shallow_commits()?.expect("present").as_slice(), | |
| 185 | + vec![ | |
| 186 | + hex_to_id("27e71576a6335294aa6073ab767f8b36bdba81d0"), | |
| 187 | + hex_to_id("82024b2ef7858273337471cbd1ca1cedbdfd5616"), | |
| 188 | + ] | |
| 189 | + ); | |
| 190 | + | |
| 191 | + let remote = repo.head()?.into_remote(Direction::Fetch).expect("present")?; | |
| 192 | + remote | |
| 193 | + .connect(Direction::Fetch, gix::progress::Discard)? | |
| 194 | + .prepare_fetch(Default::default())? | |
| 195 | + .with_shallow(Shallow::Deepen(2)) | |
| 196 | + .receive(&AtomicBool::default())?; | |
| 197 | + | |
| 198 | + assert!(!repo.is_shallow(), "one is just enough to unshallow it"); | |
| 199 | + Ok(()) | |
| 200 | + } | |
| 201 | + | |
| 11 | 202 | #[test] |
| 12 | 203 | fn fetch_only_with_configuration() -> crate::Result { |
| 13 | 204 | let tmp = gix_testtools::tempfile::TempDir::new()?; |
| @@ -20,8 +211,8 @@ mod blocking_io { · 6 unmodified lines | ||
| 20 | 211 | gix::create::Kind::Bare, |
| 21 | 212 | Default::default(), |
| 22 | 213 | gix::open::Options::isolated().config_overrides([ |
| 23 | − "init.defaultBranch=unused-as-overridden-by-remote", | |
| 24 | − "core.logAllRefUpdates", | |
| 214 | + Init::DEFAULT_BRANCH.validated_assignment_fmt(&"unused-as-overridden-by-remote")?, | |
| 215 | + Core::LOG_ALL_REF_UPDATES.logical_name().into(), | |
| 25 | 216 | // missing user and email is acceptable in this special case, i.e. `git` also doesn't mind filling it in. |
| 26 | 217 | ]), |
| 27 | 218 | )? |
| @@ -1,3 +1,3 @@ | ||
| 1 | 1 | version https://git-lfs.github.com/spec/v1 |
| 2 | −oid sha256:f0ffd6adef1a5755c2013dd7e793ca8e239308448872681a36a2216ea56623b2 | |
| 3 | −size 32032 | |
| 2 | +oid sha256:dceeedc3ee706632030f8317f2245c327456ec58a6ac690d09099557a5aa23b0 | |
| 3 | +size 32336 | |
| @@ -1,7 +1,7 @@ | ||
| 1 | 1 | set -eu -o pipefail |
| 2 | 2 | |
| 3 | 3 | # IMPORTANT: keep this repo small as it's used for writes, hence will be executed for each writer! |
| 4 | −git clone --bare "${1:?First argument is the complex base repo from make_remote_repos.sh/base}" base | |
| 4 | +git clone ${2:-} --bare "${1:?First argument is the complex base repo from make_remote_repos.sh/base}" base | |
| 5 | 5 | |
| 6 | 6 | git clone --shared base clone-as-base-with-changes |
| 7 | 7 | (cd clone-as-base-with-changes |
| @@ -107,6 +107,9 @@ git init base · 106 unmodified lines | ||
| 107 | 107 | git branch -D tmp |
| 108 | 108 | ) |
| 109 | 109 | |
| 110 | +git clone --shared --depth 2 file://$PWD/base base.shallow | |
| 111 | + | |
| 112 | + | |
| 110 | 113 | git clone --shared base clone |
| 111 | 114 | (cd clone |
| 112 | 115 | git remote add myself . |
| @@ -591,10 +591,7 @@ mod track_rewrites { · 590 unmodified lines | ||
| 591 | 591 | out.num_similarity_checks_skipped_for_rename_tracking_due_to_limit, 0, |
| 592 | 592 | "no limit configured" |
| 593 | 593 | ); |
| 594 | − assert_eq!( | |
| 595 | − out.num_similarity_checks_skipped_for_copy_tracking_due_to_limit, | |
| 596 | − if cfg!(windows) { 63 } else { 57 } | |
| 597 | − ); | |
| 594 | + assert_eq!(out.num_similarity_checks_skipped_for_copy_tracking_due_to_limit, 57); | |
| 598 | 595 | |
| 599 | 596 | Ok(()) |
| 600 | 597 | } |
| @@ -1,3 +1,15 @@ | ||
| 1 | +mod shallow { | |
| 2 | + use gix::remote::fetch::Shallow; | |
| 3 | + | |
| 4 | + #[test] | |
| 5 | + fn undo() { | |
| 6 | + assert_eq!( | |
| 7 | + Shallow::undo(), | |
| 8 | + Shallow::DepthAtRemote(2147483647u32.try_into().expect("known at compile time")) | |
| 9 | + ); | |
| 10 | + } | |
| 11 | +} | |
| 12 | + | |
| 1 | 13 | #[cfg(any(feature = "blocking-network-client", feature = "async-network-client-async-std"))] |
| 2 | 14 | mod blocking_and_async_io { |
| 3 | 15 | use std::sync::atomic::AtomicBool; |