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
637 * [x] find remote itself637 * [x] find remote itself
638 - [ ] respect `branch.<name>.merge` in the returned remote.638 - [ ] respect `branch.<name>.merge` in the returned remote.
639 * **remotes** 639 * **remotes**
640 * [ ] clone 640 * [x] clone
641 * [ ] shallow641 * [ ] shallow
642 * [ ] include-tags when shallow is used (needs separate fetch)
643 * [ ] prune non-existing shallow commits
642 * [ ] [bundles](https://git-scm.com/docs/git-bundle)644 * [ ] [bundles](https://git-scm.com/docs/git-bundle)
643 * [x] fetch645 * [x] fetch
646 * [ ] 'ref-in-want'
647 * [ ] standard negotiation algorithms (right now we only have a 'naive' one)
644 * [ ] push648 * [ ] push
645 * [x] ls-refs649 * [x] ls-refs
646 * [x] ls-refs with ref-spec filter650 * [x] ls-refs with ref-spec filter
gix/src/clone/fetch/mod.rs+7−0
@@ -121,6 +121,7 @@ impl PrepareFetch { · 120 unmodified lines
121 .with_reflog_message(RefLogMessage::Override {121 .with_reflog_message(RefLogMessage::Override {
122 message: reflog_message.clone(),122 message: reflog_message.clone(),
123 })123 })
124 .with_shallow(self.shallow.clone())
124 .receive(should_interrupt)?;125 .receive(should_interrupt)?;
125 126
126 util::append_config_to_repo_config(repo, config);127 util::append_config_to_repo_config(repo, config);
@@ -184,6 +185,12 @@ impl PrepareFetch { · 57 unmodified lines
184 self.remote_name = Some(crate::remote::name::validated(name)?);185 self.remote_name = Some(crate::remote::name::validated(name)?);
185 Ok(self)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/// Consumption196/// Consumption
gix/src/clone/mod.rs+5−1
@@ -1,7 +1,7 @@
1#![allow(clippy::result_large_err)]1#![allow(clippy::result_large_err)]
2use std::convert::TryInto;2use std::convert::TryInto;
3 3
4use crate::{bstr::BString, config::tree::gitoxide};4use crate::{bstr::BString, config::tree::gitoxide, remote};
5 5
6type ConfigureRemoteFn =6type ConfigureRemoteFn =
7 Box<dyn FnMut(crate::Remote<'_>) -> Result<crate::Remote<'_>, Box<dyn std::error::Error + Send + Sync>>>;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 /// The url to clone from22 /// The url to clone from
23 #[cfg_attr(not(feature = "blocking-network-client"), allow(dead_code))]23 #[cfg_attr(not(feature = "blocking-network-client"), allow(dead_code))]
24 url: gix_url::Url,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/// The error returned by [`PrepareFetch::new()`].30/// The error returned by [`PrepareFetch::new()`].
@@ -99,6 +102,7 @@ impl PrepareFetch { · 71 unmodified lines
99 repo: Some(repo),102 repo: Some(repo),
100 remote_name: None,103 remote_name: None,
101 configure_remote: None,104 configure_remote: None,
105 shallow: remote::fetch::Shallow::NoChange,
102 })106 })
103 }107 }
104}108}
gix/src/config/tree/sections/clone.rs+3−1
@@ -7,6 +7,8 @@ impl Clone { · 6 unmodified lines
7 /// The `clone.defaultRemoteName` key.7 /// The `clone.defaultRemoteName` key.
8 pub const DEFAULT_REMOTE_NAME: keys::RemoteName =8 pub const DEFAULT_REMOTE_NAME: keys::RemoteName =
9 keys::RemoteName::new_remote_name("defaultRemoteName", &config::Tree::CLONE);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
12impl Section for Clone {14impl Section for Clone {
@@ -15,6 +17,6 @@ impl Section for Clone { · 2 unmodified lines
15 }17 }
16 18
17 fn keys(&self) -> &[&dyn Key] {19 fn keys(&self) -> &[&dyn Key] {
18 &[&Self::DEFAULT_REMOTE_NAME]20 &[&Self::DEFAULT_REMOTE_NAME, &Self::REJECT_SHALLOW]
19 }21 }
20}22}
gix/src/remote/connection/fetch/error.rs+15−0
@@ -28,6 +28,21 @@ pub enum Error { · 27 unmodified lines
28 path: std::path::PathBuf,28 path: std::path::PathBuf,
29 source: std::io::Error,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
33impl gix_protocol::transport::IsSpuriousError for Error {48impl gix_protocol::transport::IsSpuriousError for Error {
gix/src/remote/connection/fetch/mod.rs+10−0
@@ -149,6 +149,7 @@ where · 148 unmodified lines
149 dry_run: DryRun::No,149 dry_run: DryRun::No,
150 reflog_message: None,150 reflog_message: None,
151 write_packed_refs: WritePackedRefs::Never,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 dry_run: DryRun,180 dry_run: DryRun,
180 reflog_message: Option<RefLogMessage>,181 reflog_message: Option<RefLogMessage>,
181 write_packed_refs: WritePackedRefs,182 write_packed_refs: WritePackedRefs,
183 shallow: remote::fetch::Shallow,
182}184}
183 185
184/// Builder186/// Builder
@@ -212,6 +214,14 @@ where · 27 unmodified lines
212 self.reflog_message = reflog_message.into();214 self.reflog_message = reflog_message.into();
213 self215 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
217impl<'remote, 'repo, T, P> Drop for Prepare<'remote, 'repo, T, P>227impl<'remote, 'repo, T, P> Drop for Prepare<'remote, 'repo, T, P>
gix/src/remote/connection/fetch/negotiate.rs+17−3
@@ -1,3 +1,5 @@
1use gix_refspec::RefSpec;
2
1/// The way the negotiation is performed3/// The way the negotiation is performed
2#[derive(Copy, Clone)]4#[derive(Copy, Clone)]
3pub(crate) enum Algorithm {5pub(crate) enum Algorithm {
@@ -16,6 +18,7 @@ pub enum Error { · 12 unmodified lines
16/// Negotiate one round with `algo` by looking at `ref_map` and adjust `arguments` to contain the haves and wants.18/// Negotiate one round with `algo` by looking at `ref_map` and adjust `arguments` to contain the haves and wants.
17/// If this is not the first round, the `previous_response` is set with the last recorded server response.19/// If this is not the first round, the `previous_response` is set with the last recorded server response.
18/// Returns `true` if the negotiation is done from our side so the server won't keep asking.20/// Returns `true` if the negotiation is done from our side so the server won't keep asking.
21#[allow(clippy::too_many_arguments)]
19pub(crate) fn one_round(22pub(crate) fn one_round(
20 algo: Algorithm,23 algo: Algorithm,
21 round: usize,24 round: usize,
@@ -24,13 +27,15 @@ pub(crate) fn one_round( · 2 unmodified lines
24 fetch_tags: crate::remote::fetch::Tags,27 fetch_tags: crate::remote::fetch::Tags,
25 arguments: &mut gix_protocol::fetch::Arguments,28 arguments: &mut gix_protocol::fetch::Arguments,
26 _previous_response: Option<&gix_protocol::fetch::Response>,29 _previous_response: Option<&gix_protocol::fetch::Response>,
30 wants_shallow_change: Option<&[RefSpec]>,
27) -> Result<bool, Error> {31) -> Result<bool, Error> {
28 let tag_refspec_to_ignore = fetch_tags32 let tag_refspec_to_ignore = fetch_tags
29 .to_refspec()33 .to_refspec()
30 .filter(|_| matches!(fetch_tags, crate::remote::fetch::Tags::Included));34 .filter(|_| matches!(fetch_tags, crate::remote::fetch::Tags::Included));
35 let non_wildcard_specs_only = wants_shallow_change;
31 match algo {36 match algo {
32 Algorithm::Naive => {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 let mut has_missing_tracking_branch = false;39 let mut has_missing_tracking_branch = false;
35 for mapping in &ref_map.mappings {40 for mapping in &ref_map.mappings {
36 if tag_refspec_to_ignore.map_or(false, |tag_spec| {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 continue;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 let have_id = mapping.local.as_ref().and_then(|name| {58 let have_id = mapping.local.as_ref().and_then(|name| {
46 repo.find_reference(name)59 repo.find_reference(name)
47 .ok()60 .ok()
@@ -50,7 +63,7 @@ pub(crate) fn one_round( · 2 unmodified lines
50 match have_id {63 match have_id {
51 Some(have_id) => {64 Some(have_id) => {
52 if let Some(want_id) = mapping.remote.as_id() {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 arguments.want(want_id);67 arguments.want(want_id);
55 arguments.have(have_id);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 if let Ok(Some(r)) = repo.head_ref() {82 if let Ok(Some(r)) = repo.head_ref() {
70 if let Some(id) = r.target().try_id() {83 if let Some(id) = r.target().try_id() {
71 arguments.have(id);84 arguments.have(id);
85 arguments.want(id);
72 }86 }
73 }87 }
74 }88 }
gix/src/remote/connection/fetch/receive_pack.rs+85−3
@@ -1,16 +1,18 @@
1use std::sync::atomic::AtomicBool;1use std::sync::atomic::AtomicBool;
2 2
3use gix_odb::FindExt;3use gix_odb::FindExt;
4use gix_protocol::fetch::Arguments;
4use gix_protocol::transport::client::Transport;5use gix_protocol::transport::client::Transport;
5 6
7use crate::config::tree::Clone;
6use crate::{8use crate::{
7 remote,9 remote,
8 remote::{10 remote::{
9 connection::fetch::config,11 connection::fetch::config,
10 fetch,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
16impl<'remote, 'repo, T, P> Prepare<'remote, 'repo, T, P>18impl<'remote, 'repo, T, P> Prepare<'remote, 'repo, T, P>
@@ -82,10 +84,17 @@ where · 65 unmodified lines
82 let mut arguments = gix_protocol::fetch::Arguments::new(protocol_version, fetch_features);84 let mut arguments = gix_protocol::fetch::Arguments::new(protocol_version, fetch_features);
83 if matches!(con.remote.fetch_tags, crate::remote::fetch::Tags::Included) {85 if matches!(con.remote.fetch_tags, crate::remote::fetch::Tags::Included) {
84 if !arguments.can_use_include_tag() {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 arguments.use_include_tag();94 arguments.use_include_tag();
88 }95 }
96 let (shallow_commits, mut shallow_lock) = add_shallow_args(&mut arguments, &self.shallow, repo)?;
97
89 let mut previous_response = None::<gix_protocol::fetch::Response>;98 let mut previous_response = None::<gix_protocol::fetch::Response>;
90 let mut round = 1;99 let mut round = 1;
91 100
@@ -108,6 +117,7 @@ where · 16 unmodified lines
108 con.remote.fetch_tags,117 con.remote.fetch_tags,
109 &mut arguments,118 &mut arguments,
110 previous_response.as_ref(),119 previous_response.as_ref(),
120 (self.shallow != Shallow::NoChange).then(|| con.remote.refspecs(remote::Direction::Fetch)),
111 ) {121 ) {
112 Ok(_) if arguments.is_empty() => {122 Ok(_) if arguments.is_empty() => {
113 gix_protocol::indicate_end_of_interaction(&mut con.transport).await.ok();123 gix_protocol::indicate_end_of_interaction(&mut con.transport).await.ok();
@@ -146,11 +156,26 @@ where · 32 unmodified lines
146 if !sideband_all {156 if !sideband_all {
147 setup_remote_progress(progress, &mut reader);157 setup_remote_progress(progress, &mut reader);
148 }158 }
159 previous_response = Some(response);
149 break 'negotiation reader;160 break 'negotiation reader;
150 } else {161 } else {
151 previous_response = Some(response);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 let options = gix_pack::bundle::write::Options {180 let options = gix_pack::bundle::write::Options {
156 thread_limit: config::index_threads(repo)?,181 thread_limit: config::index_threads(repo)?,
@@ -187,6 +212,12 @@ where · 30 unmodified lines
187 gix_protocol::indicate_end_of_interaction(&mut con.transport).await.ok();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 let update_refs = refs::update(221 let update_refs = refs::update(
191 repo,222 repo,
192 self.reflog_message223 self.reflog_message
@@ -221,6 +252,57 @@ where · 28 unmodified lines
221 }252 }
222}253}
223 254
255fn 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
260fn 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
224fn setup_remote_progress<P>(306fn setup_remote_progress<P>(
225 progress: &mut P,307 progress: &mut P,
226 reader: &mut Box<dyn gix_protocol::transport::client::ExtendedBufRead + Unpin + '_>,308 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
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)]
59pub 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
87impl Default for Shallow {
88 fn default() -> Self {
89 Shallow::NoChange
90 }
91}
92
93impl 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/// Information about the relationship between our refspecs, and remote references with their local counterparts.101/// Information about the relationship between our refspecs, and remote references with their local counterparts.
56#[derive(Default, Debug, Clone)]102#[derive(Default, Debug, Clone)]
57#[cfg(any(feature = "blocking-network-client", feature = "async-network-client"))]103#[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
12 .map_or(false, |m| m.is_file() && m.len() > 0)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 file15 /// Return a shared list of shallow commits which is updated automatically if the in-memory snapshot has become stale
16 /// on disk has changed.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 /// The shared list is shared across all clones of this repository.22 /// The shared list is shared across all clones of this repository.
19 pub fn shallow_commits(&self) -> Result<Option<crate::shallow::Commits>, crate::shallow::open::Error> {23 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
4/// sorted to allow bisecting.4/// sorted to allow bisecting.
5pub type Commits = gix_features::fs::SharedSnapshot<Vec<gix_hash::ObjectId>>;5pub type Commits = gix_features::fs::SharedSnapshot<Vec<gix_hash::ObjectId>>;
6 6
7///
8#[cfg(any(feature = "blocking-network-client", feature = "async-network-client"))]
9pub 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"))]
72pub use write::function::write;
73
7///74///
8pub mod open {75pub mod open {
9 /// The error returned by [`Repository::shallow_commits()`][crate::Repository::shallow_commits()].76 /// 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
2 2
3#[cfg(feature = "blocking-network-client")]3#[cfg(feature = "blocking-network-client")]
4mod blocking_io {4mod blocking_io {
5 use gix::remote::fetch::SpecIndex;5 use gix::config::tree::{Clone, Core, Init, Key};
6 use gix::remote::Direction;
6 use gix_object::bstr::ByteSlice;7 use gix_object::bstr::ByteSlice;
7 use gix_ref::TargetRef;8 use gix_ref::TargetRef;
9 use std::sync::atomic::AtomicBool;
8 10
9 use crate::{remote, util::restricted};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 #[test]202 #[test]
12 fn fetch_only_with_configuration() -> crate::Result {203 fn fetch_only_with_configuration() -> crate::Result {
13 let tmp = gix_testtools::tempfile::TempDir::new()?;204 let tmp = gix_testtools::tempfile::TempDir::new()?;
@@ -20,8 +211,8 @@ mod blocking_io { · 6 unmodified lines
20 gix::create::Kind::Bare,211 gix::create::Kind::Bare,
21 Default::default(),212 Default::default(),
22 gix::open::Options::isolated().config_overrides([213 gix::open::Options::isolated().config_overrides([
23 "init.defaultBranch=unused-as-overridden-by-remote",214 Init::DEFAULT_BRANCH.validated_assignment_fmt(&"unused-as-overridden-by-remote")?,
24 "core.logAllRefUpdates",215 Core::LOG_ALL_REF_UPDATES.logical_name().into(),
25 // missing user and email is acceptable in this special case, i.e. `git` also doesn't mind filling it in.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 )?
gix/tests/fixtures/generated-archives/make_diff_repo.tar.xz+2−2
@@ -1,3 +1,3 @@
1version https://git-lfs.github.com/spec/v11version https://git-lfs.github.com/spec/v1
2oid sha256:f0ffd6adef1a5755c2013dd7e793ca8e239308448872681a36a2216ea56623b22oid sha256:dceeedc3ee706632030f8317f2245c327456ec58a6ac690d09099557a5aa23b0
3size 320323size 32336
gix/tests/fixtures/make_fetch_repos.sh+1−1
@@ -1,7 +1,7 @@
1set -eu -o pipefail1set -eu -o pipefail
2 2
3# IMPORTANT: keep this repo small as it's used for writes, hence will be executed for each writer!3# IMPORTANT: keep this repo small as it's used for writes, hence will be executed for each writer!
4git clone --bare "${1:?First argument is the complex base repo from make_remote_repos.sh/base}" base4git clone ${2:-} --bare "${1:?First argument is the complex base repo from make_remote_repos.sh/base}" base
5 5
6git clone --shared base clone-as-base-with-changes6git clone --shared base clone-as-base-with-changes
7(cd clone-as-base-with-changes7(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
107 git branch -D tmp107 git branch -D tmp
108)108)
109 109
110git clone --shared --depth 2 file://$PWD/base base.shallow
111
112
110git clone --shared base clone113git clone --shared base clone
111(cd clone114(cd clone
112 git remote add myself .115 git remote add myself .
gix/tests/object/tree/diff.rs+1−4
@@ -591,10 +591,7 @@ mod track_rewrites { · 590 unmodified lines
591 out.num_similarity_checks_skipped_for_rename_tracking_due_to_limit, 0,591 out.num_similarity_checks_skipped_for_rename_tracking_due_to_limit, 0,
592 "no limit configured"592 "no limit configured"
593 );593 );
594 assert_eq!(594 assert_eq!(out.num_similarity_checks_skipped_for_copy_tracking_due_to_limit, 57);
595 out.num_similarity_checks_skipped_for_copy_tracking_due_to_limit,
596 if cfg!(windows) { 63 } else { 57 }
597 );
598 595
599 Ok(())596 Ok(())
600 }597 }
gix/tests/remote/fetch.rs+12−0
@@ -1,3 +1,15 @@
1mod 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#[cfg(any(feature = "blocking-network-client", feature = "async-network-client-async-std"))]13#[cfg(any(feature = "blocking-network-client", feature = "async-network-client-async-std"))]
2mod blocking_and_async_io {14mod blocking_and_async_io {
3 use std::sync::atomic::AtomicBool;15 use std::sync::atomic::AtomicBool;