From 9723e1addf52cc336d59322de039ea0537cdca36 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 8 Mar 2023 10:14:39 +0100 Subject: [PATCH] feat: `gix clone` and `gix fetch` with controls for shallow repositories. TBD: more elaborate documentation --- gitoxide-core/src/repository/clone.rs | 7 ++- gitoxide-core/src/repository/fetch.rs | 3 + src/plumbing/main.rs | 4 ++ src/plumbing/options/mod.rs | 87 +++++++++++++++++++++++++++ src/plumbing/progress.rs | 8 ++- src/shared.rs | 30 ++++++++- 6 files changed, 134 insertions(+), 5 deletions(-) diff --git a/gitoxide-core/src/repository/clone.rs b/gitoxide-core/src/repository/clone.rs index e5a0bc55d0..789087b62f 100644 --- a/gitoxide-core/src/repository/clone.rs +++ b/gitoxide-core/src/repository/clone.rs @@ -5,6 +5,7 @@ pub struct Options { pub bare: bool, pub handshake_info: bool, pub no_tags: bool, + pub shallow: gix::remote::fetch::Shallow, } pub const PROGRESS_RANGE: std::ops::RangeInclusive = 1..=3; @@ -30,6 +31,7 @@ pub(crate) mod function { handshake_info, bare, no_tags, + shallow, }: Options, ) -> anyhow::Result<()> where @@ -66,8 +68,9 @@ pub(crate) mod function { if no_tags { prepare = prepare.configure_remote(|r| Ok(r.with_fetch_tags(gix::remote::fetch::Tags::None))); } - let (mut checkout, fetch_outcome) = - prepare.fetch_then_checkout(&mut progress, &gix::interrupt::IS_INTERRUPTED)?; + let (mut checkout, fetch_outcome) = prepare + .with_shallow(shallow) + .fetch_then_checkout(&mut progress, &gix::interrupt::IS_INTERRUPTED)?; let (repo, outcome) = if bare { (checkout.persist(), None) diff --git a/gitoxide-core/src/repository/fetch.rs b/gitoxide-core/src/repository/fetch.rs index 580cc4d919..e942074cf8 100644 --- a/gitoxide-core/src/repository/fetch.rs +++ b/gitoxide-core/src/repository/fetch.rs @@ -8,6 +8,7 @@ pub struct Options { pub remote: Option, /// If non-empty, override all ref-specs otherwise configured in the remote pub ref_specs: Vec, + pub shallow: gix::remote::fetch::Shallow, pub handshake_info: bool, } @@ -30,6 +31,7 @@ pub(crate) mod function { dry_run, remote, handshake_info, + shallow, ref_specs, }: Options, ) -> anyhow::Result<()> @@ -50,6 +52,7 @@ pub(crate) mod function { .connect(gix::remote::Direction::Fetch, progress)? .prepare_fetch(Default::default())? .with_dry_run(dry_run) + .with_shallow(shallow) .receive(&gix::interrupt::IS_INTERRUPTED)?; if handshake_info { diff --git a/src/plumbing/main.rs b/src/plumbing/main.rs index 733022fb7c..3c0af9d054 100644 --- a/src/plumbing/main.rs +++ b/src/plumbing/main.rs @@ -130,6 +130,7 @@ pub fn main() -> Result<()> { bare, no_tags, remote, + shallow, directory, }) => { let opts = core::repository::clone::Options { @@ -137,6 +138,7 @@ pub fn main() -> Result<()> { bare, handshake_info, no_tags, + shallow: shallow.into(), }; prepare_and_run( "clone", @@ -152,6 +154,7 @@ pub fn main() -> Result<()> { dry_run, handshake_info, remote, + shallow, ref_spec, }) => { let opts = core::repository::fetch::Options { @@ -159,6 +162,7 @@ pub fn main() -> Result<()> { dry_run, remote, handshake_info, + shallow: shallow.into(), ref_specs: ref_spec, }; prepare_and_run( diff --git a/src/plumbing/options/mod.rs b/src/plumbing/options/mod.rs index 54335f70d4..d22d024dbb 100644 --- a/src/plumbing/options/mod.rs +++ b/src/plumbing/options/mod.rs @@ -135,6 +135,9 @@ pub mod config { #[cfg(feature = "gitoxide-core-blocking-client")] pub mod fetch { + use gix::remote::fetch::Shallow; + use std::num::NonZeroU32; + #[derive(Debug, clap::Parser)] pub struct Platform { /// Don't change the local repository, but otherwise try to be as accurate as possible. @@ -145,6 +148,9 @@ pub mod fetch { #[clap(long, short = 'H')] pub handshake_info: bool, + #[clap(flatten)] + pub shallow: ShallowOptions, + /// The name of the remote to connect to, or the url of the remote to connect to directly. /// /// If unset, the current branch will determine the remote. @@ -155,10 +161,56 @@ pub mod fetch { #[clap(value_parser = crate::shared::AsBString)] pub ref_spec: Vec, } + + #[derive(Debug, clap::Parser)] + pub struct ShallowOptions { + /// Fetch with the history truncated to the given number of commits as seen from the remote. + #[clap(long, conflicts_with_all = ["shallow_since", "shallow_exclude", "deepen", "unshallow"])] + pub depth: Option, + + /// Extend the current shallow boundary by the given amount of commits, with 0 meaning no change. + #[clap(long, value_name = "DEPTH", conflicts_with_all = ["depth", "shallow_since", "shallow_exclude", "unshallow"])] + pub deepen: Option, + + /// Cutoff all history past the given date. Can be combined with shallow-exclude. + #[clap(long, value_parser = crate::shared::AsTime, value_name = "DATE", conflicts_with_all = ["depth", "deepen", "unshallow"])] + pub shallow_since: Option, + + /// Cutoff all history past the tag-name or ref-name. Can be combined with shallow-since. + #[clap(long, value_parser = crate::shared::AsPartialRefName, value_name = "REF_NAME", conflicts_with_all = ["depth", "deepen", "unshallow"])] + pub shallow_exclude: Vec, + + /// Remove the shallow boundary and fetch the entire history available on the remote. + #[clap(long, conflicts_with_all = ["shallow_since", "shallow_exclude", "depth", "deepen", "unshallow"])] + pub unshallow: bool, + } + + impl From for Shallow { + fn from(opts: ShallowOptions) -> Self { + if let Some(depth) = opts.depth { + Shallow::DepthAtRemote(depth) + } else if !opts.shallow_exclude.is_empty() { + Shallow::Exclude { + remote_refs: opts.shallow_exclude, + since_cutoff: opts.shallow_since, + } + } else if let Some(cutoff) = opts.shallow_since { + Shallow::Since { cutoff } + } else if let Some(depth) = opts.deepen { + Shallow::Deepen(depth) + } else if opts.unshallow { + Shallow::undo() + } else { + Shallow::default() + } + } + } } #[cfg(feature = "gitoxide-core-blocking-client")] pub mod clone { + use gix::remote::fetch::Shallow; + use std::num::NonZeroU32; use std::{ffi::OsString, path::PathBuf}; #[derive(Debug, clap::Parser)] @@ -175,12 +227,47 @@ pub mod clone { #[clap(long)] pub no_tags: bool, + #[clap(flatten)] + pub shallow: ShallowOptions, + /// The url of the remote to connect to, like `https://github.com/byron/gitoxide`. pub remote: OsString, /// The directory to initialize with the new repository and to which all data should be written. pub directory: Option, } + + #[derive(Debug, clap::Parser)] + pub struct ShallowOptions { + /// Create a shallow clone with the history truncated to the given number of commits. + #[clap(long, conflicts_with_all = ["shallow_since", "shallow_exclude"])] + pub depth: Option, + + /// Cutoff all history past the given date. Can be combined with shallow-exclude. + #[clap(long, value_parser = crate::shared::AsTime, value_name = "DATE")] + pub shallow_since: Option, + + /// Cutoff all history past the tag-name or ref-name. Can be combined with shallow-since. + #[clap(long, value_parser = crate::shared::AsPartialRefName, value_name = "REF_NAME")] + pub shallow_exclude: Vec, + } + + impl From for Shallow { + fn from(opts: ShallowOptions) -> Self { + if let Some(depth) = opts.depth { + Shallow::DepthAtRemote(depth) + } else if !opts.shallow_exclude.is_empty() { + Shallow::Exclude { + remote_refs: opts.shallow_exclude, + since_cutoff: opts.shallow_since, + } + } else if let Some(cutoff) = opts.shallow_since { + Shallow::Since { cutoff } + } else { + Shallow::default() + } + } + } } #[cfg(any(feature = "gitoxide-core-async-client", feature = "gitoxide-core-blocking-client"))] diff --git a/src/plumbing/progress.rs b/src/plumbing/progress.rs index dde6a10a09..a6f6d28b66 100644 --- a/src/plumbing/progress.rs +++ b/src/plumbing/progress.rs @@ -214,8 +214,14 @@ static GIT_CONFIG: &[Record] = &[ }, Record { config: "clone.rejectShallow", + usage: Planned { + note: Some("probably trivial to implement once there is protocol support for shallow clones"), + }, + }, + Record { + config: "receive.shallowUpdate", usage: NotPlanned { - reason: "it's not a use-case we consider important now, but once that changes it can be implemented", + reason: "it looks like a server-only setting that allows boundaries to change if refs are pushed that are outside of the boundary.", }, }, Record { diff --git a/src/shared.rs b/src/shared.rs index ee16f0c34e..64c602b3f9 100644 --- a/src/shared.rs +++ b/src/shared.rs @@ -292,7 +292,7 @@ mod clap { } } - use clap::builder::{OsStringValueParser, TypedValueParser}; + use clap::builder::{OsStringValueParser, StringValueParser, TypedValueParser}; #[derive(Clone)] pub struct AsPathSpec; @@ -306,5 +306,31 @@ mod clap { .parse_ref(cmd, arg, value) } } + + #[derive(Clone)] + pub struct AsTime; + + impl TypedValueParser for AsTime { + type Value = gix::date::Time; + + fn parse_ref(&self, cmd: &Command, arg: Option<&Arg>, value: &OsStr) -> Result { + StringValueParser::new() + .try_map(|arg| gix::date::parse(&arg, Some(std::time::SystemTime::now()))) + .parse_ref(cmd, arg, value) + } + } + + #[derive(Clone)] + pub struct AsPartialRefName; + + impl TypedValueParser for AsPartialRefName { + type Value = gix::refs::PartialName; + + fn parse_ref(&self, cmd: &Command, arg: Option<&Arg>, value: &OsStr) -> Result { + AsBString + .try_map(gix::refs::PartialName::try_from) + .parse_ref(cmd, arg, value) + } + } } -pub use self::clap::{AsBString, AsHashKind, AsOutputFormat, AsPathSpec}; +pub use self::clap::{AsBString, AsHashKind, AsOutputFormat, AsPartialRefName, AsPathSpec, AsTime};