Skip to content

Commit baa0ea8

Browse files
committed
feat: shallow support for clone operations.
TODO: more elaborate docs
1 parent 62647dd commit baa0ea8

File tree

15 files changed

+302
-16
lines changed

15 files changed

+302
-16
lines changed

crate-status.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -637,10 +637,14 @@ See its [README.md](https://github.com/Byron/gitoxide/blob/main/gix-lock/README.
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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ impl PrepareFetch {
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 {
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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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 {
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 {
99102
repo: Some(repo),
100103
remote_name: None,
101104
configure_remote: None,
105+
shallow: remote::fetch::Shallow::NoChange,
102106
})
103107
}
104108
}

gix/src/remote/connection/fetch/error.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,17 @@ pub enum Error {
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),
3142
}
3243

3344
impl gix_protocol::transport::IsSpuriousError for Error {

gix/src/remote/connection/fetch/mod.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ where
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
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
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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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 {
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,10 +27,12 @@ pub(crate) fn one_round(
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 => {
3338
assert_eq!(round, 1, "Naive always finishes after the first round, and claims.");
@@ -42,6 +47,14 @@ pub(crate) fn one_round(
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(
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
}

gix/src/remote/connection/fetch/receive_pack.rs

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
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

67
use crate::{
78
remote,
89
remote::{
910
connection::fetch::config,
1011
fetch,
11-
fetch::{negotiate, refs, Error, Outcome, Prepare, ProgressId, RefLogMessage, Status},
12+
fetch::{negotiate, refs, Error, Outcome, Prepare, ProgressId, RefLogMessage, Shallow, Status},
1213
},
13-
Progress,
14+
Progress, Repository,
1415
};
1516

1617
impl<'remote, 'repo, T, P> Prepare<'remote, 'repo, T, P>
@@ -82,10 +83,17 @@ where
8283
let mut arguments = gix_protocol::fetch::Arguments::new(protocol_version, fetch_features);
8384
if matches!(con.remote.fetch_tags, crate::remote::fetch::Tags::Included) {
8485
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");
86+
return Err(Error::MissingServerFeature {
87+
feature: "include-tag",
88+
description:
89+
// NOTE: if this is an issue, we could probably do what's proposed here.
90+
"To make this work we would have to implement another pass to fetch attached tags separately",
91+
});
8692
}
8793
arguments.use_include_tag();
8894
}
95+
let (shallow_commits, shallow_lock) = add_shallow_args(&mut arguments, &self.shallow, repo)?;
96+
8997
let mut previous_response = None::<gix_protocol::fetch::Response>;
9098
let mut round = 1;
9199

@@ -108,6 +116,7 @@ where
108116
con.remote.fetch_tags,
109117
&mut arguments,
110118
previous_response.as_ref(),
119+
(self.shallow != Shallow::NoChange).then(|| con.remote.refspecs(remote::Direction::Fetch)),
111120
) {
112121
Ok(_) if arguments.is_empty() => {
113122
gix_protocol::indicate_end_of_interaction(&mut con.transport).await.ok();
@@ -146,6 +155,7 @@ where
146155
if !sideband_all {
147156
setup_remote_progress(progress, &mut reader);
148157
}
158+
previous_response = Some(response);
149159
break 'negotiation reader;
150160
} else {
151161
previous_response = Some(response);
@@ -187,6 +197,12 @@ where
187197
gix_protocol::indicate_end_of_interaction(&mut con.transport).await.ok();
188198
}
189199

200+
if let Some((response, shallow_lock)) = previous_response.zip(shallow_lock) {
201+
if !response.shallow_updates().is_empty() {
202+
crate::shallow::write(shallow_lock, shallow_commits, response.shallow_updates())?;
203+
}
204+
}
205+
190206
let update_refs = refs::update(
191207
repo,
192208
self.reflog_message
@@ -221,6 +237,56 @@ where
221237
}
222238
}
223239

240+
fn add_shallow_args(
241+
args: &mut Arguments,
242+
shallow: &Shallow,
243+
repo: &Repository,
244+
) -> Result<(Option<crate::shallow::Commits>, Option<gix_lock::File>), Error> {
245+
let expect_change = *shallow != Shallow::NoChange;
246+
let shallow_lock = expect_change
247+
.then(|| {
248+
gix_lock::File::acquire_to_update_resource(repo.shallow_file(), gix_lock::acquire::Fail::Immediately, None)
249+
})
250+
.transpose()?;
251+
252+
let shallow_commits = repo.shallow_commits()?;
253+
if (shallow_commits.is_some() || expect_change) && !args.can_use_shallow() {
254+
// NOTE: if this is an issue, we can always unshallow the repo ourselves.
255+
return Err(Error::MissingServerFeature {
256+
feature: "shallow",
257+
description: "shallow clones need server support to remain shallow, otherwise bigger than expected packs are sent effectively unshallowing the repository",
258+
});
259+
}
260+
if let Some(shallow_commits) = &shallow_commits {
261+
for commit in shallow_commits.iter() {
262+
args.shallow(commit);
263+
}
264+
}
265+
match shallow {
266+
Shallow::NoChange => {}
267+
Shallow::DepthAtRemote(commits) => args.deepen(commits.get() as usize),
268+
Shallow::Deepen(commits) => {
269+
args.deepen(*commits as usize);
270+
args.deepen_relative();
271+
}
272+
Shallow::Since { cutoff } => {
273+
args.deepen_since(cutoff.seconds_since_unix_epoch as usize);
274+
}
275+
Shallow::Exclude {
276+
remote_refs,
277+
since_cutoff,
278+
} => {
279+
if let Some(cutoff) = since_cutoff {
280+
args.deepen_since(cutoff.seconds_since_unix_epoch as usize);
281+
}
282+
for ref_ in remote_refs {
283+
args.deepen_not(ref_.as_ref().as_bstr());
284+
}
285+
}
286+
}
287+
Ok((shallow_commits, shallow_lock))
288+
}
289+
224290
fn setup_remote_progress<P>(
225291
progress: &mut P,
226292
reader: &mut Box<dyn gix_protocol::transport::client::ExtendedBufRead + Unpin + '_>,

gix/src/remote/fetch.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,52 @@ impl Tags {
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 unshallow() -> 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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,12 @@ impl Repository {
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> {

0 commit comments

Comments
 (0)