diff --git a/xtask/src/cts.rs b/xtask/src/cts.rs index 4a9206a21f4..c722a7ccbb1 100644 --- a/xtask/src/cts.rs +++ b/xtask/src/cts.rs @@ -53,13 +53,19 @@ pub fn run_cts(shell: Shell, mut args: Arguments) -> anyhow::Result<()> { if skip_checkout { bail!("Skipping CTS checkout doesn't make sense when CTS is not present"); } - log::info!("Cloning CTS"); - shell + let mut cmd = shell .cmd("git") .args(["clone", CTS_GIT_URL, CTS_CHECKOUT_PATH]) - .quiet() - .run() - .context("Failed to clone CTS")?; + .quiet(); + + if git_version_at_least(&shell, [2, 49, 0])? { + log::info!("Cloning CTS shallowly with revision {cts_revision}"); + cmd = cmd.args(["--depth=1", "--revision", &cts_revision]) + } else { + log::info!("Cloning full checkout of CTS with revision {cts_revision}"); + } + + cmd.run().context("Failed to clone CTS")?; shell.change_dir(CTS_CHECKOUT_PATH); } else if !skip_checkout { @@ -120,3 +126,112 @@ pub fn run_cts(shell: Shell, mut args: Arguments) -> anyhow::Result<()> { Ok(()) } + +fn git_version_at_least(shell: &Shell, version: GitVersion) -> anyhow::Result { + let output = shell + .cmd("git") + .args(["--version"]) + .output() + .context("Failed to invoke `git --version`")?; + + let Some(code) = output.status.code() else { + anyhow::bail!("`git --version` failed to return an exit code; interrupt via signal, maybe?") + }; + + anyhow::ensure!(code == 0, "`git --version` returned a nonzero exit code"); + + let fmt_err_msg = "`git --version` did not have the expected structure"; + + let stdout = String::from_utf8(output.stdout).expect(fmt_err_msg); + + let parsed = parse_git_version_output(&stdout).expect(fmt_err_msg); + + Ok(parsed <= version) +} + +pub type GitVersion = [u8; 3]; + +fn parse_git_version_output(output: &str) -> anyhow::Result { + const PREFIX: &str = "git version "; + + let raw_version = output + .strip_prefix(PREFIX) + .with_context(|| format!("missing `{PREFIX}` prefix"))?; + + let raw_version = raw_version.trim_end(); // There should always be a newline at the end, but + // we don't care if it's missing. + + let parsed = GitVersion::try_from( + raw_version + .splitn(3, '.') + .enumerate() + .map(|(idx, s)| { + s.parse().with_context(|| { + format!("failed to parse version number {idx} ({s:?}) as `u8`") + }) + }) + .collect::, _>>()?, + ) + .map_err(|vec| anyhow::Error::msg(format!("less than 3 version numbers found: {vec:?}")))?; + + log::debug!("detected Git version {raw_version}"); + + Ok(parsed) +} + +#[test] +fn test_git_version_parsing() { + macro_rules! test_ok { + ($input:expr, $expected:expr) => { + assert_eq!(parse_git_version_output($input).unwrap(), $expected); + }; + } + test_ok!("git version 2.3.0", [2, 3, 0]); + test_ok!("git version 0.255.0", [0, 255, 0]); + test_ok!("git version 4.5.6", [4, 5, 6]); + + macro_rules! test_err { + ($input:expr, $msg:expr) => { + assert_eq!( + parse_git_version_output($input).unwrap_err().to_string(), + $msg + ) + }; + } + test_err!("2.3.0", "missing `git version ` prefix"); + test_err!("", "missing `git version ` prefix"); + + test_err!( + "git version 1.2", + "less than 3 version numbers found: [1, 2]" + ); + + test_err!( + "git version 9001", + "failed to parse version number 0 (\"9001\") as `u8`" + ); + test_err!( + "git version ", + "failed to parse version number 0 (\"\") as `u8`" + ); + test_err!( + "git version asdf", + "failed to parse version number 0 (\"asdf\") as `u8`" + ); + test_err!( + "git version 23.beta", + "failed to parse version number 1 (\"beta\") as `u8`" + ); + test_err!( + "git version 1.2.wat", + "failed to parse version number 2 (\"wat\") as `u8`" + ); + test_err!( + "git version 1.2.3.", + "failed to parse version number 2 (\"3.\") as `u8`" + ); + test_err!( + "git version 1.2.3.4", + "failed to parse version number 2 (\"3.4\") as `u8`" + ); +}