Compare commits

...

1 Commits

Author SHA1 Message Date
Mikayla Maki
7c2dffc792 Wire through IPC mechanism for GIT_ASKPASS
co-authored-by: julia@zed.dev
2025-02-28 20:56:11 -08:00
9 changed files with 176 additions and 6 deletions

3
Cargo.lock generated
View File

@@ -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",

View File

@@ -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<dyn Fs>;
let fs = Arc::new(RealFs::new(
git_hosting_provider_registry,
None,
PathBuf::from("/non/existent/askpass"),
)) as Arc<dyn Fs>;
let clock = Arc::new(RealSystemClock);
let client = cx
.update(|cx| {

View File

@@ -248,6 +248,7 @@ impl From<MTime> for proto::Timestamp {
pub struct RealFs {
git_hosting_provider_registry: Arc<GitHostingProviderRegistry>,
git_binary_path: Option<PathBuf>,
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<GitHostingProviderRegistry>,
git_binary_path: Option<PathBuf>,
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(),
)))
}

View File

@@ -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

View File

@@ -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<git2::Repository>,
pub git_binary_path: PathBuf,
pub askpass_path: PathBuf,
hosting_provider_registry: Arc<GitHostingProviderRegistry>,
}
@@ -207,11 +211,13 @@ impl RealGitRepository {
pub fn new(
repository: git2::Repository,
git_binary_path: Option<PathBuf>,
askpass_path: PathBuf,
hosting_provider_registry: Arc<GitHostingProviderRegistry>,
) -> 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()?;

View File

@@ -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

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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<Result<String>> {
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<gpui::Entity<Workspace>> {
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::<Workspace>() {
return Some(workspace);
}
}
return None;
})
.ok()
.flatten();
if let Some(workspace) = workspace {
return Some(workspace);
} else {
continue;
}
}
None
})
.ok()?;
workspace
}