Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion app/src/ai/agent_sdk/driver/snapshot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1204,7 +1204,7 @@ where
.into_iter()
.map(|arg| arg.as_ref().to_os_string())
.collect::<Vec<_>>();
let mut command = Command::new("git");
let mut command = Command::new(warp_util::wsl::git_binary());
command
.args(&args)
.current_dir(repo_dir)
Expand Down
4 changes: 2 additions & 2 deletions app/src/ai/agent_sdk/driver/snapshot_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ impl HarnessSupportClient for TestClient {
/// (uncommitted edit) when `dirty` is true so `git diff --binary HEAD` has something to emit.
fn init_git_repo(dir: &Path, dirty: bool) {
let run = |args: &[&str]| {
let output = BlockingCommand::new("git")
let output = BlockingCommand::new(warp_util::wsl::git_binary())
.current_dir(dir)
.args(args)
.output()
Expand All @@ -174,7 +174,7 @@ fn init_git_repo(dir: &Path, dirty: bool) {
}

fn git_stdout(dir: &Path, args: &[&str]) -> String {
let output = BlockingCommand::new("git")
let output = BlockingCommand::new(warp_util::wsl::git_binary())
.current_dir(dir)
.args(args)
.output()
Expand Down
2 changes: 1 addition & 1 deletion app/src/ai/blocklist/passive_suggestions/legacy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ impl PassiveSuggestionsModel {

self.unit_test_generation_future_handle = Some(ctx.spawn(
async move {
let output = Command::new("git")
let output = Command::new(warp_util::wsl::git_binary())
.args(["show", "HEAD"])
.current_dir(current_dir)
.stdout(Stdio::piped())
Expand Down
4 changes: 2 additions & 2 deletions app/src/ai/skills/resolve_skill_spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ pub async fn clone_repo_for_skill(
target_dir.display()
);

let output = AsyncCommand::new("git")
let output = AsyncCommand::new(warp_util::wsl::git_binary())
.arg("clone")
.arg(&repo_url)
.arg(&target_dir)
Expand Down Expand Up @@ -529,7 +529,7 @@ fn get_git_remote_org(repo_path: &Path) -> Option<String> {
log::debug!(
"[GIT OPERATION] resolve_skill_spec.rs get_git_remote_org git remote get-url origin"
);
let output = Command::new("git")
let output = Command::new(warp_util::wsl::git_binary())
.args(["remote", "get-url", "origin"])
.current_dir(repo_path)
.output()
Expand Down
6 changes: 1 addition & 5 deletions app/src/crash_reporting/linux.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,7 @@ pub fn get_virtualized_environment() -> Option<VirtualEnvironment> {
}
};

// Test specifically for WSL based on existence of a particular file under
// /proc.
//
// See: https://superuser.com/questions/1749781/how-can-i-check-if-the-environment-is-wsl-from-a-shell-script
if std::path::Path::new("/proc/sys/fs/binfmt_misc/WSLInterop").exists() {
if warp_util::wsl::is_wsl() {
return Some(VirtualEnvironment {
name: "wsl".to_owned(),
});
Expand Down
6 changes: 3 additions & 3 deletions app/src/integration_testing/agent_mode/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,14 @@ pub fn output_code_diff_with_base_commit(
log::debug!(
"[GIT OPERATION] mod.rs output_code_diff_with_base_commit git checkout {base_commit} -- {test_files_str}"
);
let _ = Command::new("git")
let _ = Command::new(warp_util::wsl::git_binary())
.args(["checkout", base_commit, "--", test_files_str])
.current_dir(working_dir)
.output();
log::debug!(
"[GIT OPERATION] mod.rs output_code_diff_with_base_commit git --no-pager diff {base_commit}"
);
let git_diff_output = Command::new("git")
let git_diff_output = Command::new(warp_util::wsl::git_binary())
.args(["--no-pager", "diff", base_commit])
.current_dir(working_dir)
.output();
Expand Down Expand Up @@ -104,7 +104,7 @@ pub fn output_code_diff_debug_info(app: &mut App, window_id: WindowId) {
log::debug!(
"[GIT OPERATION] mod.rs output_code_diff_debug_info git diff -- {file_name}"
);
let output = Command::new("git")
let output = Command::new(warp_util::wsl::git_binary())
.args(["diff", "--", &file_name])
.current_dir(&current_dir)
.output();
Expand Down
2 changes: 1 addition & 1 deletion app/src/search/files/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -505,7 +505,7 @@ impl FileSearchModel {

log::debug!("[GIT OPERATION] model.rs get_git_changed_files git status --porcelain");
// Run `git status --porcelain` to get changed files
let output = Command::new("git")
let output = Command::new(warp_util::wsl::git_binary())
.args(["status", "--porcelain"])
.current_dir(repo_path)
.output()?;
Expand Down
6 changes: 3 additions & 3 deletions app/src/util/git.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ pub async fn run_git_command_with_env(
"[GIT OPERATION] git.rs run_git_command git {}",
args.join(" ")
);
let mut cmd = Command::new("git");
let mut cmd = Command::new(warp_util::wsl::git_binary());
cmd.arg("-c")
.arg("diff.autoRefreshIndex=false")
.args(args)
Expand Down Expand Up @@ -81,7 +81,7 @@ pub async fn run_git_command_with_env(
/// Returns an empty set on any failure (not a git repo, git not found, etc.).
#[cfg(feature = "local_fs")]
pub fn list_local_branches_sync(repo_path: &Path) -> HashSet<String> {
let output = command::blocking::Command::new("git")
let output = command::blocking::Command::new(warp_util::wsl::git_binary())
.args(["branch", "--list", "--format=%(refname:short)"])
.current_dir(repo_path)
.stdout(command::Stdio::piped())
Expand Down Expand Up @@ -761,7 +761,7 @@ async fn run_gh_command(repo_path: &Path, args: &[&str], path_env: Option<&str>)
args.join(" ")
);

let mut cmd = Command::new("gh");
let mut cmd = Command::new(warp_util::wsl::gh_binary());
cmd.args(args)
.current_dir(repo_path)
.stdin(Stdio::null())
Expand Down
2 changes: 1 addition & 1 deletion app/src/util/git_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use super::{detect_current_branch, detect_current_branch_display};

/// Helper: run a git command inside the given repo directory.
async fn git(repo: &Path, args: &[&str]) -> String {
let output = Command::new("git")
let output = Command::new(warp_util::wsl::git_binary())
.args(args)
.current_dir(repo)
.stdout(Stdio::piped())
Expand Down
1 change: 1 addition & 0 deletions crates/integration/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ version-compare.workspace = true
warp = { workspace = true, features = ["integration_tests"] }
warp_cli = { workspace = true, features = ["integration_tests"] }
warp_core.workspace = true
warp_util.workspace = true
warp-command-signatures.workspace = true
warp_multi_agent_api.workspace = true
warp-workflows.workspace = true
Expand Down
2 changes: 1 addition & 1 deletion crates/integration/src/test/code_review.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ fn insert_lines(path: &Path, before_line_number: usize, new_lines: &[String]) {
}

fn run_git(test_dir: &Path, args: &[&str]) {
let status = Command::new("git")
let status = Command::new(warp_util::wsl::git_binary())
.args(args)
.current_dir(test_dir)
.status()
Expand Down
2 changes: 2 additions & 0 deletions crates/warp_util/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ rand.workspace = true
dirs.workspace = true
hex.workspace = true
lazy_static.workspace = true
log.workspace = true
mime_guess.workspace = true
regex.workspace = true
thiserror.workspace = true
Expand All @@ -33,4 +34,5 @@ windows.workspace = true
gloo.workspace = true

[dev-dependencies]
tempfile = "3.8.0"
warpui.workspace = true
1 change: 1 addition & 0 deletions crates/warp_util/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pub mod path;
pub mod standardized_path;
pub mod user_input;
pub mod worktree_names;
pub mod wsl;

#[cfg(windows)]
pub mod windows;
112 changes: 112 additions & 0 deletions crates/warp_util/src/wsl.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
//! WSL detection and binary resolution for Warp-internal subprocesses.
//!
//! Warp ships as a Linux ELF binary that users routinely run inside
//! WSL. WSL's default `appendWindowsPath = true` (in `/etc/wsl.conf`)
//! puts directories like `/mnt/c/Program Files/Git/cmd/` on `PATH`,
//! so a bare `Command::new("git")` can resolve to Windows `git.exe`
//! through WSL interop. That path works for some commands but is
//! dramatically slower, can mishandle Linux paths, and breaks
//! Linux-side hooks.
//!
//! [`git_binary`] and [`gh_binary`] return an absolute Linux-side
//! path when running inside WSL, and the literal program name on
//! every other host so `Command::new` performs its normal PATH
//! lookup at spawn time (preserving call-site `cmd.env("PATH", …)`
//! overrides). The same `/mnt/*` filtering precedent is used for
//! `compgen` in
//! `app/src/terminal/model/session/command_executor/wsl_command_executor.rs`.
//!
//! Resolution is cached for the life of the process; PATH is
//! effectively static for the Warp host process.

use std::ffi::{OsStr, OsString};
use std::path::{Path, PathBuf};
use std::sync::OnceLock;

#[cfg(test)]
#[path = "wsl_tests.rs"]
mod tests;

/// True when running inside a WSL guest. Cached for the life of the
/// process. Detection probes `/proc/sys/fs/binfmt_misc/WSLInterop`,
/// matching the existing checks in `crates/warpui/src/platform/linux/`
/// and `app/src/crash_reporting/linux.rs`.
pub fn is_wsl() -> bool {
static IS_WSL: OnceLock<bool> = OnceLock::new();
*IS_WSL.get_or_init(|| Path::new("/proc/sys/fs/binfmt_misc/WSLInterop").exists())
}

/// Program name to pass to `Command::new` for invoking `git` from
/// Warp-internal code. See module docs for the WSL behavior.
pub fn git_binary() -> &'static OsStr {
static GIT_BIN: OnceLock<OsString> = OnceLock::new();
GIT_BIN.get_or_init(|| resolve_or_warn("git"))
}

/// Program name to pass to `Command::new` for invoking `gh`.
pub fn gh_binary() -> &'static OsStr {
static GH_BIN: OnceLock<OsString> = OnceLock::new();
GH_BIN.get_or_init(|| resolve_or_warn("gh"))
}

fn resolve_or_warn(name: &str) -> OsString {
// Outside WSL, return the bare program name so each `Command::new`
// performs the OS's normal PATH lookup at spawn time. Resolving up
// front would freeze the binary path at module init and silently
// ignore call-site `cmd.env("PATH", ...)` overrides used to expose
// user-installed hooks (see `run_git_command_with_env` and
// `run_gh_command` in `app/src/util/git.rs`).
if !is_wsl() {
return OsString::from(name);
}
let path_env = std::env::var_os("PATH");
match resolve_binary_in_wsl_safe_path(name, path_env.as_deref(), true) {
Some(p) => p.into_os_string(),
None => {
log::warn!(
"wsl: no Linux-side `{name}` found on PATH (excluding /mnt/*); \
falling back to bare `{name}` which may resolve to a Windows .exe"
);
OsString::from(name)
}
}
}

/// Returns the first executable named `name` on `path_env`, skipping
/// any PATH entry under `/mnt/` when `is_wsl` is true. Returns `None`
/// if no acceptable match exists. Pure — exposed for unit testing
/// without depending on a real WSL host.
pub fn resolve_binary_in_wsl_safe_path(
name: &str,
path_env: Option<&OsStr>,
is_wsl: bool,
) -> Option<PathBuf> {
let path_env = path_env?;
for dir in std::env::split_paths(path_env) {
if is_wsl && dir.starts_with("/mnt") {
continue;
}
let candidate = dir.join(name);
if is_executable_file(&candidate) {
return Some(candidate);
}
}
None
}

#[cfg(unix)]
fn is_executable_file(path: &Path) -> bool {
use std::os::unix::fs::PermissionsExt as _;
match std::fs::metadata(path) {
Ok(md) => md.is_file() && (md.permissions().mode() & 0o111 != 0),
Err(_) => false,
}
}

#[cfg(not(unix))]
fn is_executable_file(_path: &Path) -> bool {
// The WSL-safe resolver only runs on Linux. Other targets short-
// circuit through `is_wsl() == false`, so this stub is unreachable
// in practice — present only to keep the crate compiling.
false
}
Loading