Skip to content

Commit cf5a1f9

Browse files
feat(clone): turbo clone (#9904)
### Description Implements `turbo clone`, essentially a small wrapper around `git clone` that does simple optimizations like blobless clones for local development and treeless clones for CI. ### Testing Instructions Wrote some tests but not convinced they're actually effective. See comments below
1 parent 00ada83 commit cf5a1f9

File tree

13 files changed

+245
-24
lines changed

13 files changed

+245
-24
lines changed

crates/turborepo-lib/src/cli/error.rs

+2
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ pub enum Error {
2626
#[error(transparent)]
2727
Boundaries(#[from] crate::boundaries::Error),
2828
#[error(transparent)]
29+
Clone(#[from] crate::commands::clone::Error),
30+
#[error(transparent)]
2931
Path(#[from] turbopath::PathError),
3032
#[error(transparent)]
3133
#[diagnostic(transparent)]

crates/turborepo-lib/src/cli/mod.rs

+25-3
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ use turborepo_ui::{ColorConfig, GREY};
2828
use crate::{
2929
cli::error::print_potential_tasks,
3030
commands::{
31-
bin, boundaries, config, daemon, generate, info, link, login, logout, ls, prune, query,
32-
run, scan, telemetry, unlink, CommandBase,
31+
bin, boundaries, clone, config, daemon, generate, info, link, login, logout, ls, prune,
32+
query, run, scan, telemetry, unlink, CommandBase,
3333
},
3434
get_version,
3535
run::watch::WatchClient,
@@ -588,6 +588,17 @@ pub enum Command {
588588
#[clap(short = 'F', long, group = "scope-filter-group")]
589589
filter: Vec<String>,
590590
},
591+
#[clap(hide = true)]
592+
Clone {
593+
url: String,
594+
dir: Option<String>,
595+
#[clap(long, conflicts_with = "local")]
596+
ci: bool,
597+
#[clap(long, conflicts_with = "ci")]
598+
local: bool,
599+
#[clap(long)]
600+
depth: Option<usize>,
601+
},
591602
/// Generate the autocompletion script for the specified shell
592603
Completion { shell: Shell },
593604
/// Runs the Turborepo background daemon
@@ -1360,7 +1371,6 @@ pub async fn run(
13601371
};
13611372

13621373
cli_args.command = Some(command);
1363-
cli_args.cwd = Some(repo_root.as_path().to_owned());
13641374

13651375
let root_telemetry = GenericEventBuilder::new();
13661376
root_telemetry.track_start();
@@ -1389,6 +1399,18 @@ pub async fn run(
13891399

13901400
Ok(boundaries::run(base, event).await?)
13911401
}
1402+
Command::Clone {
1403+
url,
1404+
dir,
1405+
ci,
1406+
local,
1407+
depth,
1408+
} => {
1409+
let event = CommandEventBuilder::new("clone").with_parent(&root_telemetry);
1410+
event.track_call();
1411+
1412+
Ok(clone::run(cwd, url, dir.as_deref(), *ci, *local, *depth)?)
1413+
}
13921414
#[allow(unused_variables)]
13931415
Command::Daemon { command, idle_time } => {
13941416
let event = CommandEventBuilder::new("daemon").with_parent(&root_telemetry);
+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
use std::env::current_dir;
2+
3+
use camino::Utf8Path;
4+
use thiserror::Error;
5+
use turbopath::AbsoluteSystemPathBuf;
6+
use turborepo_ci::is_ci;
7+
use turborepo_scm::clone::{CloneMode, Git};
8+
9+
#[derive(Debug, Error)]
10+
pub enum Error {
11+
#[error("path is not valid UTF-8")]
12+
Path(#[from] camino::FromPathBufError),
13+
14+
#[error(transparent)]
15+
Turbopath(#[from] turbopath::PathError),
16+
17+
#[error("failed to clone repository")]
18+
Scm(#[from] turborepo_scm::Error),
19+
}
20+
21+
pub fn run(
22+
cwd: Option<&Utf8Path>,
23+
url: &str,
24+
dir: Option<&str>,
25+
ci: bool,
26+
local: bool,
27+
depth: Option<usize>,
28+
) -> Result<i32, Error> {
29+
// We do *not* want to use the repo root but the actual, literal cwd for clone
30+
let cwd = if let Some(cwd) = cwd {
31+
cwd.to_owned()
32+
} else {
33+
current_dir()
34+
.expect("could not get current directory")
35+
.try_into()?
36+
};
37+
let abs_cwd = AbsoluteSystemPathBuf::from_cwd(cwd)?;
38+
39+
let clone_mode = if ci {
40+
CloneMode::CI
41+
} else if local {
42+
CloneMode::Local
43+
} else if is_ci() {
44+
CloneMode::CI
45+
} else {
46+
CloneMode::Local
47+
};
48+
49+
let git = Git::find()?;
50+
git.clone(url, abs_cwd, dir, None, clone_mode, depth)?;
51+
52+
Ok(0)
53+
}

crates/turborepo-lib/src/commands/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ use crate::{
1616

1717
pub(crate) mod bin;
1818
pub(crate) mod boundaries;
19+
pub(crate) mod clone;
1920
pub(crate) mod config;
2021
pub(crate) mod daemon;
2122
pub(crate) mod generate;

crates/turborepo-lib/src/diagnostics.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use tokio::{
1111
};
1212
use turbo_updater::check_for_updates;
1313
use turbopath::AbsoluteSystemPathBuf;
14-
use turborepo_scm::Git;
14+
use turborepo_scm::GitRepo;
1515

1616
use crate::{
1717
commands::{
@@ -157,7 +157,7 @@ impl Diagnostic for GitDaemonDiagnostic {
157157
// get the current setting
158158
let stdout = Stdio::piped();
159159

160-
let Ok(git_path) = Git::find_bin() else {
160+
let Ok(git_path) = GitRepo::find_bin() else {
161161
return Err("git not found");
162162
};
163163

crates/turborepo-scm/src/clone.rs

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
use std::{backtrace::Backtrace, process::Command};
2+
3+
use turbopath::AbsoluteSystemPathBuf;
4+
5+
use crate::{Error, GitRepo};
6+
7+
pub enum CloneMode {
8+
/// Cloning locally, do a blobless clone (good UX and reasonably fast)
9+
Local,
10+
/// Cloning on CI, do a treeless clone (worse UX but fastest)
11+
CI,
12+
}
13+
14+
// Provide a sane maximum depth for cloning. If a user needs more history than
15+
// this, they can override or fetch it themselves.
16+
const MAX_CLONE_DEPTH: usize = 64;
17+
18+
/// A wrapper around the git binary that is not tied to a specific repo.
19+
pub struct Git {
20+
bin: AbsoluteSystemPathBuf,
21+
}
22+
23+
impl Git {
24+
pub fn find() -> Result<Self, Error> {
25+
Ok(Self {
26+
bin: GitRepo::find_bin()?,
27+
})
28+
}
29+
30+
pub fn spawn_git_command(
31+
&self,
32+
cwd: &AbsoluteSystemPathBuf,
33+
args: &[&str],
34+
pathspec: &str,
35+
) -> Result<(), Error> {
36+
let mut command = Command::new(self.bin.as_std_path());
37+
command
38+
.args(args)
39+
.current_dir(cwd)
40+
.env("GIT_OPTIONAL_LOCKS", "0");
41+
42+
if !pathspec.is_empty() {
43+
command.arg("--").arg(pathspec);
44+
}
45+
46+
let output = command.spawn()?.wait_with_output()?;
47+
48+
if !output.status.success() {
49+
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
50+
Err(Error::Git(stderr, Backtrace::capture()))
51+
} else {
52+
Ok(())
53+
}
54+
}
55+
56+
pub fn clone(
57+
&self,
58+
url: &str,
59+
cwd: AbsoluteSystemPathBuf,
60+
dir: Option<&str>,
61+
branch: Option<&str>,
62+
mode: CloneMode,
63+
depth: Option<usize>,
64+
) -> Result<(), Error> {
65+
let depth = depth.unwrap_or(MAX_CLONE_DEPTH).to_string();
66+
let mut args = vec!["clone", "--depth", &depth];
67+
if let Some(branch) = branch {
68+
args.push("--branch");
69+
args.push(branch);
70+
}
71+
match mode {
72+
CloneMode::Local => {
73+
args.push("--filter=blob:none");
74+
}
75+
CloneMode::CI => {
76+
args.push("--filter=tree:0");
77+
}
78+
}
79+
args.push(url);
80+
if let Some(dir) = dir {
81+
args.push(dir);
82+
}
83+
84+
self.spawn_git_command(&cwd, &args, "")?;
85+
86+
Ok(())
87+
}
88+
}

crates/turborepo-scm/src/git.rs

+6-6
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use turbopath::{
1414
};
1515
use turborepo_ci::Vendor;
1616

17-
use crate::{Error, Git, SCM};
17+
use crate::{Error, GitRepo, SCM};
1818

1919
#[derive(Debug, PartialEq, Eq)]
2020
pub struct InvalidRange {
@@ -171,7 +171,7 @@ impl CIEnv {
171171
}
172172
}
173173

174-
impl Git {
174+
impl GitRepo {
175175
fn get_current_branch(&self) -> Result<String, Error> {
176176
let output = self.execute_git_command(&["branch", "--show-current"], "")?;
177177
let output = String::from_utf8(output)?;
@@ -324,7 +324,7 @@ impl Git {
324324
Ok(files)
325325
}
326326

327-
fn execute_git_command(&self, args: &[&str], pathspec: &str) -> Result<Vec<u8>, Error> {
327+
pub fn execute_git_command(&self, args: &[&str], pathspec: &str) -> Result<Vec<u8>, Error> {
328328
let mut command = Command::new(self.bin.as_std_path());
329329
command
330330
.args(args)
@@ -432,7 +432,7 @@ mod tests {
432432
use super::{previous_content, CIEnv, InvalidRange};
433433
use crate::{
434434
git::{GitHubCommit, GitHubEvent},
435-
Error, Git, SCM,
435+
Error, GitRepo, SCM,
436436
};
437437

438438
fn setup_repository(
@@ -1045,7 +1045,7 @@ mod tests {
10451045
repo.branch(branch, &commit, true).unwrap();
10461046
});
10471047

1048-
let thing = Git::find(&root).unwrap();
1048+
let thing = GitRepo::find(&root).unwrap();
10491049
let actual = thing.resolve_base(target_branch, CIEnv::none()).ok();
10501050

10511051
assert_eq!(actual.as_deref(), expected);
@@ -1281,7 +1281,7 @@ mod tests {
12811281
Err(VarError::NotPresent)
12821282
};
12831283

1284-
let actual = Git::get_github_base_ref(CIEnv {
1284+
let actual = GitRepo::get_github_base_ref(CIEnv {
12851285
is_github_actions: test_case.env.is_github_actions,
12861286
github_base_ref: test_case.env.github_base_ref,
12871287
github_event_path: temp_file

crates/turborepo-scm/src/lib.rs

+10-7
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ use thiserror::Error;
1919
use tracing::debug;
2020
use turbopath::{AbsoluteSystemPath, AbsoluteSystemPathBuf, PathError, RelativeUnixPathBuf};
2121

22+
pub mod clone;
2223
pub mod git;
2324
mod hash_object;
2425
mod ls_tree;
@@ -171,7 +172,7 @@ pub(crate) fn wait_for_success<R: Read, T>(
171172
}
172173

173174
#[derive(Debug, Clone)]
174-
pub struct Git {
175+
pub struct GitRepo {
175176
root: AbsoluteSystemPathBuf,
176177
bin: AbsoluteSystemPathBuf,
177178
}
@@ -184,7 +185,7 @@ enum GitError {
184185
Root(AbsoluteSystemPathBuf, Error),
185186
}
186187

187-
impl Git {
188+
impl GitRepo {
188189
fn find(path_in_repo: &AbsoluteSystemPath) -> Result<Self, GitError> {
189190
// If which produces an invalid absolute path, it's not an execution error, it's
190191
// a programming error. We expect it to always give us an absolute path
@@ -236,17 +237,19 @@ fn find_git_root(turbo_root: &AbsoluteSystemPath) -> Result<AbsoluteSystemPathBu
236237

237238
#[derive(Debug, Clone)]
238239
pub enum SCM {
239-
Git(Git),
240+
Git(GitRepo),
240241
Manual,
241242
}
242243

243244
impl SCM {
244245
#[tracing::instrument]
245246
pub fn new(path_in_repo: &AbsoluteSystemPath) -> SCM {
246-
Git::find(path_in_repo).map(SCM::Git).unwrap_or_else(|e| {
247-
debug!("{}, continuing with manual hashing", e);
248-
SCM::Manual
249-
})
247+
GitRepo::find(path_in_repo)
248+
.map(SCM::Git)
249+
.unwrap_or_else(|e| {
250+
debug!("{}, continuing with manual hashing", e);
251+
SCM::Manual
252+
})
250253
}
251254

252255
pub fn is_manual(&self) -> bool {

crates/turborepo-scm/src/ls_tree.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ use std::{
66
use nom::Finish;
77
use turbopath::{AbsoluteSystemPathBuf, RelativeUnixPathBuf};
88

9-
use crate::{wait_for_success, Error, Git, GitHashes};
9+
use crate::{wait_for_success, Error, GitHashes, GitRepo};
1010

11-
impl Git {
11+
impl GitRepo {
1212
#[tracing::instrument(skip(self))]
1313
pub fn git_ls_tree(&self, root_path: &AbsoluteSystemPathBuf) -> Result<GitHashes, Error> {
1414
let mut hashes = GitHashes::new();

crates/turborepo-scm/src/package_deps.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use turborepo_telemetry::events::task::{FileHashMethod, PackageTaskEventBuilder}
88

99
#[cfg(feature = "git2")]
1010
use crate::hash_object::hash_objects;
11-
use crate::{Error, Git, GitHashes, SCM};
11+
use crate::{Error, GitHashes, GitRepo, SCM};
1212

1313
pub const INPUT_INCLUDE_DEFAULT_FILES: &str = "$TURBO_DEFAULT$";
1414

@@ -112,7 +112,7 @@ impl SCM {
112112
}
113113
}
114114

115-
impl Git {
115+
impl GitRepo {
116116
fn get_package_file_hashes<S: AsRef<str>>(
117117
&self,
118118
turbo_root: &AbsoluteSystemPath,

crates/turborepo-scm/src/status.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ use std::{
99
use nom::Finish;
1010
use turbopath::{AbsoluteSystemPath, RelativeUnixPathBuf};
1111

12-
use crate::{wait_for_success, Error, Git, GitHashes};
12+
use crate::{wait_for_success, Error, GitHashes, GitRepo};
1313

14-
impl Git {
14+
impl GitRepo {
1515
#[tracing::instrument(skip(self, root_path, hashes))]
1616
pub(crate) fn append_git_status(
1717
&self,

0 commit comments

Comments
 (0)