History

feat: shallow support for `clone` operations.

Sebastian Thiel committed 3 years ago

Commit 4e89c191 parent 397ed9017 files · +479 −21Browse files

Unified · Split

17 files
  1. crate-status.md +5 −1
  2. gix/src/clone/fetch/mod.rs +7 −0
  3. gix/src/clone/mod.rs +5 −1
  4. gix/src/config/tree/sections/clone.rs +3 −1
  5. gix/src/remote/connection/fetch/error.rs +15 −0
  6. gix/src/remote/connection/fetch/mod.rs +10 −0
  7. gix/src/remote/connection/fetch/negotiate.rs +17 −3
  8. gix/src/remote/connection/fetch/receive_pack.rs +85 −3
  9. gix/src/remote/fetch.rs +46 −0
  10. gix/src/repository/shallow.rs +6 −2
  11. gix/src/shallow.rs +67 −0
  12. gix/tests/clone/mod.rs +194 −3
  13. gix/tests/fixtures/generated-archives/make_diff_repo.tar.xz +2 −2
  14. gix/tests/fixtures/make_fetch_repos.sh +1 −1
  15. gix/tests/fixtures/make_remote_repos.sh +3 −0
  16. gix/tests/object/tree/diff.rs +1 −4
  17. gix/tests/remote/fetch.rs +12 −0
