diff --git a/Cargo.lock b/Cargo.lock index 04cfb6d6a7..92f96f2716 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5350,6 +5350,7 @@ dependencies = [ "serde_json", "smol", "sum_tree", + "tempfile", "text", "time", "unindent", @@ -16850,6 +16851,7 @@ dependencies = [ "tasks_ui", "telemetry", "telemetry_events", + "tempfile", "terminal_view", "theme", "theme_extension", @@ -16866,6 +16868,7 @@ dependencies = [ "vim", "vim_mode_setting", "welcome", + "which 6.0.3", "windows 0.58.0", "winresource", "workspace", diff --git a/crates/evals/src/eval.rs b/crates/evals/src/eval.rs index 911637aa34..ffca1c6c00 100644 --- a/crates/evals/src/eval.rs +++ b/crates/evals/src/eval.rs @@ -275,7 +275,11 @@ async fn run_evaluation( let db_path = Path::new(EVAL_DB_PATH); let api_key = std::env::var("OPENAI_API_KEY").unwrap(); let git_hosting_provider_registry = Arc::new(GitHostingProviderRegistry::new()); - let fs = Arc::new(RealFs::new(git_hosting_provider_registry, None)) as Arc; + let fs = Arc::new(RealFs::new( + git_hosting_provider_registry, + None, + PathBuf::from("/non/existent/askpass"), + )) as Arc; let clock = Arc::new(RealSystemClock); let client = cx .update(|cx| { diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 9af1d92dea..f9ba647c1d 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -248,6 +248,7 @@ impl From for proto::Timestamp { pub struct RealFs { git_hosting_provider_registry: Arc, git_binary_path: Option, + askpass_path: PathBuf, } pub trait FileHandle: Send + Sync + std::fmt::Debug { @@ -302,10 +303,12 @@ impl RealFs { pub fn new( git_hosting_provider_registry: Arc, git_binary_path: Option, + askpass_path: PathBuf, ) -> Self { Self { git_hosting_provider_registry, git_binary_path, + askpass_path, } } } @@ -769,6 +772,7 @@ impl Fs for RealFs { Some(Arc::new(RealGitRepository::new( repo, self.git_binary_path.clone(), + self.askpass_path.to_owned(), self.git_hosting_provider_registry.clone(), ))) } diff --git a/crates/git/Cargo.toml b/crates/git/Cargo.toml index 0473b1dd57..79ed6ea7ec 100644 --- a/crates/git/Cargo.toml +++ b/crates/git/Cargo.toml @@ -30,6 +30,7 @@ schemars.workspace = true serde.workspace = true smol.workspace = true sum_tree.workspace = true +tempfile.workspace = true text.workspace = true time.workspace = true url.workspace = true diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index 0edbd62fb9..60a16d86ba 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -10,8 +10,11 @@ use rope::Rope; use schemars::JsonSchema; use serde::Deserialize; use std::borrow::Borrow; +use std::env::temp_dir; use std::io::Write as _; -use std::process::Stdio; +use std::os::unix::fs::PermissionsExt as _; +use std::os::unix::net::UnixListener; +use std::process::{Command, Stdio}; use std::sync::LazyLock; use std::{ cmp::Ordering, @@ -200,6 +203,7 @@ impl std::fmt::Debug for dyn GitRepository { pub struct RealGitRepository { pub repository: Mutex, pub git_binary_path: PathBuf, + pub askpass_path: PathBuf, hosting_provider_registry: Arc, } @@ -207,11 +211,13 @@ impl RealGitRepository { pub fn new( repository: git2::Repository, git_binary_path: Option, + askpass_path: PathBuf, hosting_provider_registry: Arc, ) -> Self { Self { repository: Mutex::new(repository), git_binary_path: git_binary_path.unwrap_or_else(|| PathBuf::from("git")), + askpass_path, hosting_provider_registry, } } @@ -608,7 +614,10 @@ impl GitRepository for RealGitRepository { ) -> Result<()> { let working_directory = self.working_directory()?; - let output = new_std_command(&self.git_binary_path) + // We don't use the bundled git, so we can ensure that system + // credential management and transfer mechanisms are respected + let output = new_std_command("git") + .env("GIT_ASKPASS", &self.askpass_path) .current_dir(&working_directory) .args(["push", "--quiet"]) .args(options.map(|option| match option { @@ -632,9 +641,12 @@ impl GitRepository for RealGitRepository { fn pull(&self, branch_name: &str, remote_name: &str) -> Result<()> { let working_directory = self.working_directory()?; - let output = new_std_command(&self.git_binary_path) + // We don't use the bundled git, so we can ensure that system + // credential management and transfer mechanisms are respected + let output = new_std_command("git") + .env("GIT_ASKPASS", &self.askpass_path) .current_dir(&working_directory) - .args(["pull", "--quiet"]) + .args(["pull"]) .arg(remote_name) .arg(branch_name) .output()?; @@ -652,7 +664,10 @@ impl GitRepository for RealGitRepository { fn fetch(&self) -> Result<()> { let working_directory = self.working_directory()?; - let output = new_std_command(&self.git_binary_path) + // We don't use the bundled git, so we can ensure that system + // credential management and transfer mechanisms are respected + let output = new_std_command("git") + .env("GIT_ASKPASS", &self.askpass_path) .current_dir(&working_directory) .args(["fetch", "--quiet", "--all"]) .output()?; diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 8330ee6e1f..ae99138602 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -116,6 +116,7 @@ task.workspace = true tasks_ui.workspace = true telemetry.workspace = true telemetry_events.workspace = true +tempfile.workspace = true terminal_view.workspace = true theme.workspace = true theme_extension.workspace = true @@ -130,6 +131,7 @@ uuid.workspace = true vim.workspace = true vim_mode_setting.workspace = true welcome.workspace = true +which.workspace = true workspace.workspace = true zed_actions.workspace = true zeta.workspace = true diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index ce898b33a0..fc438b778a 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -256,9 +256,11 @@ fn main() { }; log::info!("Using git binary path: {:?}", git_binary_path); + let git_askpass_path = zed::git_askpass::get_askpass_dir(); let fs = Arc::new(RealFs::new( git_hosting_provider_registry.clone(), git_binary_path, + git_askpass_path.clone(), )); let user_settings_file_rx = watch_config_file( &app.background_executor(), @@ -301,6 +303,7 @@ fn main() { }); app.run(move |cx| { + zed::git_askpass::setup_git_askpass(git_askpass_path, cx); release_channel::init(app_version, cx); gpui_tokio::init(cx); if let Some(app_commit_sha) = app_commit_sha { diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 31d11104ea..41f384f9d6 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -1,4 +1,5 @@ mod app_menus; +pub mod git_askpass; pub mod inline_completion_registry; #[cfg(any(target_os = "linux", target_os = "freebsd"))] pub(crate) mod linux_prompts; diff --git a/crates/zed/src/zed/git_askpass.rs b/crates/zed/src/zed/git_askpass.rs new file mode 100644 index 0000000000..e5f312c1fa --- /dev/null +++ b/crates/zed/src/zed/git_askpass.rs @@ -0,0 +1,137 @@ +use std::{os::unix::fs::PermissionsExt, path::PathBuf}; + +use anyhow::{anyhow, Context, Result}; +use gpui::AsyncApp; +use smol::{ + io::{AsyncWriteExt as _, BufReader}, + net::unix::UnixListener, +}; +use ui::{App, Window}; +use util::{maybe, ResultExt as _}; +use workspace::Workspace; + +pub fn get_askpass_dir() -> PathBuf { + // TODO: bundle this script instead of creating it + let temp_dir = tempfile::Builder::new() + .prefix("zed-git-askpass-session") + .tempdir() + .unwrap(); + + // Create a domain socket listener to handle requests from the askpass program. + let askpass_socket = temp_dir.path().join("git_askpass.sock"); + + // Create an askpass script that communicates back to this process. + let askpass_script = format!( + "{shebang}\n{print_args} | {nc} -U {askpass_socket} 2> /dev/null \n", + // on macOS `brew install netcat` provides the GNU netcat implementation + // which does not support -U. + nc = if cfg!(target_os = "macos") { + "/usr/bin/nc" + } else { + "nc" + }, + askpass_socket = askpass_socket.display(), + print_args = "printf '%s\\0' \"$@\"", + shebang = "#!/bin/sh", + ); + let askpass_script_path = temp_dir.path().join("askpass.sh"); + std::fs::write(&askpass_script_path, &askpass_script).unwrap(); + std::fs::set_permissions(&askpass_script_path, std::fs::Permissions::from_mode(0o755)).unwrap(); + + PathBuf::from(askpass_script) +} + +pub fn setup_git_askpass(askpasss_file: PathBuf, cx: &mut App) { + maybe!({ + anyhow::ensure!( + which::which("nc").is_ok(), + "Cannot find `nc` command (netcat), which is required to connect over SSH." + ); + + // TODO: REMOVE THIS ONCE WE HAVE A WAY OF BUNDLING AN ASKPASS SCRIPT + let askpass_socket = askpasss_file.parent().unwrap().join("git_askpass.sock"); + + let listener = + UnixListener::bind(&askpass_socket).context("failed to create askpass socket")?; + + cx.spawn({ + |mut cx| async move { + while let Ok((mut stream, _)) = listener.accept().await { + let mut buffer = Vec::new(); + let mut reader = BufReader::new(&mut stream); + if smol::io::AsyncBufReadExt::read_until(&mut reader, b'\0', &mut buffer) + .await + .is_err() + { + buffer.clear(); + } + let password_prompt = String::from_utf8_lossy(&buffer); + if let Some(Ok(password)) = ask_password(&password_prompt, &mut cx) + .await + .context("failed to get ssh password") + .log_err() + { + stream.write_all(password.as_bytes()).await.log_err(); + } else { + stream.write("\n".as_bytes()).await.log_err(); + } + + stream.flush().await.log_err(); + stream.close().await.log_err(); + } + } + }) + .detach(); + + Ok(()) + }) + .log_err(); +} + +async fn ask_password(prompt: &str, cx: &mut AsyncApp) -> Option> { + let mut workspace = get_workspace(cx, |window| window.is_window_active()); + if workspace.is_none() { + workspace = get_workspace(cx, |_| true); + } + + let Some(workspace) = workspace else { + return None; + }; + + // DO THINGS WITH THE WORKSPACE + // pop the askpass modal, get the output out of a oneshot, and we're good to go + None +} + +fn get_workspace( + cx: &mut AsyncApp, + predicate: impl Fn(&mut Window) -> bool, +) -> Option> { + let workspace = cx + .update(|cx| { + for window in cx.windows() { + let workspace = window + .update(cx, |view, window, _| { + if predicate(window) { + if let Ok(workspace) = view.downcast::() { + return Some(workspace); + } + } + return None; + }) + .ok() + .flatten(); + + if let Some(workspace) = workspace { + return Some(workspace); + } else { + continue; + } + } + + None + }) + .ok()?; + + workspace +}