Skip to content

Commit 27629ce

Browse files
committed
perf: Reuse git root from worktree detection in SCM::new
The worktree detection call (git rev-parse --show-toplevel --git-common-dir) now also queries --show-cdup in the same subprocess. The resolved git root is stored on WorktreeInfo and threaded through CacheDirResult → Opts → RunBuilder, so SCM::new can skip its own git rev-parse subprocess entirely. This eliminates one fork+exec (~30ms) from the critical path.
1 parent 37a599d commit 27629ce

File tree

5 files changed

+70
-7
lines changed

5 files changed

+70
-7
lines changed

crates/turborepo-config/src/lib.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ pub struct CacheDirResult {
6363
/// This is used for messaging to inform users when worktree cache sharing
6464
/// is active.
6565
pub is_shared_worktree: bool,
66+
/// The git repository root, if detected during worktree resolution.
67+
/// Captured here so `SCM::new` can skip its own `git rev-parse` call.
68+
pub git_root: Option<AbsoluteSystemPathBuf>,
6669
}
6770

6871
/// Configuration errors for turborepo.
@@ -393,6 +396,7 @@ impl ConfigurationOptions {
393396
return CacheDirResult {
394397
path: explicit_cache_dir.clone(),
395398
is_shared_worktree: false,
399+
git_root: None,
396400
};
397401
}
398402

@@ -405,6 +409,7 @@ impl ConfigurationOptions {
405409
worktree_info.main_worktree_root,
406410
worktree_info.is_linked_worktree()
407411
);
412+
let git_root = Some(worktree_info.git_root.clone());
408413
if worktree_info.is_linked_worktree() {
409414
// We're in a linked worktree - use the main worktree's cache
410415
// Use turbopath's join_component to ensure consistent path separators
@@ -415,6 +420,7 @@ impl ConfigurationOptions {
415420
let result = CacheDirResult {
416421
path: Utf8PathBuf::from(main_cache_path.as_str()),
417422
is_shared_worktree: true,
423+
git_root,
418424
};
419425
debug!("Using shared worktree cache at: {}", result.path);
420426
result
@@ -427,6 +433,7 @@ impl ConfigurationOptions {
427433
CacheDirResult {
428434
path: Utf8PathBuf::from(Self::DEFAULT_CACHE_DIR),
429435
is_shared_worktree: false,
436+
git_root,
430437
}
431438
}
432439
}
@@ -440,6 +447,7 @@ impl ConfigurationOptions {
440447
CacheDirResult {
441448
path: Utf8PathBuf::from(Self::DEFAULT_CACHE_DIR),
442449
is_shared_worktree: false,
450+
git_root: None,
443451
}
444452
}
445453
}

crates/turborepo-lib/src/opts.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use camino::Utf8PathBuf;
44
use serde::Serialize;
55
use thiserror::Error;
66
use tracing::debug;
7-
use turbopath::{AbsoluteSystemPath, AnchoredSystemPathBuf};
7+
use turbopath::{AbsoluteSystemPath, AbsoluteSystemPathBuf, AnchoredSystemPathBuf};
88
use turborepo_api_client::APIAuth;
99
use turborepo_cache::{CacheOpts, RemoteCacheOpts};
1010
// Re-export RunCacheOpts from turborepo-run-cache
@@ -66,6 +66,9 @@ pub struct Opts {
6666
pub scope_opts: ScopeOpts,
6767
pub tui_opts: TuiOpts,
6868
pub future_flags: FutureFlags,
69+
/// Pre-resolved git root from worktree detection, if available.
70+
/// Allows `SCM::new` to skip its own `git rev-parse` subprocess.
71+
pub git_root: Option<AbsoluteSystemPathBuf>,
6972
}
7073

7174
impl Opts {
@@ -189,6 +192,7 @@ impl Opts {
189192
api_client_opts,
190193
tui_opts,
191194
future_flags,
195+
git_root: cache_dir_result.git_root,
192196
})
193197
}
194198
}

crates/turborepo-lib/src/run/builder.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,8 +203,12 @@ impl RunBuilder {
203203

204204
let scm_task = {
205205
let repo_root = self.repo_root.clone();
206+
let git_root = self.opts.git_root.clone();
206207
tokio::task::spawn_blocking(move || {
207-
let scm = SCM::new(&repo_root);
208+
let scm = match git_root {
209+
Some(root) => SCM::new_with_git_root(&repo_root, root),
210+
None => SCM::new(&repo_root),
211+
};
208212
let repo_index = scm.build_repo_index_eager();
209213
(scm, repo_index)
210214
})

crates/turborepo-scm/src/lib.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,29 @@ impl SCM {
293293
})
294294
}
295295