crate-status.md+5−1
@@ -637,10 +637,14 @@ See its [README.md](https://github.com/Byron/gitoxide/blob/main/gix-lock/README. · 636 unmodified lines
637637 * [x] find remote itself
638638 - [ ] respect `branch.<name>.merge` in the returned remote.
639639 * **remotes**
640− * [ ] clone
640+ * [x] clone
641641 * [ ] shallow
642+ * [ ] include-tags when shallow is used (needs separate fetch)
643+ * [ ] prune non-existing shallow commits
642644 * [ ] [bundles](https://git-scm.com/docs/git-bundle)
643645 * [x] fetch
646+ * [ ] 'ref-in-want'
647+ * [ ] standard negotiation algorithms (right now we only have a 'naive' one)
644648 * [ ] push
645649 * [x] ls-refs
646650 * [x] ls-refs with ref-spec filter
gix/src/clone/fetch/mod.rs+7−0
@@ -121,6 +121,7 @@ impl PrepareFetch { · 120 unmodified lines
121121 .with_reflog_message(RefLogMessage::Override {
122122 message: reflog_message.clone(),
123123 })
124+ .with_shallow(self.shallow.clone())
124125 .receive(should_interrupt)?;
125126
126127 util::append_config_to_repo_config(repo, config);
@@ -184,6 +185,12 @@ impl PrepareFetch { · 57 unmodified lines
184185 self.remote_name = Some(crate::remote::name::validated(name)?);
185186 Ok(self)
186187 }
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+ }
187194 }
188195
189196 /// Consumption
gix/src/clone/mod.rs+5−1
@@ -1,7 +1,7 @@
11 #![allow(clippy::result_large_err)]
22 use std::convert::TryInto;
33
4−use crate::{bstr::BString, config::tree::gitoxide};
4+use crate::{bstr::BString, config::tree::gitoxide, remote};
55
66 type ConfigureRemoteFn =
77 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
2222 /// The url to clone from
2323 #[cfg_attr(not(feature = "blocking-network-client"), allow(dead_code))]
2424 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,
2528 }
2629
2730 /// The error returned by [`PrepareFetch::new()`].
@@ -99,6 +102,7 @@ impl PrepareFetch { · 71 unmodified lines
99102 repo: Some(repo),
100103 remote_name: None,
101104 configure_remote: None,
105+ shallow: remote::fetch::Shallow::NoChange,
102106 })
103107 }
104108 }
gix/src/config/tree/sections/clone.rs+3−1
@@ -7,6 +7,8 @@ impl Clone { · 6 unmodified lines
77 /// The `clone.defaultRemoteName` key.
88 pub const DEFAULT_REMOTE_NAME: keys::RemoteName =
99 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);
1012 }
1113
1214 impl Section for Clone {
@@ -15,6 +17,6 @@ impl Section for Clone { · 2 unmodified lines
1517 }
1618
1719 fn keys(&self) -> &[&dyn Key] {
18− &[&Self::DEFAULT_REMOTE_NAME]
20+ &[&Self::DEFAULT_REMOTE_NAME, &Self::REJECT_SHALLOW]
1921 }
2022 }
gix/src/remote/connection/fetch/error.rs+15−0
@@ -28,6 +28,21 @@ pub enum Error { · 27 unmodified lines
2828 path: std::path::PathBuf,
2929 source: std::io::Error,
3030 },
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,
3146 }
3247
3348 impl gix_protocol::transport::IsSpuriousError for Error {
gix/src/remote/connection/fetch/mod.rs+10−0
@@ -149,6 +149,7 @@ where · 148 unmodified lines
149149 dry_run: DryRun::No,
150150 reflog_message: None,
151151 write_packed_refs: WritePackedRefs::Never,
152+ shallow: Default::default(),
152153 })
153154 }
154155 }
@@ -179,6 +180,7 @@ where · 24 unmodified lines
179180 dry_run: DryRun,
180181 reflog_message: Option<RefLogMessage>,
181182 write_packed_refs: WritePackedRefs,
183+ shallow: remote::fetch::Shallow,
182184 }
183185
184186 /// Builder
@@ -212,6 +214,14 @@ where · 27 unmodified lines
212214 self.reflog_message = reflog_message.into();
213215 self
214216 }
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+ }
215225 }
216226
217227 impl<'remote, 'repo, T, P> Drop for Prepare<'remote, 'repo, T, P>
gix/src/remote/connection/fetch/negotiate.rs+17−3
@@ -1,3 +1,5 @@
1+use gix_refspec::RefSpec;
2+
13 /// The way the negotiation is performed
24 #[derive(Copy, Clone)]
35 pub(crate) enum Algorithm {
@@ -16,6 +18,7 @@ pub enum Error { · 12 unmodified lines
1618 /// Negotiate one round with `algo` by looking at `ref_map` and adjust `arguments` to contain the haves and wants.
1719 /// If this is not the first round, the `previous_response` is set with the last recorded server response.
1820 /// Returns `true` if the negotiation is done from our side so the server won't keep asking.
21+#[allow(clippy::too_many_arguments)]
1922 pub(crate) fn one_round(
2023 algo: Algorithm,
2124 round: usize,
@@ -24,13 +27,15 @@ pub(crate) fn one_round( · 2 unmodified lines
2427 fetch_tags: crate::remote::fetch::Tags,
2528 arguments: &mut gix_protocol::fetch::Arguments,
2629 _previous_response: Option<&gix_protocol::fetch::Response>,
30+ wants_shallow_change: Option<&[RefSpec]>,
2731 ) -> Result<bool, Error> {
2832 let tag_refspec_to_ignore = fetch_tags
2933 .to_refspec()
3034 .filter(|_| matches!(fetch_tags, crate::remote::fetch::Tags::Included));
35+ let non_wildcard_specs_only = wants_shallow_change;
3136 match algo {
3237 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.");
3439 let mut has_missing_tracking_branch = false;
3540 for mapping in &ref_map.mappings {
3641 if tag_refspec_to_ignore.map_or(false, |tag_spec| {
@@ -42,6 +47,14 @@ pub(crate) fn one_round( · 5 unmodified lines
4247 }) {
4348 continue;
4449 }
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+ }
4558 let have_id = mapping.local.as_ref().and_then(|name| {
4659 repo.find_reference(name)
4760 .ok()
@@ -50,7 +63,7 @@ pub(crate) fn one_round( · 2 unmodified lines
5063 match have_id {
5164 Some(have_id) => {
5265 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() {
5467 arguments.want(want_id);
5568 arguments.have(have_id);
5669 }
@@ -65,10 +78,11 @@ pub(crate) fn one_round( · 8 unmodified lines
6578 }
6679 }
6780
68− if has_missing_tracking_branch {
81+ if has_missing_tracking_branch || (wants_shallow_change.is_some() && arguments.is_empty()) {
6982 if let Ok(Some(r)) = repo.head_ref() {
7083 if let Some(id) = r.target().try_id() {
7184 arguments.have(id);
85+ arguments.want(id);
7286 }
7387 }
7488 }
gix/src/remote/connection/fetch/receive_pack.rs+85−3
@@ -1,16 +1,18 @@
11 use std::sync::atomic::AtomicBool;
22
33 use gix_odb::FindExt;
4+use gix_protocol::fetch::Arguments;
45 use gix_protocol::transport::client::Transport;
56
7+use crate::config::tree::Clone;
68 use crate::{
79 remote,
810 remote::{
911 connection::fetch::config,
1012 fetch,
11− fetch::{negotiate, refs, Error, Outcome, Prepare, ProgressId, RefLogMessage, Status},
13+ fetch::{negotiate, refs, Error, Outcome, Prepare, ProgressId, RefLogMessage, Shallow, Status},
1214 },
13− Progress,
15+ Progress, Repository,
1416 };
1517
1618 impl<'remote, 'repo, T, P> Prepare<'remote, 'repo, T, P>
@@ -82,10 +84,17 @@ where · 65 unmodified lines
8284 let mut arguments = gix_protocol::fetch::Arguments::new(protocol_version, fetch_features);
8385 if matches!(con.remote.fetch_tags, crate::remote::fetch::Tags::Included) {
8486 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+ });
8693 }
8794 arguments.use_include_tag();
8895 }
96+ let (shallow_commits, mut shallow_lock) = add_shallow_args(&mut arguments, &self.shallow, repo)?;
97+
8998 let mut previous_response = None::<gix_protocol::fetch::Response>;
9099 let mut round = 1;
91100
@@ -108,6 +117,7 @@ where · 16 unmodified lines
108117 con.remote.fetch_tags,
109118 &mut arguments,
110119 previous_response.as_ref(),
120+ (self.shallow != Shallow::NoChange).then(|| con.remote.refspecs(remote::Direction::Fetch)),
111121 ) {
112122 Ok(_) if arguments.is_empty() => {
113123 gix_protocol::indicate_end_of_interaction(&mut con.transport).await.ok();
@@ -146,11 +156,26 @@ where · 32 unmodified lines
146156 if !sideband_all {
147157 setup_remote_progress(progress, &mut reader);
148158 }
159+ previous_response = Some(response);
149160 break 'negotiation reader;
150161 } else {
151162 previous_response = Some(response);
152163 }
153164 };
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+ }
154179
155180 let options = gix_pack::bundle::write::Options {
156181 thread_limit: config::index_threads(repo)?,
@@ -187,6 +212,12 @@ where · 30 unmodified lines
187212 gix_protocol::indicate_end_of_interaction(&mut con.transport).await.ok();
188213 }
189214
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+
190221 let update_refs = refs::update(
191222 repo,
192223 self.reflog_message
@@ -221,6 +252,57 @@ where · 28 unmodified lines
221252 }
222253 }
223254
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+
224306 fn setup_remote_progress<P>(
225307 progress: &mut P,
226308 reader: &mut Box<dyn gix_protocol::transport::client::ExtendedBufRead + Unpin + '_>,
gix/src/remote/fetch.rs+46−0
@@ -52,6 +52,52 @@ impl Tags { · 51 unmodified lines
5252 }
5353 }
5454
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+
55101 /// Information about the relationship between our refspecs, and remote references with their local counterparts.
56102 #[derive(Default, Debug, Clone)]
57103 #[cfg(any(feature = "blocking-network-client", feature = "async-network-client"))]
gix/src/repository/shallow.rs+6−2
@@ -12,8 +12,12 @@ impl Repository { · 11 unmodified lines
1212 .map_or(false, |m| m.is_file() && m.len() > 0)
1313 }
1414
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.
1721 ///
1822 /// The shared list is shared across all clones of this repository.
1923 pub fn shallow_commits(&self) -> Result<Option<crate::shallow::Commits>, crate::shallow::open::Error> {
gix/src/shallow.rs+67−0
@@ -4,6 +4,73 @@ pub(crate) type CommitsStorage = · 3 unmodified lines
44 /// sorted to allow bisecting.
55 pub type Commits = gix_features::fs::SharedSnapshot<Vec<gix_hash::ObjectId>>;
66
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+
774 ///
875 pub mod open {
976 /// The error returned by [`Repository::shallow_commits()`][crate::Repository::shallow_commits()].
gix/tests/clone/mod.rs+194−3
@@ -2,12 +2,203 @@ use crate::{remote, util::restricted}; · 1 unmodified line
22
33 #[cfg(feature = "blocking-network-client")]
44 mod blocking_io {
5− use gix::remote::fetch::SpecIndex;
5+ use gix::config::tree::{Clone, Core, Init, Key};
6+ use gix::remote::Direction;
67 use gix_object::bstr::ByteSlice;
78 use gix_ref::TargetRef;
9+ use std::sync::atomic::AtomicBool;
810
911 use crate::{remote, util::restricted};
1012
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+
11202 #[test]
12203 fn fetch_only_with_configuration() -> crate::Result {
13204 let tmp = gix_testtools::tempfile::TempDir::new()?;
@@ -20,8 +211,8 @@ mod blocking_io { · 6 unmodified lines
20211 gix::create::Kind::Bare,
21212 Default::default(),
22213 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(),
25216 // missing user and email is acceptable in this special case, i.e. `git` also doesn't mind filling it in.
26217 ]),
27218 )?
gix/tests/fixtures/generated-archives/make_diff_repo.tar.xz+2−2
@@ -1,3 +1,3 @@
11 version https://git-lfs.github.com/spec/v1
2−oid sha256:f0ffd6adef1a5755c2013dd7e793ca8e239308448872681a36a2216ea56623b2
3−size 32032
2+oid sha256:dceeedc3ee706632030f8317f2245c327456ec58a6ac690d09099557a5aa23b0
3+size 32336
gix/tests/fixtures/make_fetch_repos.sh+1−1
@@ -1,7 +1,7 @@
11 set -eu -o pipefail
22
33 # 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
55
66 git clone --shared base clone-as-base-with-changes
77 (cd clone-as-base-with-changes
gix/tests/fixtures/make_remote_repos.sh+3−0
@@ -107,6 +107,9 @@ git init base · 106 unmodified lines
107107 git branch -D tmp
108108 )
109109
110+git clone --shared --depth 2 file://$PWD/base base.shallow
111+
112+
110113 git clone --shared base clone
111114 (cd clone
112115 git remote add myself .
gix/tests/object/tree/diff.rs+1−4
@@ -591,10 +591,7 @@ mod track_rewrites { · 590 unmodified lines
591591 out.num_similarity_checks_skipped_for_rename_tracking_due_to_limit, 0,
592592 "no limit configured"
593593 );
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);
598595
599596 Ok(())
600597 }
gix/tests/remote/fetch.rs+12−0
@@ -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+
113 #[cfg(any(feature = "blocking-network-client", feature = "async-network-client-async-std"))]
214 mod blocking_and_async_io {
315 use std::sync::atomic::AtomicBool;