diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index be11dc5a54b6c4..d08dc95d02bc61 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -295,7 +295,10 @@ jobs: # Since the Windows runners are stateful, so we need to remove the config file to prevent potential bug. - name: Clean CI config file if: always() - run: Remove-Item -Path "${{ env.CARGO_HOME }}/config.toml" -Force + run: | + if (Test-Path "${{ env.CARGO_HOME }}/config.toml") { + Remove-Item -Path "${{ env.CARGO_HOME }}/config.toml" -Force + } # Windows CI takes twice as long as our other platforms and fast github hosted runners are expensive. # But we still want to do CI, so let's only run tests on main and come back to this when we're @@ -364,7 +367,10 @@ jobs: # Since the Windows runners are stateful, so we need to remove the config file to prevent potential bug. - name: Clean CI config file if: always() - run: Remove-Item -Path "${{ env.CARGO_HOME }}/config.toml" -Force + run: | + if (Test-Path "${{ env.CARGO_HOME }}/config.toml") { + Remove-Item -Path "${{ env.CARGO_HOME }}/config.toml" -Force + } bundle-mac: timeout-minutes: 120 diff --git a/Cargo.lock b/Cargo.lock index d1769079ff0354..736ed2617f6019 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -257,9 +257,9 @@ checksum = "34cd60c5e3152cef0a592f1b296f1cc93715d89d2551d85315828c3a09575ff4" [[package]] name = "anyhow" -version = "1.0.97" +version = "1.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" +checksum = "6b964d184e89d9b6b67dd2715bc8e74cf3107fb2b529990c90cf517326150bf4" [[package]] name = "approx" @@ -358,6 +358,19 @@ dependencies = [ "zbus", ] +[[package]] +name = "askpass" +version = "0.1.0" +dependencies = [ + "anyhow", + "futures 0.3.31", + "gpui", + "smol", + "tempfile", + "util", + "which 6.0.3", +] + [[package]] name = "assets" version = "0.1.0" @@ -1011,9 +1024,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.87" +version = "0.1.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d556ec1359574147ec0c4fc5eb525f3f23263a592b1a9c07e0a75b427de55c97" +checksum = "644dd749086bf3771a2fbc5f256fdb982d53f011c7d5d560304eafeecebce79d" dependencies = [ "proc-macro2", "quote", @@ -5364,9 +5377,11 @@ name = "git" version = "0.1.0" dependencies = [ "anyhow", + "askpass", "async-trait", "collections", "derive_more", + "futures 0.3.31", "git2", "gpui", "http_client", @@ -5380,7 +5395,6 @@ dependencies = [ "serde_json", "smol", "sum_tree", - "tempfile", "text", "time", "unindent", @@ -5424,6 +5438,7 @@ name = "git_ui" version = "0.1.0" dependencies = [ "anyhow", + "askpass", "buffer_diff", "collections", "component", @@ -10258,6 +10273,7 @@ version = "0.1.0" dependencies = [ "aho-corasick", "anyhow", + "askpass", "async-trait", "buffer_diff", "client", @@ -11079,6 +11095,7 @@ name = "remote" version = "0.1.0" dependencies = [ "anyhow", + "askpass", "async-trait", "collections", "fs", @@ -11099,7 +11116,6 @@ dependencies = [ "tempfile", "thiserror 1.0.69", "util", - "which 6.0.3", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index df9b8c8466d7dc..1f6cae0b089738 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ resolver = "2" members = [ "crates/activity_indicator", "crates/anthropic", + "crates/askpass", "crates/assets", "crates/assistant", "crates/assistant2", @@ -207,6 +208,7 @@ edition = "2021" activity_indicator = { path = "crates/activity_indicator" } ai = { path = "crates/ai" } anthropic = { path = "crates/anthropic" } +askpass = { path = "crates/askpass" } assets = { path = "crates/assets" } assistant = { path = "crates/assistant" } assistant2 = { path = "crates/assistant2" } diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 81785a8db91be9..0a4df0747dd0b5 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -739,7 +739,7 @@ "tab": "git_panel::FocusEditor", "shift-tab": "git_panel::FocusEditor", "escape": "git_panel::ToggleFocus", - "ctrl-enter": "git::ShowCommitEditor", + "ctrl-enter": "git::Commit", "alt-enter": "menu::SecondaryConfirm" } }, @@ -753,7 +753,13 @@ { "context": "GitDiff > Editor", "bindings": { - "ctrl-enter": "git::ShowCommitEditor" + "ctrl-enter": "git::Commit" + } + }, + { + "context": "AskPass > Editor", + "bindings": { + "enter": "menu::Confirm" } }, { diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 0d6077e5a75cac..fcdb8aa42c077c 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -760,14 +760,21 @@ "tab": "git_panel::FocusEditor", "shift-tab": "git_panel::FocusEditor", "escape": "git_panel::ToggleFocus", - "cmd-enter": "git::ShowCommitEditor" + "cmd-enter": "git::Commit" } }, { "context": "GitDiff > Editor", "use_key_equivalents": true, "bindings": { - "cmd-enter": "git::ShowCommitEditor" + "cmd-enter": "git::Commit" + } + }, + { + "context": "AskPass > Editor", + "use_key_equivalents": true, + "bindings": { + "enter": "menu::Confirm" } }, { @@ -778,7 +785,8 @@ "cmd-enter": "git::Commit", "tab": "git_panel::FocusChanges", "shift-tab": "git_panel::FocusChanges", - "alt-up": "git_panel::FocusChanges" + "alt-up": "git_panel::FocusChanges", + "shift-escape": "git::ExpandCommitEditor" } }, { diff --git a/crates/askpass/Cargo.toml b/crates/askpass/Cargo.toml new file mode 100644 index 00000000000000..f00e37efffb242 --- /dev/null +++ b/crates/askpass/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "askpass" +version = "0.1.0" +edition.workspace = true +publish.workspace = true +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +path = "src/askpass.rs" + +[dependencies] +anyhow.workspace = true +futures.workspace = true +gpui.workspace = true +smol.workspace = true +tempfile.workspace = true +util.workspace = true +which.workspace = true diff --git a/crates/askpass/LICENSE-APACHE b/crates/askpass/LICENSE-APACHE new file mode 120000 index 00000000000000..1cd601d0a3affa --- /dev/null +++ b/crates/askpass/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/askpass/src/askpass.rs b/crates/askpass/src/askpass.rs new file mode 100644 index 00000000000000..cf0349759e0254 --- /dev/null +++ b/crates/askpass/src/askpass.rs @@ -0,0 +1,194 @@ +use std::path::{Path, PathBuf}; +use std::time::Duration; + +#[cfg(unix)] +use anyhow::Context as _; +use futures::channel::{mpsc, oneshot}; +#[cfg(unix)] +use futures::{io::BufReader, AsyncBufReadExt as _}; +#[cfg(unix)] +use futures::{select_biased, AsyncWriteExt as _, FutureExt as _}; +use futures::{SinkExt, StreamExt}; +use gpui::{AsyncApp, BackgroundExecutor, Task}; +#[cfg(unix)] +use smol::fs; +#[cfg(unix)] +use smol::{fs::unix::PermissionsExt as _, net::unix::UnixListener}; +#[cfg(unix)] +use util::ResultExt as _; + +#[derive(PartialEq, Eq)] +pub enum AskPassResult { + CancelledByUser, + Timedout, +} + +pub struct AskPassDelegate { + tx: mpsc::UnboundedSender<(String, oneshot::Sender)>, + _task: Task<()>, +} + +impl AskPassDelegate { + pub fn new( + cx: &mut AsyncApp, + password_prompt: impl Fn(String, oneshot::Sender, &mut AsyncApp) + Send + Sync + 'static, + ) -> Self { + let (tx, mut rx) = mpsc::unbounded::<(String, oneshot::Sender)>(); + let task = cx.spawn(|mut cx| async move { + while let Some((prompt, channel)) = rx.next().await { + password_prompt(prompt, channel, &mut cx); + } + }); + Self { tx, _task: task } + } + + pub async fn ask_password(&mut self, prompt: String) -> anyhow::Result { + let (tx, rx) = oneshot::channel(); + self.tx.send((prompt, tx)).await?; + Ok(rx.await?) + } +} + +#[cfg(unix)] +pub struct AskPassSession { + script_path: PathBuf, + _askpass_task: Task<()>, + askpass_opened_rx: Option>, + askpass_kill_master_rx: Option>, +} + +#[cfg(unix)] +impl AskPassSession { + /// This will create a new AskPassSession. + /// You must retain this session until the master process exits. + #[must_use] + pub async fn new( + executor: &BackgroundExecutor, + mut delegate: AskPassDelegate, + ) -> anyhow::Result { + let temp_dir = tempfile::Builder::new().prefix("zed-askpass").tempdir()?; + let askpass_socket = temp_dir.path().join("askpass.sock"); + let askpass_script_path = temp_dir.path().join("askpass.sh"); + let (askpass_opened_tx, askpass_opened_rx) = oneshot::channel::<()>(); + let listener = + UnixListener::bind(&askpass_socket).context("failed to create askpass socket")?; + + let (askpass_kill_master_tx, askpass_kill_master_rx) = oneshot::channel::<()>(); + let mut kill_tx = Some(askpass_kill_master_tx); + + let askpass_task = executor.spawn(async move { + let mut askpass_opened_tx = Some(askpass_opened_tx); + + while let Ok((mut stream, _)) = listener.accept().await { + if let Some(askpass_opened_tx) = askpass_opened_tx.take() { + askpass_opened_tx.send(()).ok(); + } + let mut buffer = Vec::new(); + let mut reader = BufReader::new(&mut stream); + if reader.read_until(b'\0', &mut buffer).await.is_err() { + buffer.clear(); + } + let prompt = String::from_utf8_lossy(&buffer); + if let Some(password) = delegate + .ask_password(prompt.to_string()) + .await + .context("failed to get askpass password") + .log_err() + { + stream.write_all(password.as_bytes()).await.log_err(); + } else { + if let Some(kill_tx) = kill_tx.take() { + kill_tx.send(()).log_err(); + } + // note: we expect the caller to drop this task when it's done. + // We need to keep the stream open until the caller is done to avoid + // spurious errors from ssh. + std::future::pending::<()>().await; + drop(stream); + } + } + drop(temp_dir) + }); + + anyhow::ensure!( + which::which("nc").is_ok(), + "Cannot find `nc` command (netcat), which is required to connect over SSH." + ); + + // 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", + ); + fs::write(&askpass_script_path, askpass_script).await?; + fs::set_permissions(&askpass_script_path, std::fs::Permissions::from_mode(0o755)).await?; + + Ok(Self { + script_path: askpass_script_path, + _askpass_task: askpass_task, + askpass_kill_master_rx: Some(askpass_kill_master_rx), + askpass_opened_rx: Some(askpass_opened_rx), + }) + } + + pub fn script_path(&self) -> &Path { + &self.script_path + } + + // This will run the askpass task forever, resolving as many authentication requests as needed. + // The caller is responsible for examining the result of their own commands and cancelling this + // future when this is no longer needed. Note that this can only be called once, but due to the + // drop order this takes an &mut, so you can `drop()` it after you're done with the master process. + pub async fn run(&mut self) -> AskPassResult { + let connection_timeout = Duration::from_secs(10); + let askpass_opened_rx = self.askpass_opened_rx.take().expect("Only call run once"); + let askpass_kill_master_rx = self + .askpass_kill_master_rx + .take() + .expect("Only call run once"); + + select_biased! { + _ = askpass_opened_rx.fuse() => { + // Note: this await can only resolve after we are dropped. + askpass_kill_master_rx.await.ok(); + return AskPassResult::CancelledByUser + } + + _ = futures::FutureExt::fuse(smol::Timer::after(connection_timeout)) => { + return AskPassResult::Timedout + } + } + } +} + +#[cfg(not(unix))] +pub struct AskPassSession { + path: PathBuf, +} + +#[cfg(not(unix))] +impl AskPassSession { + pub async fn new(_: &BackgroundExecutor, _: AskPassDelegate) -> anyhow::Result { + Ok(Self { + path: PathBuf::new(), + }) + } + + pub fn script_path(&self) -> &Path { + &self.path + } + + pub async fn run(&mut self) -> AskPassResult { + futures::FutureExt::fuse(smol::Timer::after(Duration::from_secs(10))).await; + AskPassResult::Timedout + } +} diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index fc6290837b8100..1665945dceef3a 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -393,9 +393,6 @@ impl Server { .add_request_handler(forward_mutating_project_request::) .add_request_handler(forward_mutating_project_request::) .add_request_handler(forward_mutating_project_request::) - .add_request_handler(forward_mutating_project_request::) - .add_request_handler(forward_mutating_project_request::) - .add_request_handler(forward_mutating_project_request::) .add_request_handler(forward_mutating_project_request::) .add_request_handler(forward_mutating_project_request::) .add_request_handler(forward_mutating_project_request::) diff --git a/crates/git/Cargo.toml b/crates/git/Cargo.toml index f32d704b3e043d..6458a796bc840c 100644 --- a/crates/git/Cargo.toml +++ b/crates/git/Cargo.toml @@ -16,6 +16,7 @@ test-support = [] [dependencies] anyhow.workspace = true +askpass.workspace = true async-trait.workspace = true collections.workspace = true derive_more.workspace = true @@ -34,7 +35,7 @@ text.workspace = true time.workspace = true url.workspace = true util.workspace = true -tempfile.workspace = true +futures.workspace = true [dev-dependencies] pretty_assertions.workspace = true diff --git a/crates/git/src/git.rs b/crates/git/src/git.rs index c99b260577a2c4..99d3d744a0cbf8 100644 --- a/crates/git/src/git.rs +++ b/crates/git/src/git.rs @@ -8,9 +8,6 @@ pub mod status; use anyhow::{anyhow, Context as _, Result}; use gpui::action_with_deprecated_aliases; use gpui::actions; -use gpui::impl_actions; -use repository::PushOptions; -use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::ffi::OsStr; use std::fmt; @@ -31,13 +28,6 @@ pub static COMMIT_MESSAGE: LazyLock<&'static OsStr> = LazyLock::new(|| OsStr::new("COMMIT_EDITMSG")); pub static INDEX_LOCK: LazyLock<&'static OsStr> = LazyLock::new(|| OsStr::new("index.lock")); -#[derive(Debug, Copy, Clone, PartialEq, Deserialize, JsonSchema)] -pub struct Push { - pub options: Option, -} - -impl_actions!(git, [Push]); - actions!( git, [ @@ -54,10 +44,12 @@ actions!( RestoreTrackedFiles, TrashUntrackedFiles, Uncommit, + Push, + ForcePush, Pull, Fetch, Commit, - ShowCommitEditor, + ExpandCommitEditor ] ); action_with_deprecated_aliases!(git, RestoreFile, ["editor::RevertFile"]); diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index b1185c685670bb..834f55bd338d4a 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -2,7 +2,9 @@ use crate::status::FileStatus; use crate::GitHostingProviderRegistry; use crate::{blame::Blame, status::GitStatus}; use anyhow::{anyhow, Context, Result}; +use askpass::{AskPassResult, AskPassSession}; use collections::{HashMap, HashSet}; +use futures::{select_biased, FutureExt as _}; use git2::BranchType; use gpui::SharedString; use parking_lot::Mutex; @@ -11,8 +13,6 @@ use schemars::JsonSchema; use serde::Deserialize; use std::borrow::Borrow; use std::io::Write as _; -#[cfg(not(windows))] -use std::os::unix::fs::PermissionsExt; use std::process::Stdio; use std::sync::LazyLock; use std::{ @@ -21,9 +21,11 @@ use std::{ sync::Arc, }; use sum_tree::MapSeekTarget; -use util::command::new_std_command; +use util::command::{new_smol_command, new_std_command}; use util::ResultExt; +pub const REMOTE_CANCELLED_BY_USER: &str = "Operation cancelled by user"; + #[derive(Clone, Debug, Hash, PartialEq, Eq)] pub struct Branch { pub is_head: bool, @@ -200,9 +202,16 @@ pub trait GitRepository: Send + Sync { branch_name: &str, upstream_name: &str, options: Option, + askpass: AskPassSession, ) -> Result; - fn pull(&self, branch_name: &str, upstream_name: &str) -> Result; - fn fetch(&self) -> Result; + + fn pull( + &self, + branch_name: &str, + upstream_name: &str, + askpass: AskPassSession, + ) -> Result; + fn fetch(&self, askpass: AskPassSession) -> Result; fn get_remotes(&self, branch_name: Option<&str>) -> Result>; @@ -578,7 +587,6 @@ impl GitRepository for RealGitRepository { .args(paths.iter().map(|p| p.as_ref())) .output()?; - // TODO: Get remote response out of this and show it to the user if !output.status.success() { return Err(anyhow!( "Failed to stage paths:\n{}", @@ -599,7 +607,6 @@ impl GitRepository for RealGitRepository { .args(paths.iter().map(|p| p.as_ref())) .output()?; - // TODO: Get remote response out of this and show it to the user if !output.status.success() { return Err(anyhow!( "Failed to unstage:\n{}", @@ -625,7 +632,6 @@ impl GitRepository for RealGitRepository { let output = cmd.output()?; - // TODO: Get remote response out of this and show it to the user if !output.status.success() { return Err(anyhow!( "Failed to commit:\n{}", @@ -640,15 +646,15 @@ impl GitRepository for RealGitRepository { branch_name: &str, remote_name: &str, options: Option, + ask_pass: AskPassSession, ) -> Result { let working_directory = self.working_directory()?; - // We do this on every operation to ensure that the askpass script exists and is executable. - #[cfg(not(windows))] - let (askpass_script_path, _temp_dir) = setup_askpass()?; - - let mut command = new_std_command("git"); + let mut command = new_smol_command("git"); command + .env("GIT_ASKPASS", ask_pass.script_path()) + .env("SSH_ASKPASS", ask_pass.script_path()) + .env("SSH_ASKPASS_REQUIRE", "force") .current_dir(&working_directory) .args(["push"]) .args(options.map(|option| match option { @@ -657,91 +663,46 @@ impl GitRepository for RealGitRepository { })) .arg(remote_name) .arg(format!("{}:{}", branch_name, branch_name)); + let git_process = command.spawn()?; - #[cfg(not(windows))] - { - command.env("GIT_ASKPASS", askpass_script_path); - } - - let output = command.output()?; - - if !output.status.success() { - return Err(anyhow!( - "Failed to push:\n{}", - String::from_utf8_lossy(&output.stderr) - )); - } else { - return Ok(RemoteCommandOutput { - stdout: String::from_utf8_lossy(&output.stdout).to_string(), - stderr: String::from_utf8_lossy(&output.stderr).to_string(), - }); - } + run_remote_command(ask_pass, git_process) } - fn pull(&self, branch_name: &str, remote_name: &str) -> Result { + fn pull( + &self, + branch_name: &str, + remote_name: &str, + ask_pass: AskPassSession, + ) -> Result { let working_directory = self.working_directory()?; - // We do this on every operation to ensure that the askpass script exists and is executable. - #[cfg(not(windows))] - let (askpass_script_path, _temp_dir) = setup_askpass()?; - - let mut command = new_std_command("git"); + let mut command = new_smol_command("git"); command + .env("GIT_ASKPASS", ask_pass.script_path()) + .env("SSH_ASKPASS", ask_pass.script_path()) + .env("SSH_ASKPASS_REQUIRE", "force") .current_dir(&working_directory) .args(["pull"]) .arg(remote_name) .arg(branch_name); + let git_process = command.spawn()?; - #[cfg(not(windows))] - { - command.env("GIT_ASKPASS", askpass_script_path); - } - - let output = command.output()?; - - if !output.status.success() { - return Err(anyhow!( - "Failed to pull:\n{}", - String::from_utf8_lossy(&output.stderr) - )); - } else { - return Ok(RemoteCommandOutput { - stdout: String::from_utf8_lossy(&output.stdout).to_string(), - stderr: String::from_utf8_lossy(&output.stderr).to_string(), - }); - } + run_remote_command(ask_pass, git_process) } - fn fetch(&self) -> Result { + fn fetch(&self, ask_pass: AskPassSession) -> Result { let working_directory = self.working_directory()?; - // We do this on every operation to ensure that the askpass script exists and is executable. - #[cfg(not(windows))] - let (askpass_script_path, _temp_dir) = setup_askpass()?; - - let mut command = new_std_command("git"); + let mut command = new_smol_command("git"); command + .env("GIT_ASKPASS", ask_pass.script_path()) + .env("SSH_ASKPASS", ask_pass.script_path()) + .env("SSH_ASKPASS_REQUIRE", "force") .current_dir(&working_directory) .args(["fetch", "--all"]); + let git_process = command.spawn()?; - #[cfg(not(windows))] - { - command.env("GIT_ASKPASS", askpass_script_path); - } - - let output = command.output()?; - - if !output.status.success() { - return Err(anyhow!( - "Failed to fetch:\n{}", - String::from_utf8_lossy(&output.stderr) - )); - } else { - return Ok(RemoteCommandOutput { - stdout: String::from_utf8_lossy(&output.stdout).to_string(), - stderr: String::from_utf8_lossy(&output.stderr).to_string(), - }); - } + run_remote_command(ask_pass, git_process) } fn get_remotes(&self, branch_name: Option<&str>) -> Result> { @@ -835,16 +796,38 @@ impl GitRepository for RealGitRepository { } } -#[cfg(not(windows))] -fn setup_askpass() -> Result<(PathBuf, tempfile::TempDir), anyhow::Error> { - let temp_dir = tempfile::Builder::new() - .prefix("zed-git-askpass") - .tempdir()?; - let askpass_script = "#!/bin/sh\necho ''"; - let askpass_script_path = temp_dir.path().join("git-askpass.sh"); - std::fs::write(&askpass_script_path, askpass_script)?; - std::fs::set_permissions(&askpass_script_path, std::fs::Permissions::from_mode(0o755))?; - Ok((askpass_script_path, temp_dir)) +fn run_remote_command( + mut ask_pass: AskPassSession, + git_process: smol::process::Child, +) -> std::result::Result { + smol::block_on(async { + select_biased! { + result = ask_pass.run().fuse() => { + match result { + AskPassResult::CancelledByUser => { + Err(anyhow!(REMOTE_CANCELLED_BY_USER))? + } + AskPassResult::Timedout => { + Err(anyhow!("Connecting to host timed out"))? + } + } + } + output = git_process.output().fuse() => { + let output = output?; + if !output.status.success() { + Err(anyhow!( + "Operation failed:\n{}", + String::from_utf8_lossy(&output.stderr) + )) + } else { + Ok(RemoteCommandOutput { + stdout: String::from_utf8_lossy(&output.stdout).to_string(), + stderr: String::from_utf8_lossy(&output.stderr).to_string(), + }) + } + } + } + }) } #[derive(Debug, Clone)] @@ -1040,15 +1023,21 @@ impl GitRepository for FakeGitRepository { _branch: &str, _remote: &str, _options: Option, + _ask_pass: AskPassSession, ) -> Result { unimplemented!() } - fn pull(&self, _branch: &str, _remote: &str) -> Result { + fn pull( + &self, + _branch: &str, + _remote: &str, + _ask_pass: AskPassSession, + ) -> Result { unimplemented!() } - fn fetch(&self) -> Result { + fn fetch(&self, _ask_pass: AskPassSession) -> Result { unimplemented!() } diff --git a/crates/git_ui/Cargo.toml b/crates/git_ui/Cargo.toml index 8e4eabf59fc744..aa95e788797379 100644 --- a/crates/git_ui/Cargo.toml +++ b/crates/git_ui/Cargo.toml @@ -18,6 +18,7 @@ test-support = ["multi_buffer/test-support"] [dependencies] anyhow.workspace = true +askpass.workspace= true buffer_diff.workspace = true collections.workspace = true component.workspace = true diff --git a/crates/git_ui/src/askpass_modal.rs b/crates/git_ui/src/askpass_modal.rs new file mode 100644 index 00000000000000..17308ee2c24526 --- /dev/null +++ b/crates/git_ui/src/askpass_modal.rs @@ -0,0 +1,101 @@ +use editor::Editor; +use futures::channel::oneshot; +use gpui::{AppContext, DismissEvent, Entity, EventEmitter, Focusable, Styled}; +use ui::{ + div, h_flex, v_flex, ActiveTheme, App, Context, DynamicSpacing, Headline, HeadlineSize, Icon, + IconName, IconSize, InteractiveElement, IntoElement, ParentElement, Render, SharedString, + StyledExt, StyledTypography, Window, +}; +use workspace::ModalView; + +pub(crate) struct AskPassModal { + operation: SharedString, + prompt: SharedString, + editor: Entity, + tx: Option>, +} + +impl EventEmitter for AskPassModal {} +impl ModalView for AskPassModal {} +impl Focusable for AskPassModal { + fn focus_handle(&self, cx: &App) -> gpui::FocusHandle { + self.editor.focus_handle(cx) + } +} + +impl AskPassModal { + pub fn new( + operation: SharedString, + prompt: SharedString, + tx: oneshot::Sender, + window: &mut Window, + cx: &mut Context, + ) -> Self { + let editor = cx.new(|cx| { + let mut editor = Editor::single_line(window, cx); + if prompt.contains("yes/no") { + editor.set_masked(false, cx); + } else { + editor.set_masked(true, cx); + } + editor + }); + Self { + operation, + prompt, + editor, + tx: Some(tx), + } + } + + fn cancel(&mut self, _: &menu::Cancel, _window: &mut Window, cx: &mut Context) { + cx.emit(DismissEvent); + } + + fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context) { + if let Some(tx) = self.tx.take() { + tx.send(self.editor.read(cx).text(cx)).ok(); + } + cx.emit(DismissEvent); + } +} + +impl Render for AskPassModal { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + v_flex() + .key_context("PasswordPrompt") + .on_action(cx.listener(Self::cancel)) + .on_action(cx.listener(Self::confirm)) + .elevation_2(cx) + .size_full() + .font_buffer(cx) + .child( + h_flex() + .px(DynamicSpacing::Base12.rems(cx)) + .pt(DynamicSpacing::Base08.rems(cx)) + .pb(DynamicSpacing::Base04.rems(cx)) + .rounded_t_md() + .w_full() + .gap_1p5() + .child(Icon::new(IconName::GitBranch).size(IconSize::XSmall)) + .child(h_flex().gap_1().overflow_x_hidden().child( + div().max_w_96().overflow_x_hidden().text_ellipsis().child( + Headline::new(self.operation.clone()).size(HeadlineSize::XSmall), + ), + )), + ) + .child( + div() + .text_buffer(cx) + .py_2() + .px_3() + .bg(cx.theme().colors().editor_background) + .border_t_1() + .border_color(cx.theme().colors().border_variant) + .size_full() + .overflow_hidden() + .child(self.prompt.clone()) + .child(self.editor.clone()), + ) + } +} diff --git a/crates/git_ui/src/commit_modal.rs b/crates/git_ui/src/commit_modal.rs index 0cd76049e654c6..37541a0c844464 100644 --- a/crates/git_ui/src/commit_modal.rs +++ b/crates/git_ui/src/commit_modal.rs @@ -2,7 +2,7 @@ use crate::branch_picker::{self, BranchList}; use crate::git_panel::{commit_message_editor, GitPanel}; -use git::{Commit, ShowCommitEditor}; +use git::Commit; use panel::{panel_button, panel_editor_style, panel_filled_button}; use ui::{prelude::*, KeybindingHint, PopoverMenu, Tooltip}; @@ -109,30 +109,34 @@ struct RestoreDock { impl CommitModal { pub fn register(workspace: &mut Workspace, _: &mut Window, _cx: &mut Context) { - workspace.register_action(|workspace, _: &ShowCommitEditor, window, cx| { - let Some(git_panel) = workspace.panel::(cx) else { - return; - }; - - git_panel.update(cx, |git_panel, cx| { - git_panel.set_modal_open(true, cx); - }); + workspace.register_action(|workspace, _: &Commit, window, cx| { + CommitModal::toggle(workspace, window, cx); + }); + } - let dock = workspace.dock_at_position(git_panel.position(window, cx)); - let is_open = dock.read(cx).is_open(); - let active_index = dock.read(cx).active_panel_index(); - let dock = dock.downgrade(); - let restore_dock_position = RestoreDock { - dock, - is_open, - active_index, - }; + pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context<'_, Workspace>) { + let Some(git_panel) = workspace.panel::(cx) else { + return; + }; - workspace.open_panel::(window, cx); - workspace.toggle_modal(window, cx, move |window, cx| { - CommitModal::new(git_panel, restore_dock_position, window, cx) - }) + git_panel.update(cx, |git_panel, cx| { + git_panel.set_modal_open(true, cx); }); + + let dock = workspace.dock_at_position(git_panel.position(window, cx)); + let is_open = dock.read(cx).is_open(); + let active_index = dock.read(cx).active_panel_index(); + let dock = dock.downgrade(); + let restore_dock_position = RestoreDock { + dock, + is_open, + active_index, + }; + + workspace.open_panel::(window, cx); + workspace.toggle_modal(window, cx, move |window, cx| { + CommitModal::new(git_panel, restore_dock_position, window, cx) + }) } fn new( diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index fe16099d0d82de..631ee643231e3c 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -1,10 +1,14 @@ -use crate::branch_picker::{self}; +use crate::askpass_modal::AskPassModal; +use crate::branch_picker; +use crate::commit_modal::CommitModal; use crate::git_panel_settings::StatusStyle; use crate::remote_output_toast::{RemoteAction, RemoteOutputToast}; use crate::{ git_panel_settings::GitPanelSettings, git_status_icon, repository_selector::RepositorySelector, }; use crate::{picker_prompt, project_diff, ProjectDiff}; +use anyhow::Result; +use askpass::AskPassDelegate; use db::kvp::KEY_VALUE_STORE; use editor::commit_tooltip::CommitTooltip; @@ -101,7 +105,7 @@ const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50); pub fn init(cx: &mut App) { cx.observe_new( - |workspace: &mut Workspace, _window, _cx: &mut Context| { + |workspace: &mut Workspace, _window, _: &mut Context| { workspace.register_action(|workspace, _: &ToggleFocus, window, cx| { workspace.toggle_panel_focus::(window, cx); }); @@ -1465,13 +1469,19 @@ index 1234567..abcdef0 100644 cx.notify(); } - pub(crate) fn fetch(&mut self, _: &git::Fetch, _window: &mut Window, cx: &mut Context) { + pub(crate) fn fetch(&mut self, window: &mut Window, cx: &mut Context) { + if !self.can_push_and_pull(cx) { + return; + } + let Some(repo) = self.active_repository.clone() else { return; }; let guard = self.start_remote_operation(); - let fetch = repo.read(cx).fetch(); + let askpass = self.askpass_delegate("git fetch", window, cx); cx.spawn(|this, mut cx| async move { + let fetch = repo.update(&mut cx, |repo, cx| repo.fetch(askpass, cx))?; + let remote_message = fetch.await?; drop(guard); this.update(&mut cx, |this, cx| { @@ -1492,7 +1502,10 @@ index 1234567..abcdef0 100644 .detach_and_log_err(cx); } - pub(crate) fn pull(&mut self, _: &git::Pull, window: &mut Window, cx: &mut Context) { + pub(crate) fn pull(&mut self, window: &mut Window, cx: &mut Context) { + if !self.can_push_and_pull(cx) { + return; + } let Some(repo) = self.active_repository.clone() else { return; }; @@ -1501,7 +1514,7 @@ index 1234567..abcdef0 100644 }; let branch = branch.clone(); let remote = self.get_current_remote(window, cx); - cx.spawn(move |this, mut cx| async move { + cx.spawn_in(window, move |this, mut cx| async move { let remote = match remote.await { Ok(Some(remote)) => remote, Ok(None) => { @@ -1515,12 +1528,16 @@ index 1234567..abcdef0 100644 } }; + let askpass = this.update_in(&mut cx, |this, window, cx| { + this.askpass_delegate(format!("git pull {}", remote.name), window, cx) + })?; + let guard = this .update(&mut cx, |this, _| this.start_remote_operation()) .ok(); - let pull = repo.update(&mut cx, |repo, _cx| { - repo.pull(branch.name.clone(), remote.name.clone()) + let pull = repo.update(&mut cx, |repo, cx| { + repo.pull(branch.name.clone(), remote.name.clone(), askpass, cx) })?; let remote_message = pull.await?; @@ -1539,7 +1556,10 @@ index 1234567..abcdef0 100644 .detach_and_log_err(cx); } - pub(crate) fn push(&mut self, action: &git::Push, window: &mut Window, cx: &mut Context) { + pub(crate) fn push(&mut self, force_push: bool, window: &mut Window, cx: &mut Context) { + if !self.can_push_and_pull(cx) { + return; + } let Some(repo) = self.active_repository.clone() else { return; }; @@ -1547,10 +1567,14 @@ index 1234567..abcdef0 100644 return; }; let branch = branch.clone(); - let options = action.options; + let options = if force_push { + PushOptions::Force + } else { + PushOptions::SetUpstream + }; let remote = self.get_current_remote(window, cx); - cx.spawn(move |this, mut cx| async move { + cx.spawn_in(window, move |this, mut cx| async move { let remote = match remote.await { Ok(Some(remote)) => remote, Ok(None) => { @@ -1564,16 +1588,25 @@ index 1234567..abcdef0 100644 } }; + let askpass_delegate = this.update_in(&mut cx, |this, window, cx| { + this.askpass_delegate(format!("git push {}", remote.name), window, cx) + })?; + let guard = this .update(&mut cx, |this, _| this.start_remote_operation()) .ok(); - let push = repo.update(&mut cx, |repo, _cx| { - repo.push(branch.name.clone(), remote.name.clone(), options) + let push = repo.update(&mut cx, |repo, cx| { + repo.push( + branch.name.clone(), + remote.name.clone(), + Some(options), + askpass_delegate, + cx, + ) })?; let remote_output = push.await?; - drop(guard); this.update(&mut cx, |this, cx| match remote_output { @@ -1590,6 +1623,34 @@ index 1234567..abcdef0 100644 .detach_and_log_err(cx); } + fn askpass_delegate( + &self, + operation: impl Into, + window: &mut Window, + cx: &mut Context, + ) -> AskPassDelegate { + let this = cx.weak_entity(); + let operation = operation.into(); + let window = window.window_handle(); + AskPassDelegate::new(&mut cx.to_async(), move |prompt, tx, cx| { + window + .update(cx, |_, window, cx| { + this.update(cx, |this, cx| { + this.workspace.update(cx, |workspace, cx| { + workspace.toggle_modal(window, cx, |window, cx| { + AskPassModal::new(operation.clone(), prompt.into(), tx, window, cx) + }); + }) + }) + }) + .ok(); + }) + } + + fn can_push_and_pull(&self, cx: &App) -> bool { + !self.project.read(cx).is_via_collab() + } + fn get_current_remote( &mut self, window: &mut Window, @@ -1988,14 +2049,14 @@ index 1234567..abcdef0 100644 }; let notif_id = NotificationId::Named("git-operation-error".into()); - let mut message = e.to_string().trim().to_string(); + let message = e.to_string().trim().to_string(); let toast; - if message.matches("Authentication failed").count() >= 1 { - message = format!( - "{}\n\n{}", - message, "Please set your credentials via the CLI" - ); - toast = Toast::new(notif_id, message); + if message + .matches(git::repository::REMOTE_CANCELLED_BY_USER) + .next() + .is_some() + { + return; // Hide the cancelled by user message } else { toast = Toast::new(notif_id, message).on_click("Open Zed Log", |window, cx| { window.dispatch_action(workspace::OpenLog.boxed_clone(), cx); @@ -2108,6 +2169,22 @@ index 1234567..abcdef0 100644 } } + fn expand_commit_editor( + &mut self, + _: &git::ExpandCommitEditor, + window: &mut Window, + cx: &mut Context, + ) { + let workspace = self.workspace.clone(); + window.defer(cx, move |window, cx| { + workspace + .update(cx, |workspace, cx| { + CommitModal::toggle(workspace, window, cx) + }) + .ok(); + }) + } + pub fn render_footer( &self, window: &mut Window, @@ -2222,7 +2299,7 @@ index 1234567..abcdef0 100644 .on_click(cx.listener({ move |_, _, window, cx| { window.dispatch_action( - git::ShowCommitEditor.boxed_clone(), + git::ExpandCommitEditor.boxed_clone(), cx, ) } @@ -2840,6 +2917,7 @@ impl Render for GitPanel { .on_action(cx.listener(Self::unstage_all)) .on_action(cx.listener(Self::restore_tracked_files)) .on_action(cx.listener(Self::clean_all)) + .on_action(cx.listener(Self::expand_commit_editor)) .when(has_write_access && has_co_authors, |git_panel| { git_panel.on_action(cx.listener(Self::toggle_fill_co_authors)) }) @@ -2949,7 +3027,7 @@ impl Panel for GitPanel { } fn icon(&self, _: &Window, cx: &App) -> Option { - Some(ui::IconName::GitBranch).filter(|_| GitPanelSettings::get_global(cx).button) + Some(ui::IconName::GitBranchSmall).filter(|_| GitPanelSettings::get_global(cx).button) } fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> { @@ -3168,14 +3246,8 @@ fn render_git_action_menu(id: impl Into) -> impl IntoElement { .action("Fetch", git::Fetch.boxed_clone()) .action("Pull", git::Pull.boxed_clone()) .separator() - .action("Push", git::Push { options: None }.boxed_clone()) - .action( - "Force Push", - git::Push { - options: Some(PushOptions::Force), - } - .boxed_clone(), - ) + .action("Push", git::Push.boxed_clone()) + .action("Force Push", git::ForcePush.boxed_clone()) })) }) .anchor(Corner::TopRight) @@ -3253,14 +3325,14 @@ impl PanelRepoFooter { move |_, window, cx| { if let Some(panel) = panel.as_ref() { panel.update(cx, |panel, cx| { - panel.push(&git::Push { options: None }, window, cx); + panel.push(false, window, cx); }); } }, move |window, cx| { git_action_tooltip( "Push committed changes to remote", - &git::Push { options: None }, + &git::Push, "git push", panel_focus_handle.clone(), window, @@ -3289,7 +3361,7 @@ impl PanelRepoFooter { move |_, window, cx| { if let Some(panel) = panel.as_ref() { panel.update(cx, |panel, cx| { - panel.pull(&git::Pull, window, cx); + panel.pull(window, cx); }); } }, @@ -3319,7 +3391,7 @@ impl PanelRepoFooter { move |_, window, cx| { if let Some(panel) = panel.as_ref() { panel.update(cx, |panel, cx| { - panel.fetch(&git::Fetch, window, cx); + panel.fetch(window, cx); }); } }, @@ -3349,22 +3421,14 @@ impl PanelRepoFooter { move |_, window, cx| { if let Some(panel) = panel.as_ref() { panel.update(cx, |panel, cx| { - panel.push( - &git::Push { - options: Some(PushOptions::SetUpstream), - }, - window, - cx, - ); + panel.push(false, window, cx); }); } }, move |window, cx| { git_action_tooltip( "Publish branch to remote", - &git::Push { - options: Some(PushOptions::SetUpstream), - }, + &git::Push, "git push --set-upstream", panel_focus_handle.clone(), window, @@ -3387,22 +3451,14 @@ impl PanelRepoFooter { move |_, window, cx| { if let Some(panel) = panel.as_ref() { panel.update(cx, |panel, cx| { - panel.push( - &git::Push { - options: Some(PushOptions::SetUpstream), - }, - window, - cx, - ); + panel.push(false, window, cx); }); } }, move |window, cx| { git_action_tooltip( "Re-publish branch to remote", - &git::Push { - options: Some(PushOptions::SetUpstream), - }, + &git::Push, "git push --set-upstream", panel_focus_handle.clone(), window, @@ -3417,10 +3473,15 @@ impl PanelRepoFooter { id: impl Into, branch: &Branch, cx: &mut App, - ) -> impl IntoElement { + ) -> Option { + if let Some(git_panel) = self.git_panel.as_ref() { + if !git_panel.read(cx).can_push_and_pull(cx) { + return None; + } + } let id = id.into(); let upstream = branch.upstream.as_ref(); - match upstream { + Some(match upstream { Some(Upstream { tracking: UpstreamTracking::Tracked(UpstreamTrackingStatus { ahead, behind }), .. @@ -3434,7 +3495,7 @@ impl PanelRepoFooter { .. }) => self.render_republish_button(id, cx), None => self.render_publish_button(id, cx), - } + }) } } @@ -3550,7 +3611,7 @@ impl RenderOnce for PanelRepoFooter { .child(self.render_overflow_menu(overflow_menu_id)) .when_some(branch, |this, branch| { let button = self.render_relevant_button(self.id.clone(), &branch, cx); - this.child(button) + this.children(button) }), ) } diff --git a/crates/git_ui/src/git_ui.rs b/crates/git_ui/src/git_ui.rs index 6e5e85f73e879b..8414e7737798ff 100644 --- a/crates/git_ui/src/git_ui.rs +++ b/crates/git_ui/src/git_ui.rs @@ -6,6 +6,7 @@ use project_diff::ProjectDiff; use ui::{ActiveTheme, Color, Icon, IconName, IntoElement}; use workspace::Workspace; +mod askpass_modal; pub mod branch_picker; mod commit_modal; pub mod git_panel; @@ -20,30 +21,43 @@ pub fn init(cx: &mut App) { branch_picker::init(cx); cx.observe_new(ProjectDiff::register).detach(); commit_modal::init(cx); + git_panel::init(cx); - cx.observe_new(|workspace: &mut Workspace, _, _| { - workspace.register_action(|workspace, fetch: &git::Fetch, window, cx| { + cx.observe_new(|workspace: &mut Workspace, _, cx| { + let project = workspace.project().read(cx); + if project.is_via_collab() { + return; + } + workspace.register_action(|workspace, _: &git::Fetch, window, cx| { let Some(panel) = workspace.panel::(cx) else { return; }; panel.update(cx, |panel, cx| { - panel.fetch(fetch, window, cx); + panel.fetch(window, cx); }); }); - workspace.register_action(|workspace, push: &git::Push, window, cx| { + workspace.register_action(|workspace, _: &git::Push, window, cx| { let Some(panel) = workspace.panel::(cx) else { return; }; panel.update(cx, |panel, cx| { - panel.push(push, window, cx); + panel.push(false, window, cx); }); }); - workspace.register_action(|workspace, pull: &git::Pull, window, cx| { + workspace.register_action(|workspace, _: &git::ForcePush, window, cx| { let Some(panel) = workspace.panel::(cx) else { return; }; panel.update(cx, |panel, cx| { - panel.pull(pull, window, cx); + panel.push(true, window, cx); + }); + }); + workspace.register_action(|workspace, _: &git::Pull, window, cx| { + let Some(panel) = workspace.panel::(cx) else { + return; + }; + panel.update(cx, |panel, cx| { + panel.pull(window, cx); }); }); }) diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index bb38d9fb8e341d..fa9e1c56322e01 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/crates/git_ui/src/project_diff.rs @@ -10,8 +10,7 @@ use editor::{ use feature_flags::FeatureFlagViewExt; use futures::StreamExt; use git::{ - status::FileStatus, ShowCommitEditor, StageAll, StageAndNext, ToggleStaged, UnstageAll, - UnstageAndNext, + status::FileStatus, Commit, StageAll, StageAndNext, ToggleStaged, UnstageAll, UnstageAndNext, }; use gpui::{ actions, Action, AnyElement, AnyView, App, AppContext as _, AsyncWindowContext, Entity, @@ -923,11 +922,11 @@ impl Render for ProjectDiffToolbar { Button::new("commit", "Commit") .tooltip(Tooltip::for_action_title_in( "Commit", - &ShowCommitEditor, + &Commit, &focus_handle, )) .on_click(cx.listener(|this, _, window, cx| { - this.dispatch_action(&ShowCommitEditor, window, cx); + this.dispatch_action(&Commit, window, cx); })), ), ) diff --git a/crates/git_ui/src/repository_selector.rs b/crates/git_ui/src/repository_selector.rs index fa4d7ad3257fdf..f0755f968e5608 100644 --- a/crates/git_ui/src/repository_selector.rs +++ b/crates/git_ui/src/repository_selector.rs @@ -1,22 +1,14 @@ use gpui::{ - AnyElement, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Subscription, - Task, WeakEntity, + AnyElement, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Task, WeakEntity, }; use itertools::Itertools; use picker::{Picker, PickerDelegate}; -use project::{ - git::{GitStore, Repository}, - Project, -}; +use project::{git::Repository, Project}; use std::sync::Arc; use ui::{prelude::*, ListItem, ListItemSpacing}; pub struct RepositorySelector { picker: Entity>, - /// The task used to update the picker's matches when there is a change to - /// the repository list. - update_matches_task: Option>, - _subscriptions: Vec, } impl RepositorySelector { @@ -51,30 +43,7 @@ impl RepositorySelector { .max_height(Some(rems(20.).into())) }); - let _subscriptions = - vec![cx.subscribe_in(&git_store, window, Self::handle_project_git_event)]; - - RepositorySelector { - picker, - update_matches_task: None, - _subscriptions, - } - } - - fn handle_project_git_event( - &mut self, - git_store: &Entity, - _event: &project::git::GitEvent, - window: &mut Window, - cx: &mut Context, - ) { - // TODO handle events individually - let task = self.picker.update(cx, |this, cx| { - let query = this.query(cx); - this.delegate.repository_entries = git_store.read(cx).all_repositories(); - this.delegate.update_matches(query, window, cx) - }); - self.update_matches_task = Some(task); + RepositorySelector { picker } } } diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index d2d571a81c1f3a..4b66efda12228b 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -27,11 +27,13 @@ test-support = [ [dependencies] aho-corasick.workspace = true anyhow.workspace = true +askpass.workspace = true async-trait.workspace = true +buffer_diff.workspace = true client.workspace = true clock.workspace = true collections.workspace = true -buffer_diff.workspace = true +fancy-regex.workspace = true fs.workspace = true futures.workspace = true fuzzy.workspace = true @@ -39,25 +41,22 @@ git.workspace = true globset.workspace = true gpui.workspace = true http_client.workspace = true +image.workspace = true itertools.workspace = true language.workspace = true log.workspace = true lsp.workspace = true node_runtime.workspace = true -image.workspace = true parking_lot.workspace = true pathdiff.workspace = true paths.workspace = true postage.workspace = true prettier.workspace = true -worktree.workspace = true rand.workspace = true regex.workspace = true remote.workspace = true rpc.workspace = true schemars.workspace = true -task.workspace = true -tempfile.workspace = true serde.workspace = true serde_json.workspace = true settings.workspace = true @@ -67,13 +66,15 @@ shlex.workspace = true smol.workspace = true snippet.workspace = true snippet_provider.workspace = true +task.workspace = true +tempfile.workspace = true terminal.workspace = true text.workspace = true toml.workspace = true -util.workspace = true url.workspace = true +util.workspace = true which.workspace = true -fancy-regex.workspace = true +worktree.workspace = true [dev-dependencies] client = { workspace = true, features = ["test-support"] } diff --git a/crates/project/src/git.rs b/crates/project/src/git.rs index efdf80c5ee686a..62f5e095a610e3 100644 --- a/crates/project/src/git.rs +++ b/crates/project/src/git.rs @@ -4,8 +4,10 @@ use crate::{ Project, ProjectItem, ProjectPath, }; use anyhow::{Context as _, Result}; +use askpass::{AskPassDelegate, AskPassSession}; use buffer_diff::BufferDiffEvent; use client::ProjectId; +use collections::HashMap; use futures::{ channel::{mpsc, oneshot}, StreamExt as _, @@ -22,6 +24,7 @@ use gpui::{ WeakEntity, }; use language::{Buffer, LanguageRegistry}; +use parking_lot::Mutex; use rpc::{ proto::{self, git_reset, ToProto}, AnyProtoClient, TypedEnvelope, @@ -34,13 +37,13 @@ use std::{ sync::Arc, }; use text::BufferId; -use util::{maybe, ResultExt}; +use util::{debug_panic, maybe, ResultExt}; use worktree::{ProjectEntryId, RepositoryEntry, StatusEntry, WorkDirectory}; pub struct GitStore { buffer_store: Entity, pub(super) project_id: Option, - pub(super) client: Option, + pub(super) client: AnyProtoClient, repositories: Vec>, active_index: Option, update_sender: mpsc::UnboundedSender, @@ -55,6 +58,8 @@ pub struct Repository { pub git_repo: GitRepo, pub merge_message: Option, job_sender: mpsc::UnboundedSender, + askpass_delegates: Arc>>, + latest_askpass_id: u64, } #[derive(Clone)] @@ -92,7 +97,7 @@ impl GitStore { pub fn new( worktree_store: &Entity, buffer_store: Entity, - client: Option, + client: AnyProtoClient, project_id: Option, cx: &mut Context<'_, Self>, ) -> Self { @@ -129,6 +134,7 @@ impl GitStore { client.add_entity_request_handler(Self::handle_checkout_files); client.add_entity_request_handler(Self::handle_open_commit_message_buffer); client.add_entity_request_handler(Self::handle_set_index_text); + client.add_entity_request_handler(Self::handle_askpass); client.add_entity_request_handler(Self::handle_check_for_pushed_commits); } @@ -164,7 +170,7 @@ impl GitStore { ) }) .or_else(|| { - let client = client.clone()?; + let client = client.clone(); let project_id = project_id?; Some(( GitRepo::Remote { @@ -216,6 +222,8 @@ impl GitStore { cx.new(|_| Repository { git_store: this.clone(), worktree_id, + askpass_delegates: Default::default(), + latest_askpass_id: 0, repository_entry: repo.clone(), git_repo, job_sender: self.update_sender.clone(), @@ -362,9 +370,21 @@ impl GitStore { let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id); let repository_handle = Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?; + let askpass_id = envelope.payload.askpass_id; + + let askpass = make_remote_delegate( + this, + envelope.payload.project_id, + worktree_id, + work_directory_id, + askpass_id, + &mut cx, + ); let remote_output = repository_handle - .update(&mut cx, |repository_handle, _cx| repository_handle.fetch())? + .update(&mut cx, |repository_handle, cx| { + repository_handle.fetch(askpass, cx) + })? .await??; Ok(proto::RemoteMessageResponse { @@ -383,6 +403,16 @@ impl GitStore { let repository_handle = Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?; + let askpass_id = envelope.payload.askpass_id; + let askpass = make_remote_delegate( + this, + envelope.payload.project_id, + worktree_id, + work_directory_id, + askpass_id, + &mut cx, + ); + let options = envelope .payload .options @@ -396,8 +426,8 @@ impl GitStore { let remote_name = envelope.payload.remote_name.into(); let remote_output = repository_handle - .update(&mut cx, |repository_handle, _cx| { - repository_handle.push(branch_name, remote_name, options) + .update(&mut cx, |repository_handle, cx| { + repository_handle.push(branch_name, remote_name, options, askpass, cx) })? .await??; Ok(proto::RemoteMessageResponse { @@ -415,15 +445,25 @@ impl GitStore { let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id); let repository_handle = Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?; + let askpass_id = envelope.payload.askpass_id; + let askpass = make_remote_delegate( + this, + envelope.payload.project_id, + worktree_id, + work_directory_id, + askpass_id, + &mut cx, + ); let branch_name = envelope.payload.branch_name.into(); let remote_name = envelope.payload.remote_name.into(); let remote_message = repository_handle - .update(&mut cx, |repository_handle, _cx| { - repository_handle.pull(branch_name, remote_name) + .update(&mut cx, |repository_handle, cx| { + repository_handle.pull(branch_name, remote_name, askpass, cx) })? .await??; + Ok(proto::RemoteMessageResponse { stdout: remote_message.stdout, stderr: remote_message.stderr, @@ -719,6 +759,31 @@ impl GitStore { }) } + async fn handle_askpass( + this: Entity, + envelope: TypedEnvelope, + mut cx: AsyncApp, + ) -> Result { + let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); + let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id); + let repository = + Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?; + + let delegates = cx.update(|cx| repository.read(cx).askpass_delegates.clone())?; + let Some(mut askpass) = delegates.lock().remove(&envelope.payload.askpass_id) else { + debug_panic!("no askpass found"); + return Err(anyhow::anyhow!("no askpass found")); + }; + + let response = askpass.ask_password(envelope.payload.prompt).await?; + + delegates + .lock() + .insert(envelope.payload.askpass_id, askpass); + + Ok(proto::AskPassResponse { response }) + } + async fn handle_check_for_pushed_commits( this: Entity, envelope: TypedEnvelope, @@ -765,6 +830,33 @@ impl GitStore { } } +fn make_remote_delegate( + this: Entity, + project_id: u64, + worktree_id: WorktreeId, + work_directory_id: ProjectEntryId, + askpass_id: u64, + cx: &mut AsyncApp, +) -> AskPassDelegate { + AskPassDelegate::new(cx, move |prompt, tx, cx| { + this.update(cx, |this, cx| { + let response = this.client.request(proto::AskPassRequest { + project_id, + worktree_id: worktree_id.to_proto(), + work_directory_id: work_directory_id.to_proto(), + askpass_id, + prompt, + }); + cx.spawn(|_, _| async move { + tx.send(response.await?.response).ok(); + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + }) + .log_err(); + }) +} + impl GitRepo {} impl Repository { @@ -1286,21 +1378,39 @@ impl Repository { }) } - pub fn fetch(&self) -> oneshot::Receiver> { - self.send_job(|git_repo| async move { + pub fn fetch( + &mut self, + askpass: AskPassDelegate, + cx: &App, + ) -> oneshot::Receiver> { + let executor = cx.background_executor().clone(); + let askpass_delegates = self.askpass_delegates.clone(); + let askpass_id = util::post_inc(&mut self.latest_askpass_id); + + self.send_job(move |git_repo| async move { match git_repo { - GitRepo::Local(git_repository) => git_repository.fetch(), + GitRepo::Local(git_repository) => { + let askpass = AskPassSession::new(&executor, askpass).await?; + git_repository.fetch(askpass) + } GitRepo::Remote { project_id, client, worktree_id, work_directory_id, } => { + askpass_delegates.lock().insert(askpass_id, askpass); + let _defer = util::defer(|| { + let askpass_delegate = askpass_delegates.lock().remove(&askpass_id); + debug_assert!(askpass_delegate.is_some()); + }); + let response = client .request(proto::Fetch { project_id: project_id.0, worktree_id: worktree_id.to_proto(), work_directory_id: work_directory_id.to_proto(), + askpass_id, }) .await .context("sending fetch request")?; @@ -1315,25 +1425,40 @@ impl Repository { } pub fn push( - &self, + &mut self, branch: SharedString, remote: SharedString, options: Option, + askpass: AskPassDelegate, + cx: &App, ) -> oneshot::Receiver> { + let executor = cx.background_executor().clone(); + let askpass_delegates = self.askpass_delegates.clone(); + let askpass_id = util::post_inc(&mut self.latest_askpass_id); + self.send_job(move |git_repo| async move { match git_repo { - GitRepo::Local(git_repository) => git_repository.push(&branch, &remote, options), + GitRepo::Local(git_repository) => { + let askpass = AskPassSession::new(&executor, askpass).await?; + git_repository.push(&branch, &remote, options, askpass) + } GitRepo::Remote { project_id, client, worktree_id, work_directory_id, } => { + askpass_delegates.lock().insert(askpass_id, askpass); + let _defer = util::defer(|| { + let askpass_delegate = askpass_delegates.lock().remove(&askpass_id); + debug_assert!(askpass_delegate.is_some()); + }); let response = client .request(proto::Push { project_id: project_id.0, worktree_id: worktree_id.to_proto(), work_directory_id: work_directory_id.to_proto(), + askpass_id, branch_name: branch.to_string(), remote_name: remote.to_string(), options: options.map(|options| match options { @@ -1354,24 +1479,38 @@ impl Repository { } pub fn pull( - &self, + &mut self, branch: SharedString, remote: SharedString, + askpass: AskPassDelegate, + cx: &App, ) -> oneshot::Receiver> { - self.send_job(|git_repo| async move { + let executor = cx.background_executor().clone(); + let askpass_delegates = self.askpass_delegates.clone(); + let askpass_id = util::post_inc(&mut self.latest_askpass_id); + self.send_job(move |git_repo| async move { match git_repo { - GitRepo::Local(git_repository) => git_repository.pull(&branch, &remote), + GitRepo::Local(git_repository) => { + let askpass = AskPassSession::new(&executor, askpass).await?; + git_repository.pull(&branch, &remote, askpass) + } GitRepo::Remote { project_id, client, worktree_id, work_directory_id, } => { + askpass_delegates.lock().insert(askpass_id, askpass); + let _defer = util::defer(|| { + let askpass_delegate = askpass_delegates.lock().remove(&askpass_id); + debug_assert!(askpass_delegate.is_some()); + }); let response = client .request(proto::Pull { project_id: project_id.0, worktree_id: worktree_id.to_proto(), work_directory_id: work_directory_id.to_proto(), + askpass_id, branch_name: branch.to_string(), remote_name: remote.to_string(), }) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index b998d4986cdf80..d3ef60506f3169 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -707,8 +707,15 @@ impl Project { ) }); - let git_store = - cx.new(|cx| GitStore::new(&worktree_store, buffer_store.clone(), None, None, cx)); + let git_store = cx.new(|cx| { + GitStore::new( + &worktree_store, + buffer_store.clone(), + client.clone().into(), + None, + cx, + ) + }); cx.subscribe(&lsp_store, Self::on_lsp_store_event).detach(); @@ -832,7 +839,7 @@ impl Project { GitStore::new( &worktree_store, buffer_store.clone(), - Some(ssh_proto.clone()), + ssh_proto.clone(), Some(ProjectId(SSH_PROJECT_ID)), cx, ) @@ -1040,7 +1047,7 @@ impl Project { GitStore::new( &worktree_store, buffer_store.clone(), - Some(client.clone().into()), + client.clone().into(), Some(ProjectId(remote_id)), cx, ) diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index 6f0d66ff9abb5a..c4ba7a92a0d321 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -333,12 +333,16 @@ message Envelope { ApplyCodeActionKindResponse apply_code_action_kind_response = 310; RemoteMessageResponse remote_message_response = 311; + GitGetBranches git_get_branches = 312; GitCreateBranch git_create_branch = 313; - GitChangeBranch git_change_branch = 314; // current max + GitChangeBranch git_change_branch = 314; CheckForPushedCommits check_for_pushed_commits = 315; - CheckForPushedCommitsResponse check_for_pushed_commits_response = 316; // current max + CheckForPushedCommitsResponse check_for_pushed_commits_response = 316; + + AskPassRequest ask_pass_request = 317; + AskPassResponse ask_pass_response = 318; // current max } reserved 87 to 88; @@ -2818,6 +2822,7 @@ message Push { string remote_name = 4; string branch_name = 5; optional PushOptions options = 6; + uint64 askpass_id = 7; enum PushOptions { SET_UPSTREAM = 0; @@ -2829,6 +2834,7 @@ message Fetch { uint64 project_id = 1; uint64 worktree_id = 2; uint64 work_directory_id = 3; + uint64 askpass_id = 4; } message GetRemotes { @@ -2852,6 +2858,7 @@ message Pull { uint64 work_directory_id = 3; string remote_name = 4; string branch_name = 5; + uint64 askpass_id = 6; } message RemoteMessageResponse { @@ -2859,6 +2866,18 @@ message RemoteMessageResponse { string stderr = 2; } +message AskPassRequest { + uint64 project_id = 1; + uint64 worktree_id = 2; + uint64 work_directory_id = 3; + uint64 askpass_id = 4; + string prompt = 5; +} + +message AskPassResponse { + string response = 1; +} + message GitGetBranches { uint64 project_id = 1; uint64 worktree_id = 2; diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index 9e3b3381148b40..fdcd8093caa746 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -452,6 +452,8 @@ messages!( (GetRemotesResponse, Background), (Pull, Background), (RemoteMessageResponse, Background), + (AskPassRequest, Background), + (AskPassResponse, Background), (GitCreateBranch, Background), (GitChangeBranch, Background), (CheckForPushedCommits, Background), @@ -598,6 +600,7 @@ request_messages!( (Fetch, RemoteMessageResponse), (GetRemotes, GetRemotesResponse), (Pull, RemoteMessageResponse), + (AskPassRequest, AskPassResponse), (GitCreateBranch, Ack), (GitChangeBranch, Ack), (CheckForPushedCommits, CheckForPushedCommitsResponse), @@ -702,6 +705,7 @@ entity_messages!( Fetch, GetRemotes, Pull, + AskPassRequest, GitChangeBranch, GitCreateBranch, CheckForPushedCommits, diff --git a/crates/recent_projects/src/ssh_connections.rs b/crates/recent_projects/src/ssh_connections.rs index f30dc190e1f5d1..cdc2655516c060 100644 --- a/crates/recent_projects/src/ssh_connections.rs +++ b/crates/recent_projects/src/ssh_connections.rs @@ -131,7 +131,7 @@ pub struct SshPrompt { connection_string: SharedString, nickname: Option, status_message: Option, - prompt: Option<(Entity, oneshot::Sender>)>, + prompt: Option<(Entity, oneshot::Sender)>, cancellation: Option>, editor: Entity, } @@ -176,7 +176,7 @@ impl SshPrompt { pub fn set_prompt( &mut self, prompt: String, - tx: oneshot::Sender>, + tx: oneshot::Sender, window: &mut Window, cx: &mut Context, ) { @@ -223,7 +223,7 @@ impl SshPrompt { if let Some((_, tx)) = self.prompt.take() { self.status_message = Some("Connecting".into()); self.editor.update(cx, |editor, cx| { - tx.send(Ok(editor.text(cx))).ok(); + tx.send(editor.text(cx)).ok(); editor.clear(window, cx); }); } @@ -429,11 +429,10 @@ pub struct SshClientDelegate { } impl remote::SshClientDelegate for SshClientDelegate { - fn ask_password(&self, prompt: String, cx: &mut AsyncApp) -> oneshot::Receiver> { - let (tx, rx) = oneshot::channel(); + fn ask_password(&self, prompt: String, tx: oneshot::Sender, cx: &mut AsyncApp) { let mut known_password = self.known_password.clone(); if let Some(password) = known_password.take() { - tx.send(Ok(password)).ok(); + tx.send(password).ok(); } else { self.window .update(cx, |_, window, cx| { @@ -443,7 +442,6 @@ impl remote::SshClientDelegate for SshClientDelegate { }) .ok(); } - rx } fn set_status(&self, status: Option<&str>, cx: &mut AsyncApp) { diff --git a/crates/remote/Cargo.toml b/crates/remote/Cargo.toml index 18b5a98faccaad..4bb33d6db448c6 100644 --- a/crates/remote/Cargo.toml +++ b/crates/remote/Cargo.toml @@ -19,6 +19,7 @@ test-support = ["fs/test-support"] [dependencies] anyhow.workspace = true +askpass.workspace = true async-trait.workspace = true collections.workspace = true fs.workspace = true @@ -26,9 +27,10 @@ futures.workspace = true gpui.workspace = true itertools.workspace = true log.workspace = true -paths.workspace = true parking_lot.workspace = true +paths.workspace = true prost.workspace = true +release_channel.workspace = true rpc = { workspace = true, features = ["gpui"] } schemars.workspace = true serde.workspace = true @@ -38,8 +40,6 @@ smol.workspace = true tempfile.workspace = true thiserror.workspace = true util.workspace = true -release_channel.workspace = true -which.workspace = true [dev-dependencies] gpui = { workspace = true, features = ["test-support"] } diff --git a/crates/remote/src/ssh_session.rs b/crates/remote/src/ssh_session.rs index b36058a62d3011..1eaba738cc7637 100644 --- a/crates/remote/src/ssh_session.rs +++ b/crates/remote/src/ssh_session.rs @@ -316,7 +316,7 @@ impl SshPlatform { } pub trait SshClientDelegate: Send + Sync { - fn ask_password(&self, prompt: String, cx: &mut AsyncApp) -> oneshot::Receiver>; + fn ask_password(&self, prompt: String, tx: oneshot::Sender, cx: &mut AsyncApp); fn get_download_params( &self, platform: SshPlatform, @@ -1454,83 +1454,22 @@ impl SshRemoteConnection { delegate: Arc, cx: &mut AsyncApp, ) -> Result { - use futures::AsyncWriteExt as _; - use futures::{io::BufReader, AsyncBufReadExt as _}; - use smol::net::unix::UnixStream; - use smol::{fs::unix::PermissionsExt as _, net::unix::UnixListener}; - use util::ResultExt as _; + use askpass::AskPassResult; delegate.set_status(Some("Connecting"), cx); let url = connection_options.ssh_url(); + let temp_dir = tempfile::Builder::new() .prefix("zed-ssh-session") .tempdir()?; - - // Create a domain socket listener to handle requests from the askpass program. - let askpass_socket = temp_dir.path().join("askpass.sock"); - let (askpass_opened_tx, askpass_opened_rx) = oneshot::channel::<()>(); - let listener = - UnixListener::bind(&askpass_socket).context("failed to create askpass socket")?; - - let (askpass_kill_master_tx, askpass_kill_master_rx) = oneshot::channel::(); - let mut kill_tx = Some(askpass_kill_master_tx); - - let askpass_task = cx.spawn({ + let askpass_delegate = askpass::AskPassDelegate::new(cx, { let delegate = delegate.clone(); - |mut cx| async move { - let mut askpass_opened_tx = Some(askpass_opened_tx); - - while let Ok((mut stream, _)) = listener.accept().await { - if let Some(askpass_opened_tx) = askpass_opened_tx.take() { - askpass_opened_tx.send(()).ok(); - } - let mut buffer = Vec::new(); - let mut reader = BufReader::new(&mut stream); - if reader.read_until(b'\0', &mut buffer).await.is_err() { - buffer.clear(); - } - let password_prompt = String::from_utf8_lossy(&buffer); - if let Some(password) = delegate - .ask_password(password_prompt.to_string(), &mut cx) - .await - .context("failed to get ssh password") - .and_then(|p| p) - .log_err() - { - stream.write_all(password.as_bytes()).await.log_err(); - } else { - if let Some(kill_tx) = kill_tx.take() { - kill_tx.send(stream).log_err(); - break; - } - } - } - } + move |prompt, tx, cx| delegate.ask_password(prompt, tx, cx) }); - anyhow::ensure!( - which::which("nc").is_ok(), - "Cannot find `nc` command (netcat), which is required to connect over SSH." - ); - - // 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"); - fs::write(&askpass_script_path, askpass_script).await?; - fs::set_permissions(&askpass_script_path, std::fs::Permissions::from_mode(0o755)).await?; + let mut askpass = + askpass::AskPassSession::new(cx.background_executor(), askpass_delegate).await?; // Start the master SSH process, which does not do anything except for establish // the connection and keep it open, allowing other ssh commands to reuse it @@ -1542,7 +1481,7 @@ impl SshRemoteConnection { .stdout(Stdio::piped()) .stderr(Stdio::piped()) .env("SSH_ASKPASS_REQUIRE", "force") - .env("SSH_ASKPASS", &askpass_script_path) + .env("SSH_ASKPASS", &askpass.script_path()) .args(connection_options.additional_args()) .args([ "-N", @@ -1556,35 +1495,25 @@ impl SshRemoteConnection { .arg(&url) .kill_on_drop(true) .spawn()?; - // Wait for this ssh process to close its stdout, indicating that authentication // has completed. let mut stdout = master_process.stdout.take().unwrap(); let mut output = Vec::new(); - let connection_timeout = Duration::from_secs(10); let result = select_biased! { - _ = askpass_opened_rx.fuse() => { - select_biased! { - stream = askpass_kill_master_rx.fuse() => { + result = askpass.run().fuse() => { + match result { + AskPassResult::CancelledByUser => { master_process.kill().ok(); - drop(stream); - Err(anyhow!("SSH connection canceled")) + Err(anyhow!("SSH connection canceled"))? } - // If the askpass script has opened, that means the user is typing - // their password, in which case we don't want to timeout anymore, - // since we know a connection has been established. - result = stdout.read_to_end(&mut output).fuse() => { - result?; - Ok(()) + AskPassResult::Timedout => { + Err(anyhow!("connecting to host timed out"))? } } } _ = stdout.read_to_end(&mut output).fuse() => { - Ok(()) - } - _ = futures::FutureExt::fuse(smol::Timer::after(connection_timeout)) => { - Err(anyhow!("Exceeded {:?} timeout trying to connect to host", connection_timeout)) + anyhow::Ok(()) } }; @@ -1592,8 +1521,6 @@ impl SshRemoteConnection { return Err(e.context("Failed to connect to host")); } - drop(askpass_task); - if master_process.try_status()?.is_some() { output.clear(); let mut stderr = master_process.stderr.take().unwrap(); @@ -1606,6 +1533,8 @@ impl SshRemoteConnection { Err(anyhow!(error_message))?; } + drop(askpass); + let socket = SshSocket { connection_options, socket_path, @@ -2558,7 +2487,7 @@ mod fake { pub(super) struct Delegate; impl SshClientDelegate for Delegate { - fn ask_password(&self, _: String, _: &mut AsyncApp) -> oneshot::Receiver> { + fn ask_password(&self, _: String, _: oneshot::Sender, _: &mut AsyncApp) { unreachable!() } diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index fadd603b502f20..bfd4f52fffce83 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -87,8 +87,15 @@ impl HeadlessProject { buffer_store }); - let git_store = - cx.new(|cx| GitStore::new(&worktree_store, buffer_store.clone(), None, None, cx)); + let git_store = cx.new(|cx| { + GitStore::new( + &worktree_store, + buffer_store.clone(), + session.clone().into(), + None, + cx, + ) + }); let prettier_store = cx.new(|cx| { PrettierStore::new( node_runtime.clone(), diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index d4932d1b7d8e03..32fcd6ea092c9c 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -508,7 +508,6 @@ fn main() { outline::init(cx); project_symbols::init(cx); project_panel::init(cx); - git_ui::git_panel::init(cx); outline_panel::init(cx); component_preview::init(cx); tasks_ui::init(cx);