296+
/// Creates an SCM instance using a pre-resolved git root, avoiding the
297+
/// `git rev-parse --show-cdup` subprocess call that `new` would perform.
298+
/// Falls back to `new` if the git binary cannot be found.
299+
#[tracing::instrument]
300+
pub fn new_with_git_root(
301+
path_in_repo: &AbsoluteSystemPath,
302+
git_root: AbsoluteSystemPathBuf,
303+
) -> SCM {
304+
match GitRepo::find_bin() {
305+
Ok(bin) => SCM::Git(GitRepo {
306+
root: git_root,
307+
bin,
308+
}),
309+
Err(e) => {
310+
debug!(
311+
"git binary not found: {}, continuing with manual hashing",
312+
e
313+
);
314+
SCM::Manual
315+
}
316+
}
317+
}
318+
296319
pub fn is_manual(&self) -> bool {
297320
matches!(self, SCM::Manual)
298321
}

crates/turborepo-scm/src/worktree.rs

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ pub struct WorktreeInfo {
1818
pub worktree_root: AbsoluteSystemPathBuf,
1919
/// The root of the main worktree
2020
pub main_worktree_root: AbsoluteSystemPathBuf,
21+
/// The root of the git repository (resolved from `--show-cdup`).
22+
/// Captured here to avoid a redundant subprocess in `SCM::new`.
23+
pub git_root: AbsoluteSystemPathBuf,
2124
}
2225

2326
impl WorktreeInfo {
@@ -41,17 +44,22 @@ impl WorktreeInfo {
4144
/// - The worktree structure cannot be determined
4245
#[tracing::instrument]
4346
pub fn detect(path: &AbsoluteSystemPath) -> Result<Self, Error> {
44-
// Single git subprocess for both queries instead of two separate calls.
47+
// Single git subprocess for all three queries. --show-cdup is included
48+
// so that SCM::new can reuse the git root without spawning another
49+
// subprocess later.
4550
let output = Command::new("git")
46-
.args(["rev-parse", "--show-toplevel", "--git-common-dir"])
51+
.args([
52+
"rev-parse",
53+
"--show-toplevel",
54+
"--git-common-dir",
55+
"--show-cdup",
56+
])
4757
.current_dir(path)
4858
.output()?;
4959

5060
if !output.status.success() {
5161
let stderr = String::from_utf8_lossy(&output.stderr);
52-
return Err(Error::git_error(format!(
53-
"git rev-parse --show-toplevel --git-common-dir failed: {stderr}"
54-
)));
62+
return Err(Error::git_error(format!("git rev-parse failed: {stderr}")));
5563
}
5664

5765
let stdout = String::from_utf8(output.stdout)?;
@@ -69,11 +77,24 @@ impl WorktreeInfo {
6977
.trim()
7078
.to_string();
7179

80+
let show_cdup = lines
81+
.next()
82+
.ok_or_else(|| Error::git_error("git rev-parse --show-cdup produced no output"))?
83+
.trim();
84+
let git_root = if show_cdup.is_empty() {
85+
// Empty --show-cdup means we're already at the git root
86+
path.to_owned()
87+
} else {
88+
let resolved = path.as_std_path().join(show_cdup);
89+
AbsoluteSystemPathBuf::try_from(resolved.as_path())?.to_realpath()?
90+
};
91+
7292
let main_worktree_root = resolve_main_worktree_root(path, &git_common_dir)?;
7393

7494
Ok(Self {
7595
worktree_root,
7696
main_worktree_root,
97+
git_root,
7798
})
7899
}
79100
}
@@ -158,6 +179,7 @@ mod tests {
158179

159180
assert_eq!(info.worktree_root, repo_root);
160181
assert_eq!(info.main_worktree_root, repo_root);
182+
assert_eq!(info.git_root, repo_root);
161183
assert!(!info.is_linked_worktree());
162184
}
163185

@@ -208,6 +230,8 @@ mod tests {
208230

209231
assert_eq!(info.worktree_root, repo_root);
210232
assert_eq!(info.main_worktree_root, repo_root);
233+
// git_root should resolve to repo_root even when called from subdir
234+
assert_eq!(info.git_root, repo_root);
211235
assert!(!info.is_linked_worktree());
212236
}
213237

0 commit comments

Comments
 (0)