Git askpass (#25953)
Supersedes #25848 Release Notes: - git: Supporting push/pull/fetch when remote requires auth --------- Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
This commit is contained in:
parent
6fdb666bb7
commit
c34357e2ab
29 changed files with 864 additions and 379 deletions
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
|
@ -295,7 +295,10 @@ jobs:
|
||||||
# Since the Windows runners are stateful, so we need to remove the config file to prevent potential bug.
|
# Since the Windows runners are stateful, so we need to remove the config file to prevent potential bug.
|
||||||
- name: Clean CI config file
|
- name: Clean CI config file
|
||||||
if: always()
|
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.
|
# 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
|
# 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.
|
# Since the Windows runners are stateful, so we need to remove the config file to prevent potential bug.
|
||||||
- name: Clean CI config file
|
- name: Clean CI config file
|
||||||
if: always()
|
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:
|
bundle-mac:
|
||||||
timeout-minutes: 120
|
timeout-minutes: 120
|
||||||
|
|
28
Cargo.lock
generated
28
Cargo.lock
generated
|
@ -257,9 +257,9 @@ checksum = "34cd60c5e3152cef0a592f1b296f1cc93715d89d2551d85315828c3a09575ff4"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anyhow"
|
||||||
version = "1.0.97"
|
version = "1.0.96"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f"
|
checksum = "6b964d184e89d9b6b67dd2715bc8e74cf3107fb2b529990c90cf517326150bf4"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "approx"
|
name = "approx"
|
||||||
|
@ -358,6 +358,19 @@ dependencies = [
|
||||||
"zbus",
|
"zbus",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "askpass"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"futures 0.3.31",
|
||||||
|
"gpui",
|
||||||
|
"smol",
|
||||||
|
"tempfile",
|
||||||
|
"util",
|
||||||
|
"which 6.0.3",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "assets"
|
name = "assets"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
@ -1011,9 +1024,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-trait"
|
name = "async-trait"
|
||||||
version = "0.1.87"
|
version = "0.1.86"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d556ec1359574147ec0c4fc5eb525f3f23263a592b1a9c07e0a75b427de55c97"
|
checksum = "644dd749086bf3771a2fbc5f256fdb982d53f011c7d5d560304eafeecebce79d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
@ -5364,9 +5377,11 @@ name = "git"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"askpass",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"collections",
|
"collections",
|
||||||
"derive_more",
|
"derive_more",
|
||||||
|
"futures 0.3.31",
|
||||||
"git2",
|
"git2",
|
||||||
"gpui",
|
"gpui",
|
||||||
"http_client",
|
"http_client",
|
||||||
|
@ -5380,7 +5395,6 @@ dependencies = [
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"smol",
|
"smol",
|
||||||
"sum_tree",
|
"sum_tree",
|
||||||
"tempfile",
|
|
||||||
"text",
|
"text",
|
||||||
"time",
|
"time",
|
||||||
"unindent",
|
"unindent",
|
||||||
|
@ -5424,6 +5438,7 @@ name = "git_ui"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"askpass",
|
||||||
"buffer_diff",
|
"buffer_diff",
|
||||||
"collections",
|
"collections",
|
||||||
"component",
|
"component",
|
||||||
|
@ -10258,6 +10273,7 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"askpass",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"buffer_diff",
|
"buffer_diff",
|
||||||
"client",
|
"client",
|
||||||
|
@ -11079,6 +11095,7 @@ name = "remote"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"askpass",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"collections",
|
"collections",
|
||||||
"fs",
|
"fs",
|
||||||
|
@ -11099,7 +11116,6 @@ dependencies = [
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
"util",
|
"util",
|
||||||
"which 6.0.3",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
@ -3,6 +3,7 @@ resolver = "2"
|
||||||
members = [
|
members = [
|
||||||
"crates/activity_indicator",
|
"crates/activity_indicator",
|
||||||
"crates/anthropic",
|
"crates/anthropic",
|
||||||
|
"crates/askpass",
|
||||||
"crates/assets",
|
"crates/assets",
|
||||||
"crates/assistant",
|
"crates/assistant",
|
||||||
"crates/assistant2",
|
"crates/assistant2",
|
||||||
|
@ -207,6 +208,7 @@ edition = "2021"
|
||||||
activity_indicator = { path = "crates/activity_indicator" }
|
activity_indicator = { path = "crates/activity_indicator" }
|
||||||
ai = { path = "crates/ai" }
|
ai = { path = "crates/ai" }
|
||||||
anthropic = { path = "crates/anthropic" }
|
anthropic = { path = "crates/anthropic" }
|
||||||
|
askpass = { path = "crates/askpass" }
|
||||||
assets = { path = "crates/assets" }
|
assets = { path = "crates/assets" }
|
||||||
assistant = { path = "crates/assistant" }
|
assistant = { path = "crates/assistant" }
|
||||||
assistant2 = { path = "crates/assistant2" }
|
assistant2 = { path = "crates/assistant2" }
|
||||||
|
|
|
@ -739,7 +739,7 @@
|
||||||
"tab": "git_panel::FocusEditor",
|
"tab": "git_panel::FocusEditor",
|
||||||
"shift-tab": "git_panel::FocusEditor",
|
"shift-tab": "git_panel::FocusEditor",
|
||||||
"escape": "git_panel::ToggleFocus",
|
"escape": "git_panel::ToggleFocus",
|
||||||
"ctrl-enter": "git::ShowCommitEditor",
|
"ctrl-enter": "git::Commit",
|
||||||
"alt-enter": "menu::SecondaryConfirm"
|
"alt-enter": "menu::SecondaryConfirm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -753,7 +753,13 @@
|
||||||
{
|
{
|
||||||
"context": "GitDiff > Editor",
|
"context": "GitDiff > Editor",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"ctrl-enter": "git::ShowCommitEditor"
|
"ctrl-enter": "git::Commit"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"context": "AskPass > Editor",
|
||||||
|
"bindings": {
|
||||||
|
"enter": "menu::Confirm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -760,14 +760,21 @@
|
||||||
"tab": "git_panel::FocusEditor",
|
"tab": "git_panel::FocusEditor",
|
||||||
"shift-tab": "git_panel::FocusEditor",
|
"shift-tab": "git_panel::FocusEditor",
|
||||||
"escape": "git_panel::ToggleFocus",
|
"escape": "git_panel::ToggleFocus",
|
||||||
"cmd-enter": "git::ShowCommitEditor"
|
"cmd-enter": "git::Commit"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "GitDiff > Editor",
|
"context": "GitDiff > Editor",
|
||||||
"use_key_equivalents": true,
|
"use_key_equivalents": true,
|
||||||
"bindings": {
|
"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",
|
"cmd-enter": "git::Commit",
|
||||||
"tab": "git_panel::FocusChanges",
|
"tab": "git_panel::FocusChanges",
|
||||||
"shift-tab": "git_panel::FocusChanges",
|
"shift-tab": "git_panel::FocusChanges",
|
||||||
"alt-up": "git_panel::FocusChanges"
|
"alt-up": "git_panel::FocusChanges",
|
||||||
|
"shift-escape": "git::ExpandCommitEditor"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
21
crates/askpass/Cargo.toml
Normal file
21
crates/askpass/Cargo.toml
Normal file
|
@ -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
|
1
crates/askpass/LICENSE-APACHE
Symbolic link
1
crates/askpass/LICENSE-APACHE
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../../LICENSE-APACHE
|
194
crates/askpass/src/askpass.rs
Normal file
194
crates/askpass/src/askpass.rs
Normal file
|
@ -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<String>)>,
|
||||||
|
_task: Task<()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AskPassDelegate {
|
||||||
|
pub fn new(
|
||||||
|
cx: &mut AsyncApp,
|
||||||
|
password_prompt: impl Fn(String, oneshot::Sender<String>, &mut AsyncApp) + Send + Sync + 'static,
|
||||||
|
) -> Self {
|
||||||
|
let (tx, mut rx) = mpsc::unbounded::<(String, oneshot::Sender<String>)>();
|
||||||
|
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<String> {
|
||||||
|
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<oneshot::Receiver<()>>,
|
||||||
|
askpass_kill_master_rx: Option<oneshot::Receiver<()>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<Self> {
|
||||||
|
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<Self> {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
|
@ -393,9 +393,6 @@ impl Server {
|
||||||
.add_request_handler(forward_mutating_project_request::<proto::OpenContext>)
|
.add_request_handler(forward_mutating_project_request::<proto::OpenContext>)
|
||||||
.add_request_handler(forward_mutating_project_request::<proto::CreateContext>)
|
.add_request_handler(forward_mutating_project_request::<proto::CreateContext>)
|
||||||
.add_request_handler(forward_mutating_project_request::<proto::SynchronizeContexts>)
|
.add_request_handler(forward_mutating_project_request::<proto::SynchronizeContexts>)
|
||||||
.add_request_handler(forward_mutating_project_request::<proto::Push>)
|
|
||||||
.add_request_handler(forward_mutating_project_request::<proto::Pull>)
|
|
||||||
.add_request_handler(forward_mutating_project_request::<proto::Fetch>)
|
|
||||||
.add_request_handler(forward_mutating_project_request::<proto::Stage>)
|
.add_request_handler(forward_mutating_project_request::<proto::Stage>)
|
||||||
.add_request_handler(forward_mutating_project_request::<proto::Unstage>)
|
.add_request_handler(forward_mutating_project_request::<proto::Unstage>)
|
||||||
.add_request_handler(forward_mutating_project_request::<proto::Commit>)
|
.add_request_handler(forward_mutating_project_request::<proto::Commit>)
|
||||||
|
|
|
@ -16,6 +16,7 @@ test-support = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
|
askpass.workspace = true
|
||||||
async-trait.workspace = true
|
async-trait.workspace = true
|
||||||
collections.workspace = true
|
collections.workspace = true
|
||||||
derive_more.workspace = true
|
derive_more.workspace = true
|
||||||
|
@ -34,7 +35,7 @@ text.workspace = true
|
||||||
time.workspace = true
|
time.workspace = true
|
||||||
url.workspace = true
|
url.workspace = true
|
||||||
util.workspace = true
|
util.workspace = true
|
||||||
tempfile.workspace = true
|
futures.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
pretty_assertions.workspace = true
|
pretty_assertions.workspace = true
|
||||||
|
|
|
@ -8,9 +8,6 @@ pub mod status;
|
||||||
use anyhow::{anyhow, Context as _, Result};
|
use anyhow::{anyhow, Context as _, Result};
|
||||||
use gpui::action_with_deprecated_aliases;
|
use gpui::action_with_deprecated_aliases;
|
||||||
use gpui::actions;
|
use gpui::actions;
|
||||||
use gpui::impl_actions;
|
|
||||||
use repository::PushOptions;
|
|
||||||
use schemars::JsonSchema;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
@ -31,13 +28,6 @@ pub static COMMIT_MESSAGE: LazyLock<&'static OsStr> =
|
||||||
LazyLock::new(|| OsStr::new("COMMIT_EDITMSG"));
|
LazyLock::new(|| OsStr::new("COMMIT_EDITMSG"));
|
||||||
pub static INDEX_LOCK: LazyLock<&'static OsStr> = LazyLock::new(|| OsStr::new("index.lock"));
|
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<PushOptions>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl_actions!(git, [Push]);
|
|
||||||
|
|
||||||
actions!(
|
actions!(
|
||||||
git,
|
git,
|
||||||
[
|
[
|
||||||
|
@ -54,10 +44,12 @@ actions!(
|
||||||
RestoreTrackedFiles,
|
RestoreTrackedFiles,
|
||||||
TrashUntrackedFiles,
|
TrashUntrackedFiles,
|
||||||
Uncommit,
|
Uncommit,
|
||||||
|
Push,
|
||||||
|
ForcePush,
|
||||||
Pull,
|
Pull,
|
||||||
Fetch,
|
Fetch,
|
||||||
Commit,
|
Commit,
|
||||||
ShowCommitEditor,
|
ExpandCommitEditor
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
action_with_deprecated_aliases!(git, RestoreFile, ["editor::RevertFile"]);
|
action_with_deprecated_aliases!(git, RestoreFile, ["editor::RevertFile"]);
|
||||||
|
|
|
@ -2,7 +2,9 @@ use crate::status::FileStatus;
|
||||||
use crate::GitHostingProviderRegistry;
|
use crate::GitHostingProviderRegistry;
|
||||||
use crate::{blame::Blame, status::GitStatus};
|
use crate::{blame::Blame, status::GitStatus};
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
|
use askpass::{AskPassResult, AskPassSession};
|
||||||
use collections::{HashMap, HashSet};
|
use collections::{HashMap, HashSet};
|
||||||
|
use futures::{select_biased, FutureExt as _};
|
||||||
use git2::BranchType;
|
use git2::BranchType;
|
||||||
use gpui::SharedString;
|
use gpui::SharedString;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
|
@ -11,8 +13,6 @@ use schemars::JsonSchema;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::borrow::Borrow;
|
use std::borrow::Borrow;
|
||||||
use std::io::Write as _;
|
use std::io::Write as _;
|
||||||
#[cfg(not(windows))]
|
|
||||||
use std::os::unix::fs::PermissionsExt;
|
|
||||||
use std::process::Stdio;
|
use std::process::Stdio;
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
use std::{
|
use std::{
|
||||||
|
@ -21,9 +21,11 @@ use std::{
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
use sum_tree::MapSeekTarget;
|
use sum_tree::MapSeekTarget;
|
||||||
use util::command::new_std_command;
|
use util::command::{new_smol_command, new_std_command};
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
|
|
||||||
|
pub const REMOTE_CANCELLED_BY_USER: &str = "Operation cancelled by user";
|
||||||
|
|
||||||
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
|
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
|
||||||
pub struct Branch {
|
pub struct Branch {
|
||||||
pub is_head: bool,
|
pub is_head: bool,
|
||||||
|
@ -200,9 +202,16 @@ pub trait GitRepository: Send + Sync {
|
||||||
branch_name: &str,
|
branch_name: &str,
|
||||||
upstream_name: &str,
|
upstream_name: &str,
|
||||||
options: Option<PushOptions>,
|
options: Option<PushOptions>,
|
||||||
|
askpass: AskPassSession,
|
||||||
) -> Result<RemoteCommandOutput>;
|
) -> Result<RemoteCommandOutput>;
|
||||||
fn pull(&self, branch_name: &str, upstream_name: &str) -> Result<RemoteCommandOutput>;
|
|
||||||
fn fetch(&self) -> Result<RemoteCommandOutput>;
|
fn pull(
|
||||||
|
&self,
|
||||||
|
branch_name: &str,
|
||||||
|
upstream_name: &str,
|
||||||
|
askpass: AskPassSession,
|
||||||
|
) -> Result<RemoteCommandOutput>;
|
||||||
|
fn fetch(&self, askpass: AskPassSession) -> Result<RemoteCommandOutput>;
|
||||||
|
|
||||||
fn get_remotes(&self, branch_name: Option<&str>) -> Result<Vec<Remote>>;
|
fn get_remotes(&self, branch_name: Option<&str>) -> Result<Vec<Remote>>;
|
||||||
|
|
||||||
|
@ -578,7 +587,6 @@ impl GitRepository for RealGitRepository {
|
||||||
.args(paths.iter().map(|p| p.as_ref()))
|
.args(paths.iter().map(|p| p.as_ref()))
|
||||||
.output()?;
|
.output()?;
|
||||||
|
|
||||||
// TODO: Get remote response out of this and show it to the user
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
return Err(anyhow!(
|
return Err(anyhow!(
|
||||||
"Failed to stage paths:\n{}",
|
"Failed to stage paths:\n{}",
|
||||||
|
@ -599,7 +607,6 @@ impl GitRepository for RealGitRepository {
|
||||||
.args(paths.iter().map(|p| p.as_ref()))
|
.args(paths.iter().map(|p| p.as_ref()))
|
||||||
.output()?;
|
.output()?;
|
||||||
|
|
||||||
// TODO: Get remote response out of this and show it to the user
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
return Err(anyhow!(
|
return Err(anyhow!(
|
||||||
"Failed to unstage:\n{}",
|
"Failed to unstage:\n{}",
|
||||||
|
@ -625,7 +632,6 @@ impl GitRepository for RealGitRepository {
|
||||||
|
|
||||||
let output = cmd.output()?;
|
let output = cmd.output()?;
|
||||||
|
|
||||||
// TODO: Get remote response out of this and show it to the user
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
return Err(anyhow!(
|
return Err(anyhow!(
|
||||||
"Failed to commit:\n{}",
|
"Failed to commit:\n{}",
|
||||||
|
@ -640,15 +646,15 @@ impl GitRepository for RealGitRepository {
|
||||||
branch_name: &str,
|
branch_name: &str,
|
||||||
remote_name: &str,
|
remote_name: &str,
|
||||||
options: Option<PushOptions>,
|
options: Option<PushOptions>,
|
||||||
|
ask_pass: AskPassSession,
|
||||||
) -> Result<RemoteCommandOutput> {
|
) -> Result<RemoteCommandOutput> {
|
||||||
let working_directory = self.working_directory()?;
|
let working_directory = self.working_directory()?;
|
||||||
|
|
||||||
// We do this on every operation to ensure that the askpass script exists and is executable.
|
let mut command = new_smol_command("git");
|
||||||
#[cfg(not(windows))]
|
|
||||||
let (askpass_script_path, _temp_dir) = setup_askpass()?;
|
|
||||||
|
|
||||||
let mut command = new_std_command("git");
|
|
||||||
command
|
command
|
||||||
|
.env("GIT_ASKPASS", ask_pass.script_path())
|
||||||
|
.env("SSH_ASKPASS", ask_pass.script_path())
|
||||||
|
.env("SSH_ASKPASS_REQUIRE", "force")
|
||||||
.current_dir(&working_directory)
|
.current_dir(&working_directory)
|
||||||
.args(["push"])
|
.args(["push"])
|
||||||
.args(options.map(|option| match option {
|
.args(options.map(|option| match option {
|
||||||
|
@ -657,91 +663,46 @@ impl GitRepository for RealGitRepository {
|
||||||
}))
|
}))
|
||||||
.arg(remote_name)
|
.arg(remote_name)
|
||||||
.arg(format!("{}:{}", branch_name, branch_name));
|
.arg(format!("{}:{}", branch_name, branch_name));
|
||||||
|
let git_process = command.spawn()?;
|
||||||
|
|
||||||
#[cfg(not(windows))]
|
run_remote_command(ask_pass, git_process)
|
||||||
{
|
|
||||||
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(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pull(&self, branch_name: &str, remote_name: &str) -> Result<RemoteCommandOutput> {
|
fn pull(
|
||||||
|
&self,
|
||||||
|
branch_name: &str,
|
||||||
|
remote_name: &str,
|
||||||
|
ask_pass: AskPassSession,
|
||||||
|
) -> Result<RemoteCommandOutput> {
|
||||||
let working_directory = self.working_directory()?;
|
let working_directory = self.working_directory()?;
|
||||||
|
|
||||||
// We do this on every operation to ensure that the askpass script exists and is executable.
|
let mut command = new_smol_command("git");
|
||||||
#[cfg(not(windows))]
|
|
||||||
let (askpass_script_path, _temp_dir) = setup_askpass()?;
|
|
||||||
|
|
||||||
let mut command = new_std_command("git");
|
|
||||||
command
|
command
|
||||||
|
.env("GIT_ASKPASS", ask_pass.script_path())
|
||||||
|
.env("SSH_ASKPASS", ask_pass.script_path())
|
||||||
|
.env("SSH_ASKPASS_REQUIRE", "force")
|
||||||
.current_dir(&working_directory)
|
.current_dir(&working_directory)
|
||||||
.args(["pull"])
|
.args(["pull"])
|
||||||
.arg(remote_name)
|
.arg(remote_name)
|
||||||
.arg(branch_name);
|
.arg(branch_name);
|
||||||
|
let git_process = command.spawn()?;
|
||||||
|
|
||||||
#[cfg(not(windows))]
|
run_remote_command(ask_pass, git_process)
|
||||||
{
|
|
||||||
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(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fetch(&self) -> Result<RemoteCommandOutput> {
|
fn fetch(&self, ask_pass: AskPassSession) -> Result<RemoteCommandOutput> {
|
||||||
let working_directory = self.working_directory()?;
|
let working_directory = self.working_directory()?;
|
||||||
|
|
||||||
// We do this on every operation to ensure that the askpass script exists and is executable.
|
let mut command = new_smol_command("git");
|
||||||
#[cfg(not(windows))]
|
|
||||||
let (askpass_script_path, _temp_dir) = setup_askpass()?;
|
|
||||||
|
|
||||||
let mut command = new_std_command("git");
|
|
||||||
command
|
command
|
||||||
|
.env("GIT_ASKPASS", ask_pass.script_path())
|
||||||
|
.env("SSH_ASKPASS", ask_pass.script_path())
|
||||||
|
.env("SSH_ASKPASS_REQUIRE", "force")
|
||||||
.current_dir(&working_directory)
|
.current_dir(&working_directory)
|
||||||
.args(["fetch", "--all"]);
|
.args(["fetch", "--all"]);
|
||||||
|
let git_process = command.spawn()?;
|
||||||
|
|
||||||
#[cfg(not(windows))]
|
run_remote_command(ask_pass, git_process)
|
||||||
{
|
|
||||||
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(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_remotes(&self, branch_name: Option<&str>) -> Result<Vec<Remote>> {
|
fn get_remotes(&self, branch_name: Option<&str>) -> Result<Vec<Remote>> {
|
||||||
|
@ -835,16 +796,38 @@ impl GitRepository for RealGitRepository {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(windows))]
|
fn run_remote_command(
|
||||||
fn setup_askpass() -> Result<(PathBuf, tempfile::TempDir), anyhow::Error> {
|
mut ask_pass: AskPassSession,
|
||||||
let temp_dir = tempfile::Builder::new()
|
git_process: smol::process::Child,
|
||||||
.prefix("zed-git-askpass")
|
) -> std::result::Result<RemoteCommandOutput, anyhow::Error> {
|
||||||
.tempdir()?;
|
smol::block_on(async {
|
||||||
let askpass_script = "#!/bin/sh\necho ''";
|
select_biased! {
|
||||||
let askpass_script_path = temp_dir.path().join("git-askpass.sh");
|
result = ask_pass.run().fuse() => {
|
||||||
std::fs::write(&askpass_script_path, askpass_script)?;
|
match result {
|
||||||
std::fs::set_permissions(&askpass_script_path, std::fs::Permissions::from_mode(0o755))?;
|
AskPassResult::CancelledByUser => {
|
||||||
Ok((askpass_script_path, temp_dir))
|
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)]
|
#[derive(Debug, Clone)]
|
||||||
|
@ -1040,15 +1023,21 @@ impl GitRepository for FakeGitRepository {
|
||||||
_branch: &str,
|
_branch: &str,
|
||||||
_remote: &str,
|
_remote: &str,
|
||||||
_options: Option<PushOptions>,
|
_options: Option<PushOptions>,
|
||||||
|
_ask_pass: AskPassSession,
|
||||||
) -> Result<RemoteCommandOutput> {
|
) -> Result<RemoteCommandOutput> {
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pull(&self, _branch: &str, _remote: &str) -> Result<RemoteCommandOutput> {
|
fn pull(
|
||||||
|
&self,
|
||||||
|
_branch: &str,
|
||||||
|
_remote: &str,
|
||||||
|
_ask_pass: AskPassSession,
|
||||||
|
) -> Result<RemoteCommandOutput> {
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fetch(&self) -> Result<RemoteCommandOutput> {
|
fn fetch(&self, _ask_pass: AskPassSession) -> Result<RemoteCommandOutput> {
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@ test-support = ["multi_buffer/test-support"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
|
askpass.workspace= true
|
||||||
buffer_diff.workspace = true
|
buffer_diff.workspace = true
|
||||||
collections.workspace = true
|
collections.workspace = true
|
||||||
component.workspace = true
|
component.workspace = true
|
||||||
|
|
101
crates/git_ui/src/askpass_modal.rs
Normal file
101
crates/git_ui/src/askpass_modal.rs
Normal file
|
@ -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<Editor>,
|
||||||
|
tx: Option<oneshot::Sender<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventEmitter<DismissEvent> 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<String>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> 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<Self>) {
|
||||||
|
cx.emit(DismissEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
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<Self>) -> 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()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
use crate::branch_picker::{self, BranchList};
|
use crate::branch_picker::{self, BranchList};
|
||||||
use crate::git_panel::{commit_message_editor, GitPanel};
|
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 panel::{panel_button, panel_editor_style, panel_filled_button};
|
||||||
use ui::{prelude::*, KeybindingHint, PopoverMenu, Tooltip};
|
use ui::{prelude::*, KeybindingHint, PopoverMenu, Tooltip};
|
||||||
|
|
||||||
|
@ -109,32 +109,36 @@ struct RestoreDock {
|
||||||
|
|
||||||
impl CommitModal {
|
impl CommitModal {
|
||||||
pub fn register(workspace: &mut Workspace, _: &mut Window, _cx: &mut Context<Workspace>) {
|
pub fn register(workspace: &mut Workspace, _: &mut Window, _cx: &mut Context<Workspace>) {
|
||||||
workspace.register_action(|workspace, _: &ShowCommitEditor, window, cx| {
|
workspace.register_action(|workspace, _: &Commit, window, cx| {
|
||||||
let Some(git_panel) = workspace.panel::<GitPanel>(cx) else {
|
CommitModal::toggle(workspace, window, cx);
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
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::<GitPanel>(window, cx);
|
|
||||||
workspace.toggle_modal(window, cx, move |window, cx| {
|
|
||||||
CommitModal::new(git_panel, restore_dock_position, window, cx)
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context<'_, Workspace>) {
|
||||||
|
let Some(git_panel) = workspace.panel::<GitPanel>(cx) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
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::<GitPanel>(window, cx);
|
||||||
|
workspace.toggle_modal(window, cx, move |window, cx| {
|
||||||
|
CommitModal::new(git_panel, restore_dock_position, window, cx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn new(
|
fn new(
|
||||||
git_panel: Entity<GitPanel>,
|
git_panel: Entity<GitPanel>,
|
||||||
restore_dock: RestoreDock,
|
restore_dock: RestoreDock,
|
||||||
|
|
|
@ -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::git_panel_settings::StatusStyle;
|
||||||
use crate::remote_output_toast::{RemoteAction, RemoteOutputToast};
|
use crate::remote_output_toast::{RemoteAction, RemoteOutputToast};
|
||||||
use crate::{
|
use crate::{
|
||||||
git_panel_settings::GitPanelSettings, git_status_icon, repository_selector::RepositorySelector,
|
git_panel_settings::GitPanelSettings, git_status_icon, repository_selector::RepositorySelector,
|
||||||
};
|
};
|
||||||
use crate::{picker_prompt, project_diff, ProjectDiff};
|
use crate::{picker_prompt, project_diff, ProjectDiff};
|
||||||
|
use anyhow::Result;
|
||||||
|
use askpass::AskPassDelegate;
|
||||||
use db::kvp::KEY_VALUE_STORE;
|
use db::kvp::KEY_VALUE_STORE;
|
||||||
use editor::commit_tooltip::CommitTooltip;
|
use editor::commit_tooltip::CommitTooltip;
|
||||||
|
|
||||||
|
@ -101,7 +105,7 @@ const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);
|
||||||
|
|
||||||
pub fn init(cx: &mut App) {
|
pub fn init(cx: &mut App) {
|
||||||
cx.observe_new(
|
cx.observe_new(
|
||||||
|workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
|
|workspace: &mut Workspace, _window, _: &mut Context<Workspace>| {
|
||||||
workspace.register_action(|workspace, _: &ToggleFocus, window, cx| {
|
workspace.register_action(|workspace, _: &ToggleFocus, window, cx| {
|
||||||
workspace.toggle_panel_focus::<GitPanel>(window, cx);
|
workspace.toggle_panel_focus::<GitPanel>(window, cx);
|
||||||
});
|
});
|
||||||
|
@ -1465,13 +1469,19 @@ index 1234567..abcdef0 100644
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn fetch(&mut self, _: &git::Fetch, _window: &mut Window, cx: &mut Context<Self>) {
|
pub(crate) fn fetch(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if !self.can_push_and_pull(cx) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let Some(repo) = self.active_repository.clone() else {
|
let Some(repo) = self.active_repository.clone() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let guard = self.start_remote_operation();
|
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 {
|
cx.spawn(|this, mut cx| async move {
|
||||||
|
let fetch = repo.update(&mut cx, |repo, cx| repo.fetch(askpass, cx))?;
|
||||||
|
|
||||||
let remote_message = fetch.await?;
|
let remote_message = fetch.await?;
|
||||||
drop(guard);
|
drop(guard);
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| {
|
||||||
|
@ -1492,7 +1502,10 @@ index 1234567..abcdef0 100644
|
||||||
.detach_and_log_err(cx);
|
.detach_and_log_err(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn pull(&mut self, _: &git::Pull, window: &mut Window, cx: &mut Context<Self>) {
|
pub(crate) fn pull(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if !self.can_push_and_pull(cx) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let Some(repo) = self.active_repository.clone() else {
|
let Some(repo) = self.active_repository.clone() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
@ -1501,7 +1514,7 @@ index 1234567..abcdef0 100644
|
||||||
};
|
};
|
||||||
let branch = branch.clone();
|
let branch = branch.clone();
|
||||||
let remote = self.get_current_remote(window, cx);
|
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 {
|
let remote = match remote.await {
|
||||||
Ok(Some(remote)) => remote,
|
Ok(Some(remote)) => remote,
|
||||||
Ok(None) => {
|
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
|
let guard = this
|
||||||
.update(&mut cx, |this, _| this.start_remote_operation())
|
.update(&mut cx, |this, _| this.start_remote_operation())
|
||||||
.ok();
|
.ok();
|
||||||
|
|
||||||
let pull = repo.update(&mut cx, |repo, _cx| {
|
let pull = repo.update(&mut cx, |repo, cx| {
|
||||||
repo.pull(branch.name.clone(), remote.name.clone())
|
repo.pull(branch.name.clone(), remote.name.clone(), askpass, cx)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let remote_message = pull.await?;
|
let remote_message = pull.await?;
|
||||||
|
@ -1539,7 +1556,10 @@ index 1234567..abcdef0 100644
|
||||||
.detach_and_log_err(cx);
|
.detach_and_log_err(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn push(&mut self, action: &git::Push, window: &mut Window, cx: &mut Context<Self>) {
|
pub(crate) fn push(&mut self, force_push: bool, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if !self.can_push_and_pull(cx) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let Some(repo) = self.active_repository.clone() else {
|
let Some(repo) = self.active_repository.clone() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
@ -1547,10 +1567,14 @@ index 1234567..abcdef0 100644
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let branch = branch.clone();
|
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);
|
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 {
|
let remote = match remote.await {
|
||||||
Ok(Some(remote)) => remote,
|
Ok(Some(remote)) => remote,
|
||||||
Ok(None) => {
|
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
|
let guard = this
|
||||||
.update(&mut cx, |this, _| this.start_remote_operation())
|
.update(&mut cx, |this, _| this.start_remote_operation())
|
||||||
.ok();
|
.ok();
|
||||||
|
|
||||||
let push = repo.update(&mut cx, |repo, _cx| {
|
let push = repo.update(&mut cx, |repo, cx| {
|
||||||
repo.push(branch.name.clone(), remote.name.clone(), options)
|
repo.push(
|
||||||
|
branch.name.clone(),
|
||||||
|
remote.name.clone(),
|
||||||
|
Some(options),
|
||||||
|
askpass_delegate,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let remote_output = push.await?;
|
let remote_output = push.await?;
|
||||||
|
|
||||||
drop(guard);
|
drop(guard);
|
||||||
|
|
||||||
this.update(&mut cx, |this, cx| match remote_output {
|
this.update(&mut cx, |this, cx| match remote_output {
|
||||||
|
@ -1590,6 +1623,34 @@ index 1234567..abcdef0 100644
|
||||||
.detach_and_log_err(cx);
|
.detach_and_log_err(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn askpass_delegate(
|
||||||
|
&self,
|
||||||
|
operation: impl Into<SharedString>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> 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(
|
fn get_current_remote(
|
||||||
&mut self,
|
&mut self,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
|
@ -1988,14 +2049,14 @@ index 1234567..abcdef0 100644
|
||||||
};
|
};
|
||||||
let notif_id = NotificationId::Named("git-operation-error".into());
|
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;
|
let toast;
|
||||||
if message.matches("Authentication failed").count() >= 1 {
|
if message
|
||||||
message = format!(
|
.matches(git::repository::REMOTE_CANCELLED_BY_USER)
|
||||||
"{}\n\n{}",
|
.next()
|
||||||
message, "Please set your credentials via the CLI"
|
.is_some()
|
||||||
);
|
{
|
||||||
toast = Toast::new(notif_id, message);
|
return; // Hide the cancelled by user message
|
||||||
} else {
|
} else {
|
||||||
toast = Toast::new(notif_id, message).on_click("Open Zed Log", |window, cx| {
|
toast = Toast::new(notif_id, message).on_click("Open Zed Log", |window, cx| {
|
||||||
window.dispatch_action(workspace::OpenLog.boxed_clone(), 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<Self>,
|
||||||
|
) {
|
||||||
|
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(
|
pub fn render_footer(
|
||||||
&self,
|
&self,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
|
@ -2222,7 +2299,7 @@ index 1234567..abcdef0 100644
|
||||||
.on_click(cx.listener({
|
.on_click(cx.listener({
|
||||||
move |_, _, window, cx| {
|
move |_, _, window, cx| {
|
||||||
window.dispatch_action(
|
window.dispatch_action(
|
||||||
git::ShowCommitEditor.boxed_clone(),
|
git::ExpandCommitEditor.boxed_clone(),
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -2840,6 +2917,7 @@ impl Render for GitPanel {
|
||||||
.on_action(cx.listener(Self::unstage_all))
|
.on_action(cx.listener(Self::unstage_all))
|
||||||
.on_action(cx.listener(Self::restore_tracked_files))
|
.on_action(cx.listener(Self::restore_tracked_files))
|
||||||
.on_action(cx.listener(Self::clean_all))
|
.on_action(cx.listener(Self::clean_all))
|
||||||
|
.on_action(cx.listener(Self::expand_commit_editor))
|
||||||
.when(has_write_access && has_co_authors, |git_panel| {
|
.when(has_write_access && has_co_authors, |git_panel| {
|
||||||
git_panel.on_action(cx.listener(Self::toggle_fill_co_authors))
|
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<ui::IconName> {
|
fn icon(&self, _: &Window, cx: &App) -> Option<ui::IconName> {
|
||||||
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> {
|
fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
|
||||||
|
@ -3168,14 +3246,8 @@ fn render_git_action_menu(id: impl Into<ElementId>) -> impl IntoElement {
|
||||||
.action("Fetch", git::Fetch.boxed_clone())
|
.action("Fetch", git::Fetch.boxed_clone())
|
||||||
.action("Pull", git::Pull.boxed_clone())
|
.action("Pull", git::Pull.boxed_clone())
|
||||||
.separator()
|
.separator()
|
||||||
.action("Push", git::Push { options: None }.boxed_clone())
|
.action("Push", git::Push.boxed_clone())
|
||||||
.action(
|
.action("Force Push", git::ForcePush.boxed_clone())
|
||||||
"Force Push",
|
|
||||||
git::Push {
|
|
||||||
options: Some(PushOptions::Force),
|
|
||||||
}
|
|
||||||
.boxed_clone(),
|
|
||||||
)
|
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
.anchor(Corner::TopRight)
|
.anchor(Corner::TopRight)
|
||||||
|
@ -3253,14 +3325,14 @@ impl PanelRepoFooter {
|
||||||
move |_, window, cx| {
|
move |_, window, cx| {
|
||||||
if let Some(panel) = panel.as_ref() {
|
if let Some(panel) = panel.as_ref() {
|
||||||
panel.update(cx, |panel, cx| {
|
panel.update(cx, |panel, cx| {
|
||||||
panel.push(&git::Push { options: None }, window, cx);
|
panel.push(false, window, cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
move |window, cx| {
|
move |window, cx| {
|
||||||
git_action_tooltip(
|
git_action_tooltip(
|
||||||
"Push committed changes to remote",
|
"Push committed changes to remote",
|
||||||
&git::Push { options: None },
|
&git::Push,
|
||||||
"git push",
|
"git push",
|
||||||
panel_focus_handle.clone(),
|
panel_focus_handle.clone(),
|
||||||
window,
|
window,
|
||||||
|
@ -3289,7 +3361,7 @@ impl PanelRepoFooter {
|
||||||
move |_, window, cx| {
|
move |_, window, cx| {
|
||||||
if let Some(panel) = panel.as_ref() {
|
if let Some(panel) = panel.as_ref() {
|
||||||
panel.update(cx, |panel, cx| {
|
panel.update(cx, |panel, cx| {
|
||||||
panel.pull(&git::Pull, window, cx);
|
panel.pull(window, cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -3319,7 +3391,7 @@ impl PanelRepoFooter {
|
||||||
move |_, window, cx| {
|
move |_, window, cx| {
|
||||||
if let Some(panel) = panel.as_ref() {
|
if let Some(panel) = panel.as_ref() {
|
||||||
panel.update(cx, |panel, cx| {
|
panel.update(cx, |panel, cx| {
|
||||||
panel.fetch(&git::Fetch, window, cx);
|
panel.fetch(window, cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -3349,22 +3421,14 @@ impl PanelRepoFooter {
|
||||||
move |_, window, cx| {
|
move |_, window, cx| {
|
||||||
if let Some(panel) = panel.as_ref() {
|
if let Some(panel) = panel.as_ref() {
|
||||||
panel.update(cx, |panel, cx| {
|
panel.update(cx, |panel, cx| {
|
||||||
panel.push(
|
panel.push(false, window, cx);
|
||||||
&git::Push {
|
|
||||||
options: Some(PushOptions::SetUpstream),
|
|
||||||
},
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
move |window, cx| {
|
move |window, cx| {
|
||||||
git_action_tooltip(
|
git_action_tooltip(
|
||||||
"Publish branch to remote",
|
"Publish branch to remote",
|
||||||
&git::Push {
|
&git::Push,
|
||||||
options: Some(PushOptions::SetUpstream),
|
|
||||||
},
|
|
||||||
"git push --set-upstream",
|
"git push --set-upstream",
|
||||||
panel_focus_handle.clone(),
|
panel_focus_handle.clone(),
|
||||||
window,
|
window,
|
||||||
|
@ -3387,22 +3451,14 @@ impl PanelRepoFooter {
|
||||||
move |_, window, cx| {
|
move |_, window, cx| {
|
||||||
if let Some(panel) = panel.as_ref() {
|
if let Some(panel) = panel.as_ref() {
|
||||||
panel.update(cx, |panel, cx| {
|
panel.update(cx, |panel, cx| {
|
||||||
panel.push(
|
panel.push(false, window, cx);
|
||||||
&git::Push {
|
|
||||||
options: Some(PushOptions::SetUpstream),
|
|
||||||
},
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
move |window, cx| {
|
move |window, cx| {
|
||||||
git_action_tooltip(
|
git_action_tooltip(
|
||||||
"Re-publish branch to remote",
|
"Re-publish branch to remote",
|
||||||
&git::Push {
|
&git::Push,
|
||||||
options: Some(PushOptions::SetUpstream),
|
|
||||||
},
|
|
||||||
"git push --set-upstream",
|
"git push --set-upstream",
|
||||||
panel_focus_handle.clone(),
|
panel_focus_handle.clone(),
|
||||||
window,
|
window,
|
||||||
|
@ -3417,10 +3473,15 @@ impl PanelRepoFooter {
|
||||||
id: impl Into<SharedString>,
|
id: impl Into<SharedString>,
|
||||||
branch: &Branch,
|
branch: &Branch,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> impl IntoElement {
|
) -> Option<impl IntoElement> {
|
||||||
|
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 id = id.into();
|
||||||
let upstream = branch.upstream.as_ref();
|
let upstream = branch.upstream.as_ref();
|
||||||
match upstream {
|
Some(match upstream {
|
||||||
Some(Upstream {
|
Some(Upstream {
|
||||||
tracking: UpstreamTracking::Tracked(UpstreamTrackingStatus { ahead, behind }),
|
tracking: UpstreamTracking::Tracked(UpstreamTrackingStatus { ahead, behind }),
|
||||||
..
|
..
|
||||||
|
@ -3434,7 +3495,7 @@ impl PanelRepoFooter {
|
||||||
..
|
..
|
||||||
}) => self.render_republish_button(id, cx),
|
}) => self.render_republish_button(id, cx),
|
||||||
None => self.render_publish_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))
|
.child(self.render_overflow_menu(overflow_menu_id))
|
||||||
.when_some(branch, |this, branch| {
|
.when_some(branch, |this, branch| {
|
||||||
let button = self.render_relevant_button(self.id.clone(), &branch, cx);
|
let button = self.render_relevant_button(self.id.clone(), &branch, cx);
|
||||||
this.child(button)
|
this.children(button)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ use project_diff::ProjectDiff;
|
||||||
use ui::{ActiveTheme, Color, Icon, IconName, IntoElement};
|
use ui::{ActiveTheme, Color, Icon, IconName, IntoElement};
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
|
||||||
|
mod askpass_modal;
|
||||||
pub mod branch_picker;
|
pub mod branch_picker;
|
||||||
mod commit_modal;
|
mod commit_modal;
|
||||||
pub mod git_panel;
|
pub mod git_panel;
|
||||||
|
@ -20,30 +21,43 @@ pub fn init(cx: &mut App) {
|
||||||
branch_picker::init(cx);
|
branch_picker::init(cx);
|
||||||
cx.observe_new(ProjectDiff::register).detach();
|
cx.observe_new(ProjectDiff::register).detach();
|
||||||
commit_modal::init(cx);
|
commit_modal::init(cx);
|
||||||
|
git_panel::init(cx);
|
||||||
|
|
||||||
cx.observe_new(|workspace: &mut Workspace, _, _| {
|
cx.observe_new(|workspace: &mut Workspace, _, cx| {
|
||||||
workspace.register_action(|workspace, fetch: &git::Fetch, window, 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::<git_panel::GitPanel>(cx) else {
|
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
panel.update(cx, |panel, cx| {
|
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::<git_panel::GitPanel>(cx) else {
|
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
panel.update(cx, |panel, cx| {
|
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::<git_panel::GitPanel>(cx) else {
|
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
panel.update(cx, |panel, cx| {
|
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::<git_panel::GitPanel>(cx) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
panel.update(cx, |panel, cx| {
|
||||||
|
panel.pull(window, cx);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|
|
@ -10,8 +10,7 @@ use editor::{
|
||||||
use feature_flags::FeatureFlagViewExt;
|
use feature_flags::FeatureFlagViewExt;
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use git::{
|
use git::{
|
||||||
status::FileStatus, ShowCommitEditor, StageAll, StageAndNext, ToggleStaged, UnstageAll,
|
status::FileStatus, Commit, StageAll, StageAndNext, ToggleStaged, UnstageAll, UnstageAndNext,
|
||||||
UnstageAndNext,
|
|
||||||
};
|
};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, Action, AnyElement, AnyView, App, AppContext as _, AsyncWindowContext, Entity,
|
actions, Action, AnyElement, AnyView, App, AppContext as _, AsyncWindowContext, Entity,
|
||||||
|
@ -923,11 +922,11 @@ impl Render for ProjectDiffToolbar {
|
||||||
Button::new("commit", "Commit")
|
Button::new("commit", "Commit")
|
||||||
.tooltip(Tooltip::for_action_title_in(
|
.tooltip(Tooltip::for_action_title_in(
|
||||||
"Commit",
|
"Commit",
|
||||||
&ShowCommitEditor,
|
&Commit,
|
||||||
&focus_handle,
|
&focus_handle,
|
||||||
))
|
))
|
||||||
.on_click(cx.listener(|this, _, window, cx| {
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
this.dispatch_action(&ShowCommitEditor, window, cx);
|
this.dispatch_action(&Commit, window, cx);
|
||||||
})),
|
})),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,22 +1,14 @@
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Subscription,
|
AnyElement, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Task, WeakEntity,
|
||||||
Task, WeakEntity,
|
|
||||||
};
|
};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use picker::{Picker, PickerDelegate};
|
use picker::{Picker, PickerDelegate};
|
||||||
use project::{
|
use project::{git::Repository, Project};
|
||||||
git::{GitStore, Repository},
|
|
||||||
Project,
|
|
||||||
};
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use ui::{prelude::*, ListItem, ListItemSpacing};
|
use ui::{prelude::*, ListItem, ListItemSpacing};
|
||||||
|
|
||||||
pub struct RepositorySelector {
|
pub struct RepositorySelector {
|
||||||
picker: Entity<Picker<RepositorySelectorDelegate>>,
|
picker: Entity<Picker<RepositorySelectorDelegate>>,
|
||||||
/// The task used to update the picker's matches when there is a change to
|
|
||||||
/// the repository list.
|
|
||||||
update_matches_task: Option<Task<()>>,
|
|
||||||
_subscriptions: Vec<Subscription>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RepositorySelector {
|
impl RepositorySelector {
|
||||||
|
@ -51,30 +43,7 @@ impl RepositorySelector {
|
||||||
.max_height(Some(rems(20.).into()))
|
.max_height(Some(rems(20.).into()))
|
||||||
});
|
});
|
||||||
|
|
||||||
let _subscriptions =
|
RepositorySelector { picker }
|
||||||
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<GitStore>,
|
|
||||||
_event: &project::git::GitEvent,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,11 +27,13 @@ test-support = [
|
||||||
[dependencies]
|
[dependencies]
|
||||||
aho-corasick.workspace = true
|
aho-corasick.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
|
askpass.workspace = true
|
||||||
async-trait.workspace = true
|
async-trait.workspace = true
|
||||||
|
buffer_diff.workspace = true
|
||||||
client.workspace = true
|
client.workspace = true
|
||||||
clock.workspace = true
|
clock.workspace = true
|
||||||
collections.workspace = true
|
collections.workspace = true
|
||||||
buffer_diff.workspace = true
|
fancy-regex.workspace = true
|
||||||
fs.workspace = true
|
fs.workspace = true
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
fuzzy.workspace = true
|
fuzzy.workspace = true
|
||||||
|
@ -39,25 +41,22 @@ git.workspace = true
|
||||||
globset.workspace = true
|
globset.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
http_client.workspace = true
|
http_client.workspace = true
|
||||||
|
image.workspace = true
|
||||||
itertools.workspace = true
|
itertools.workspace = true
|
||||||
language.workspace = true
|
language.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
lsp.workspace = true
|
lsp.workspace = true
|
||||||
node_runtime.workspace = true
|
node_runtime.workspace = true
|
||||||
image.workspace = true
|
|
||||||
parking_lot.workspace = true
|
parking_lot.workspace = true
|
||||||
pathdiff.workspace = true
|
pathdiff.workspace = true
|
||||||
paths.workspace = true
|
paths.workspace = true
|
||||||
postage.workspace = true
|
postage.workspace = true
|
||||||
prettier.workspace = true
|
prettier.workspace = true
|
||||||
worktree.workspace = true
|
|
||||||
rand.workspace = true
|
rand.workspace = true
|
||||||
regex.workspace = true
|
regex.workspace = true
|
||||||
remote.workspace = true
|
remote.workspace = true
|
||||||
rpc.workspace = true
|
rpc.workspace = true
|
||||||
schemars.workspace = true
|
schemars.workspace = true
|
||||||
task.workspace = true
|
|
||||||
tempfile.workspace = true
|
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
settings.workspace = true
|
settings.workspace = true
|
||||||
|
@ -67,13 +66,15 @@ shlex.workspace = true
|
||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
snippet.workspace = true
|
snippet.workspace = true
|
||||||
snippet_provider.workspace = true
|
snippet_provider.workspace = true
|
||||||
|
task.workspace = true
|
||||||
|
tempfile.workspace = true
|
||||||
terminal.workspace = true
|
terminal.workspace = true
|
||||||
text.workspace = true
|
text.workspace = true
|
||||||
toml.workspace = true
|
toml.workspace = true
|
||||||
util.workspace = true
|
|
||||||
url.workspace = true
|
url.workspace = true
|
||||||
|
util.workspace = true
|
||||||
which.workspace = true
|
which.workspace = true
|
||||||
fancy-regex.workspace = true
|
worktree.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
client = { workspace = true, features = ["test-support"] }
|
client = { workspace = true, features = ["test-support"] }
|
||||||
|
|
|
@ -4,8 +4,10 @@ use crate::{
|
||||||
Project, ProjectItem, ProjectPath,
|
Project, ProjectItem, ProjectPath,
|
||||||
};
|
};
|
||||||
use anyhow::{Context as _, Result};
|
use anyhow::{Context as _, Result};
|
||||||
|
use askpass::{AskPassDelegate, AskPassSession};
|
||||||
use buffer_diff::BufferDiffEvent;
|
use buffer_diff::BufferDiffEvent;
|
||||||
use client::ProjectId;
|
use client::ProjectId;
|
||||||
|
use collections::HashMap;
|
||||||
use futures::{
|
use futures::{
|
||||||
channel::{mpsc, oneshot},
|
channel::{mpsc, oneshot},
|
||||||
StreamExt as _,
|
StreamExt as _,
|
||||||
|
@ -22,6 +24,7 @@ use gpui::{
|
||||||
WeakEntity,
|
WeakEntity,
|
||||||
};
|
};
|
||||||
use language::{Buffer, LanguageRegistry};
|
use language::{Buffer, LanguageRegistry};
|
||||||
|
use parking_lot::Mutex;
|
||||||
use rpc::{
|
use rpc::{
|
||||||
proto::{self, git_reset, ToProto},
|
proto::{self, git_reset, ToProto},
|
||||||
AnyProtoClient, TypedEnvelope,
|
AnyProtoClient, TypedEnvelope,
|
||||||
|
@ -34,13 +37,13 @@ use std::{
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
use text::BufferId;
|
use text::BufferId;
|
||||||
use util::{maybe, ResultExt};
|
use util::{debug_panic, maybe, ResultExt};
|
||||||
use worktree::{ProjectEntryId, RepositoryEntry, StatusEntry, WorkDirectory};
|
use worktree::{ProjectEntryId, RepositoryEntry, StatusEntry, WorkDirectory};
|
||||||
|
|
||||||
pub struct GitStore {
|
pub struct GitStore {
|
||||||
buffer_store: Entity<BufferStore>,
|
buffer_store: Entity<BufferStore>,
|
||||||
pub(super) project_id: Option<ProjectId>,
|
pub(super) project_id: Option<ProjectId>,
|
||||||
pub(super) client: Option<AnyProtoClient>,
|
pub(super) client: AnyProtoClient,
|
||||||
repositories: Vec<Entity<Repository>>,
|
repositories: Vec<Entity<Repository>>,
|
||||||
active_index: Option<usize>,
|
active_index: Option<usize>,
|
||||||
update_sender: mpsc::UnboundedSender<GitJob>,
|
update_sender: mpsc::UnboundedSender<GitJob>,
|
||||||
|
@ -55,6 +58,8 @@ pub struct Repository {
|
||||||
pub git_repo: GitRepo,
|
pub git_repo: GitRepo,
|
||||||
pub merge_message: Option<String>,
|
pub merge_message: Option<String>,
|
||||||
job_sender: mpsc::UnboundedSender<GitJob>,
|
job_sender: mpsc::UnboundedSender<GitJob>,
|
||||||
|
askpass_delegates: Arc<Mutex<HashMap<u64, AskPassDelegate>>>,
|
||||||
|
latest_askpass_id: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
@ -92,7 +97,7 @@ impl GitStore {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
worktree_store: &Entity<WorktreeStore>,
|
worktree_store: &Entity<WorktreeStore>,
|
||||||
buffer_store: Entity<BufferStore>,
|
buffer_store: Entity<BufferStore>,
|
||||||
client: Option<AnyProtoClient>,
|
client: AnyProtoClient,
|
||||||
project_id: Option<ProjectId>,
|
project_id: Option<ProjectId>,
|
||||||
cx: &mut Context<'_, Self>,
|
cx: &mut Context<'_, Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
@ -129,6 +134,7 @@ impl GitStore {
|
||||||
client.add_entity_request_handler(Self::handle_checkout_files);
|
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_open_commit_message_buffer);
|
||||||
client.add_entity_request_handler(Self::handle_set_index_text);
|
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);
|
client.add_entity_request_handler(Self::handle_check_for_pushed_commits);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -164,7 +170,7 @@ impl GitStore {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.or_else(|| {
|
.or_else(|| {
|
||||||
let client = client.clone()?;
|
let client = client.clone();
|
||||||
let project_id = project_id?;
|
let project_id = project_id?;
|
||||||
Some((
|
Some((
|
||||||
GitRepo::Remote {
|
GitRepo::Remote {
|
||||||
|
@ -216,6 +222,8 @@ impl GitStore {
|
||||||
cx.new(|_| Repository {
|
cx.new(|_| Repository {
|
||||||
git_store: this.clone(),
|
git_store: this.clone(),
|
||||||
worktree_id,
|
worktree_id,
|
||||||
|
askpass_delegates: Default::default(),
|
||||||
|
latest_askpass_id: 0,
|
||||||
repository_entry: repo.clone(),
|
repository_entry: repo.clone(),
|
||||||
git_repo,
|
git_repo,
|
||||||
job_sender: self.update_sender.clone(),
|
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 work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
|
||||||
let repository_handle =
|
let repository_handle =
|
||||||
Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
|
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
|
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??;
|
.await??;
|
||||||
|
|
||||||
Ok(proto::RemoteMessageResponse {
|
Ok(proto::RemoteMessageResponse {
|
||||||
|
@ -383,6 +403,16 @@ impl GitStore {
|
||||||
let repository_handle =
|
let repository_handle =
|
||||||
Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
|
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
|
let options = envelope
|
||||||
.payload
|
.payload
|
||||||
.options
|
.options
|
||||||
|
@ -396,8 +426,8 @@ impl GitStore {
|
||||||
let remote_name = envelope.payload.remote_name.into();
|
let remote_name = envelope.payload.remote_name.into();
|
||||||
|
|
||||||
let remote_output = repository_handle
|
let remote_output = repository_handle
|
||||||
.update(&mut cx, |repository_handle, _cx| {
|
.update(&mut cx, |repository_handle, cx| {
|
||||||
repository_handle.push(branch_name, remote_name, options)
|
repository_handle.push(branch_name, remote_name, options, askpass, cx)
|
||||||
})?
|
})?
|
||||||
.await??;
|
.await??;
|
||||||
Ok(proto::RemoteMessageResponse {
|
Ok(proto::RemoteMessageResponse {
|
||||||
|
@ -415,15 +445,25 @@ impl GitStore {
|
||||||
let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
|
let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
|
||||||
let repository_handle =
|
let repository_handle =
|
||||||
Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
|
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 branch_name = envelope.payload.branch_name.into();
|
||||||
let remote_name = envelope.payload.remote_name.into();
|
let remote_name = envelope.payload.remote_name.into();
|
||||||
|
|
||||||
let remote_message = repository_handle
|
let remote_message = repository_handle
|
||||||
.update(&mut cx, |repository_handle, _cx| {
|
.update(&mut cx, |repository_handle, cx| {
|
||||||
repository_handle.pull(branch_name, remote_name)
|
repository_handle.pull(branch_name, remote_name, askpass, cx)
|
||||||
})?
|
})?
|
||||||
.await??;
|
.await??;
|
||||||
|
|
||||||
Ok(proto::RemoteMessageResponse {
|
Ok(proto::RemoteMessageResponse {
|
||||||
stdout: remote_message.stdout,
|
stdout: remote_message.stdout,
|
||||||
stderr: remote_message.stderr,
|
stderr: remote_message.stderr,
|
||||||
|
@ -719,6 +759,31 @@ impl GitStore {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn handle_askpass(
|
||||||
|
this: Entity<Self>,
|
||||||
|
envelope: TypedEnvelope<proto::AskPassRequest>,
|
||||||
|
mut cx: AsyncApp,
|
||||||
|
) -> Result<proto::AskPassResponse> {
|
||||||
|
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(
|
async fn handle_check_for_pushed_commits(
|
||||||
this: Entity<Self>,
|
this: Entity<Self>,
|
||||||
envelope: TypedEnvelope<proto::CheckForPushedCommits>,
|
envelope: TypedEnvelope<proto::CheckForPushedCommits>,
|
||||||
|
@ -765,6 +830,33 @@ impl GitStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn make_remote_delegate(
|
||||||
|
this: Entity<GitStore>,
|
||||||
|
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 GitRepo {}
|
||||||
|
|
||||||
impl Repository {
|
impl Repository {
|
||||||
|
@ -1286,21 +1378,39 @@ impl Repository {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fetch(&self) -> oneshot::Receiver<Result<RemoteCommandOutput>> {
|
pub fn fetch(
|
||||||
self.send_job(|git_repo| async move {
|
&mut self,
|
||||||
|
askpass: AskPassDelegate,
|
||||||
|
cx: &App,
|
||||||
|
) -> oneshot::Receiver<Result<RemoteCommandOutput>> {
|
||||||
|
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 {
|
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 {
|
GitRepo::Remote {
|
||||||
project_id,
|
project_id,
|
||||||
client,
|
client,
|
||||||
worktree_id,
|
worktree_id,
|
||||||
work_directory_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
|
let response = client
|
||||||
.request(proto::Fetch {
|
.request(proto::Fetch {
|
||||||
project_id: project_id.0,
|
project_id: project_id.0,
|
||||||
worktree_id: worktree_id.to_proto(),
|
worktree_id: worktree_id.to_proto(),
|
||||||
work_directory_id: work_directory_id.to_proto(),
|
work_directory_id: work_directory_id.to_proto(),
|
||||||
|
askpass_id,
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.context("sending fetch request")?;
|
.context("sending fetch request")?;
|
||||||
|
@ -1315,25 +1425,40 @@ impl Repository {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn push(
|
pub fn push(
|
||||||
&self,
|
&mut self,
|
||||||
branch: SharedString,
|
branch: SharedString,
|
||||||
remote: SharedString,
|
remote: SharedString,
|
||||||
options: Option<PushOptions>,
|
options: Option<PushOptions>,
|
||||||
|
askpass: AskPassDelegate,
|
||||||
|
cx: &App,
|
||||||
) -> oneshot::Receiver<Result<RemoteCommandOutput>> {
|
) -> oneshot::Receiver<Result<RemoteCommandOutput>> {
|
||||||
|
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 {
|
self.send_job(move |git_repo| async move {
|
||||||
match git_repo {
|
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 {
|
GitRepo::Remote {
|
||||||
project_id,
|
project_id,
|
||||||
client,
|
client,
|
||||||
worktree_id,
|
worktree_id,
|
||||||
work_directory_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
|
let response = client
|
||||||
.request(proto::Push {
|
.request(proto::Push {
|
||||||
project_id: project_id.0,
|
project_id: project_id.0,
|
||||||
worktree_id: worktree_id.to_proto(),
|
worktree_id: worktree_id.to_proto(),
|
||||||
work_directory_id: work_directory_id.to_proto(),
|
work_directory_id: work_directory_id.to_proto(),
|
||||||
|
askpass_id,
|
||||||
branch_name: branch.to_string(),
|
branch_name: branch.to_string(),
|
||||||
remote_name: remote.to_string(),
|
remote_name: remote.to_string(),
|
||||||
options: options.map(|options| match options {
|
options: options.map(|options| match options {
|
||||||
|
@ -1354,24 +1479,38 @@ impl Repository {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn pull(
|
pub fn pull(
|
||||||
&self,
|
&mut self,
|
||||||
branch: SharedString,
|
branch: SharedString,
|
||||||
remote: SharedString,
|
remote: SharedString,
|
||||||
|
askpass: AskPassDelegate,
|
||||||
|
cx: &App,
|
||||||
) -> oneshot::Receiver<Result<RemoteCommandOutput>> {
|
) -> oneshot::Receiver<Result<RemoteCommandOutput>> {
|
||||||
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 {
|
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 {
|
GitRepo::Remote {
|
||||||
project_id,
|
project_id,
|
||||||
client,
|
client,
|
||||||
worktree_id,
|
worktree_id,
|
||||||
work_directory_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
|
let response = client
|
||||||
.request(proto::Pull {
|
.request(proto::Pull {
|
||||||
project_id: project_id.0,
|
project_id: project_id.0,
|
||||||
worktree_id: worktree_id.to_proto(),
|
worktree_id: worktree_id.to_proto(),
|
||||||
work_directory_id: work_directory_id.to_proto(),
|
work_directory_id: work_directory_id.to_proto(),
|
||||||
|
askpass_id,
|
||||||
branch_name: branch.to_string(),
|
branch_name: branch.to_string(),
|
||||||
remote_name: remote.to_string(),
|
remote_name: remote.to_string(),
|
||||||
})
|
})
|
||||||
|
|
|
@ -707,8 +707,15 @@ impl Project {
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
let git_store =
|
let git_store = cx.new(|cx| {
|
||||||
cx.new(|cx| GitStore::new(&worktree_store, buffer_store.clone(), None, None, cx));
|
GitStore::new(
|
||||||
|
&worktree_store,
|
||||||
|
buffer_store.clone(),
|
||||||
|
client.clone().into(),
|
||||||
|
None,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
cx.subscribe(&lsp_store, Self::on_lsp_store_event).detach();
|
cx.subscribe(&lsp_store, Self::on_lsp_store_event).detach();
|
||||||
|
|
||||||
|
@ -832,7 +839,7 @@ impl Project {
|
||||||
GitStore::new(
|
GitStore::new(
|
||||||
&worktree_store,
|
&worktree_store,
|
||||||
buffer_store.clone(),
|
buffer_store.clone(),
|
||||||
Some(ssh_proto.clone()),
|
ssh_proto.clone(),
|
||||||
Some(ProjectId(SSH_PROJECT_ID)),
|
Some(ProjectId(SSH_PROJECT_ID)),
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
|
@ -1040,7 +1047,7 @@ impl Project {
|
||||||
GitStore::new(
|
GitStore::new(
|
||||||
&worktree_store,
|
&worktree_store,
|
||||||
buffer_store.clone(),
|
buffer_store.clone(),
|
||||||
Some(client.clone().into()),
|
client.clone().into(),
|
||||||
Some(ProjectId(remote_id)),
|
Some(ProjectId(remote_id)),
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
|
|
|
@ -333,12 +333,16 @@ message Envelope {
|
||||||
ApplyCodeActionKindResponse apply_code_action_kind_response = 310;
|
ApplyCodeActionKindResponse apply_code_action_kind_response = 310;
|
||||||
|
|
||||||
RemoteMessageResponse remote_message_response = 311;
|
RemoteMessageResponse remote_message_response = 311;
|
||||||
|
|
||||||
GitGetBranches git_get_branches = 312;
|
GitGetBranches git_get_branches = 312;
|
||||||
GitCreateBranch git_create_branch = 313;
|
GitCreateBranch git_create_branch = 313;
|
||||||
GitChangeBranch git_change_branch = 314; // current max
|
GitChangeBranch git_change_branch = 314;
|
||||||
|
|
||||||
CheckForPushedCommits check_for_pushed_commits = 315;
|
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;
|
reserved 87 to 88;
|
||||||
|
@ -2818,6 +2822,7 @@ message Push {
|
||||||
string remote_name = 4;
|
string remote_name = 4;
|
||||||
string branch_name = 5;
|
string branch_name = 5;
|
||||||
optional PushOptions options = 6;
|
optional PushOptions options = 6;
|
||||||
|
uint64 askpass_id = 7;
|
||||||
|
|
||||||
enum PushOptions {
|
enum PushOptions {
|
||||||
SET_UPSTREAM = 0;
|
SET_UPSTREAM = 0;
|
||||||
|
@ -2829,6 +2834,7 @@ message Fetch {
|
||||||
uint64 project_id = 1;
|
uint64 project_id = 1;
|
||||||
uint64 worktree_id = 2;
|
uint64 worktree_id = 2;
|
||||||
uint64 work_directory_id = 3;
|
uint64 work_directory_id = 3;
|
||||||
|
uint64 askpass_id = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
message GetRemotes {
|
message GetRemotes {
|
||||||
|
@ -2852,6 +2858,7 @@ message Pull {
|
||||||
uint64 work_directory_id = 3;
|
uint64 work_directory_id = 3;
|
||||||
string remote_name = 4;
|
string remote_name = 4;
|
||||||
string branch_name = 5;
|
string branch_name = 5;
|
||||||
|
uint64 askpass_id = 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
message RemoteMessageResponse {
|
message RemoteMessageResponse {
|
||||||
|
@ -2859,6 +2866,18 @@ message RemoteMessageResponse {
|
||||||
string stderr = 2;
|
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 {
|
message GitGetBranches {
|
||||||
uint64 project_id = 1;
|
uint64 project_id = 1;
|
||||||
uint64 worktree_id = 2;
|
uint64 worktree_id = 2;
|
||||||
|
|
|
@ -452,6 +452,8 @@ messages!(
|
||||||
(GetRemotesResponse, Background),
|
(GetRemotesResponse, Background),
|
||||||
(Pull, Background),
|
(Pull, Background),
|
||||||
(RemoteMessageResponse, Background),
|
(RemoteMessageResponse, Background),
|
||||||
|
(AskPassRequest, Background),
|
||||||
|
(AskPassResponse, Background),
|
||||||
(GitCreateBranch, Background),
|
(GitCreateBranch, Background),
|
||||||
(GitChangeBranch, Background),
|
(GitChangeBranch, Background),
|
||||||
(CheckForPushedCommits, Background),
|
(CheckForPushedCommits, Background),
|
||||||
|
@ -598,6 +600,7 @@ request_messages!(
|
||||||
(Fetch, RemoteMessageResponse),
|
(Fetch, RemoteMessageResponse),
|
||||||
(GetRemotes, GetRemotesResponse),
|
(GetRemotes, GetRemotesResponse),
|
||||||
(Pull, RemoteMessageResponse),
|
(Pull, RemoteMessageResponse),
|
||||||
|
(AskPassRequest, AskPassResponse),
|
||||||
(GitCreateBranch, Ack),
|
(GitCreateBranch, Ack),
|
||||||
(GitChangeBranch, Ack),
|
(GitChangeBranch, Ack),
|
||||||
(CheckForPushedCommits, CheckForPushedCommitsResponse),
|
(CheckForPushedCommits, CheckForPushedCommitsResponse),
|
||||||
|
@ -702,6 +705,7 @@ entity_messages!(
|
||||||
Fetch,
|
Fetch,
|
||||||
GetRemotes,
|
GetRemotes,
|
||||||
Pull,
|
Pull,
|
||||||
|
AskPassRequest,
|
||||||
GitChangeBranch,
|
GitChangeBranch,
|
||||||
GitCreateBranch,
|
GitCreateBranch,
|
||||||
CheckForPushedCommits,
|
CheckForPushedCommits,
|
||||||
|
|
|
@ -131,7 +131,7 @@ pub struct SshPrompt {
|
||||||
connection_string: SharedString,
|
connection_string: SharedString,
|
||||||
nickname: Option<SharedString>,
|
nickname: Option<SharedString>,
|
||||||
status_message: Option<SharedString>,
|
status_message: Option<SharedString>,
|
||||||
prompt: Option<(Entity<Markdown>, oneshot::Sender<Result<String>>)>,
|
prompt: Option<(Entity<Markdown>, oneshot::Sender<String>)>,
|
||||||
cancellation: Option<oneshot::Sender<()>>,
|
cancellation: Option<oneshot::Sender<()>>,
|
||||||
editor: Entity<Editor>,
|
editor: Entity<Editor>,
|
||||||
}
|
}
|
||||||
|
@ -176,7 +176,7 @@ impl SshPrompt {
|
||||||
pub fn set_prompt(
|
pub fn set_prompt(
|
||||||
&mut self,
|
&mut self,
|
||||||
prompt: String,
|
prompt: String,
|
||||||
tx: oneshot::Sender<Result<String>>,
|
tx: oneshot::Sender<String>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
|
@ -223,7 +223,7 @@ impl SshPrompt {
|
||||||
if let Some((_, tx)) = self.prompt.take() {
|
if let Some((_, tx)) = self.prompt.take() {
|
||||||
self.status_message = Some("Connecting".into());
|
self.status_message = Some("Connecting".into());
|
||||||
self.editor.update(cx, |editor, cx| {
|
self.editor.update(cx, |editor, cx| {
|
||||||
tx.send(Ok(editor.text(cx))).ok();
|
tx.send(editor.text(cx)).ok();
|
||||||
editor.clear(window, cx);
|
editor.clear(window, cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -429,11 +429,10 @@ pub struct SshClientDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl remote::SshClientDelegate for SshClientDelegate {
|
impl remote::SshClientDelegate for SshClientDelegate {
|
||||||
fn ask_password(&self, prompt: String, cx: &mut AsyncApp) -> oneshot::Receiver<Result<String>> {
|
fn ask_password(&self, prompt: String, tx: oneshot::Sender<String>, cx: &mut AsyncApp) {
|
||||||
let (tx, rx) = oneshot::channel();
|
|
||||||
let mut known_password = self.known_password.clone();
|
let mut known_password = self.known_password.clone();
|
||||||
if let Some(password) = known_password.take() {
|
if let Some(password) = known_password.take() {
|
||||||
tx.send(Ok(password)).ok();
|
tx.send(password).ok();
|
||||||
} else {
|
} else {
|
||||||
self.window
|
self.window
|
||||||
.update(cx, |_, window, cx| {
|
.update(cx, |_, window, cx| {
|
||||||
|
@ -443,7 +442,6 @@ impl remote::SshClientDelegate for SshClientDelegate {
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
rx
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_status(&self, status: Option<&str>, cx: &mut AsyncApp) {
|
fn set_status(&self, status: Option<&str>, cx: &mut AsyncApp) {
|
||||||
|
|
|
@ -19,6 +19,7 @@ test-support = ["fs/test-support"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
|
askpass.workspace = true
|
||||||
async-trait.workspace = true
|
async-trait.workspace = true
|
||||||
collections.workspace = true
|
collections.workspace = true
|
||||||
fs.workspace = true
|
fs.workspace = true
|
||||||
|
@ -26,9 +27,10 @@ futures.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
itertools.workspace = true
|
itertools.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
paths.workspace = true
|
|
||||||
parking_lot.workspace = true
|
parking_lot.workspace = true
|
||||||
|
paths.workspace = true
|
||||||
prost.workspace = true
|
prost.workspace = true
|
||||||
|
release_channel.workspace = true
|
||||||
rpc = { workspace = true, features = ["gpui"] }
|
rpc = { workspace = true, features = ["gpui"] }
|
||||||
schemars.workspace = true
|
schemars.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
|
@ -38,8 +40,6 @@ smol.workspace = true
|
||||||
tempfile.workspace = true
|
tempfile.workspace = true
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
util.workspace = true
|
util.workspace = true
|
||||||
release_channel.workspace = true
|
|
||||||
which.workspace = true
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
gpui = { workspace = true, features = ["test-support"] }
|
gpui = { workspace = true, features = ["test-support"] }
|
||||||
|
|
|
@ -316,7 +316,7 @@ impl SshPlatform {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait SshClientDelegate: Send + Sync {
|
pub trait SshClientDelegate: Send + Sync {
|
||||||
fn ask_password(&self, prompt: String, cx: &mut AsyncApp) -> oneshot::Receiver<Result<String>>;
|
fn ask_password(&self, prompt: String, tx: oneshot::Sender<String>, cx: &mut AsyncApp);
|
||||||
fn get_download_params(
|
fn get_download_params(
|
||||||
&self,
|
&self,
|
||||||
platform: SshPlatform,
|
platform: SshPlatform,
|
||||||
|
@ -1454,83 +1454,22 @@ impl SshRemoteConnection {
|
||||||
delegate: Arc<dyn SshClientDelegate>,
|
delegate: Arc<dyn SshClientDelegate>,
|
||||||
cx: &mut AsyncApp,
|
cx: &mut AsyncApp,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
use futures::AsyncWriteExt as _;
|
use askpass::AskPassResult;
|
||||||
use futures::{io::BufReader, AsyncBufReadExt as _};
|
|
||||||
use smol::net::unix::UnixStream;
|
|
||||||
use smol::{fs::unix::PermissionsExt as _, net::unix::UnixListener};
|
|
||||||
use util::ResultExt as _;
|
|
||||||
|
|
||||||
delegate.set_status(Some("Connecting"), cx);
|
delegate.set_status(Some("Connecting"), cx);
|
||||||
|
|
||||||
let url = connection_options.ssh_url();
|
let url = connection_options.ssh_url();
|
||||||
|
|
||||||
let temp_dir = tempfile::Builder::new()
|
let temp_dir = tempfile::Builder::new()
|
||||||
.prefix("zed-ssh-session")
|
.prefix("zed-ssh-session")
|
||||||
.tempdir()?;
|
.tempdir()?;
|
||||||
|
let askpass_delegate = askpass::AskPassDelegate::new(cx, {
|
||||||
// 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::<UnixStream>();
|
|
||||||
let mut kill_tx = Some(askpass_kill_master_tx);
|
|
||||||
|
|
||||||
let askpass_task = cx.spawn({
|
|
||||||
let delegate = delegate.clone();
|
let delegate = delegate.clone();
|
||||||
|mut cx| async move {
|
move |prompt, tx, cx| delegate.ask_password(prompt, tx, cx)
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
anyhow::ensure!(
|
let mut askpass =
|
||||||
which::which("nc").is_ok(),
|
askpass::AskPassSession::new(cx.background_executor(), askpass_delegate).await?;
|
||||||
"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?;
|
|
||||||
|
|
||||||
// Start the master SSH process, which does not do anything except for establish
|
// 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
|
// the connection and keep it open, allowing other ssh commands to reuse it
|
||||||
|
@ -1542,7 +1481,7 @@ impl SshRemoteConnection {
|
||||||
.stdout(Stdio::piped())
|
.stdout(Stdio::piped())
|
||||||
.stderr(Stdio::piped())
|
.stderr(Stdio::piped())
|
||||||
.env("SSH_ASKPASS_REQUIRE", "force")
|
.env("SSH_ASKPASS_REQUIRE", "force")
|
||||||
.env("SSH_ASKPASS", &askpass_script_path)
|
.env("SSH_ASKPASS", &askpass.script_path())
|
||||||
.args(connection_options.additional_args())
|
.args(connection_options.additional_args())
|
||||||
.args([
|
.args([
|
||||||
"-N",
|
"-N",
|
||||||
|
@ -1556,35 +1495,25 @@ impl SshRemoteConnection {
|
||||||
.arg(&url)
|
.arg(&url)
|
||||||
.kill_on_drop(true)
|
.kill_on_drop(true)
|
||||||
.spawn()?;
|
.spawn()?;
|
||||||
|
|
||||||
// Wait for this ssh process to close its stdout, indicating that authentication
|
// Wait for this ssh process to close its stdout, indicating that authentication
|
||||||
// has completed.
|
// has completed.
|
||||||
let mut stdout = master_process.stdout.take().unwrap();
|
let mut stdout = master_process.stdout.take().unwrap();
|
||||||
let mut output = Vec::new();
|
let mut output = Vec::new();
|
||||||
let connection_timeout = Duration::from_secs(10);
|
|
||||||
|
|
||||||
let result = select_biased! {
|
let result = select_biased! {
|
||||||
_ = askpass_opened_rx.fuse() => {
|
result = askpass.run().fuse() => {
|
||||||
select_biased! {
|
match result {
|
||||||
stream = askpass_kill_master_rx.fuse() => {
|
AskPassResult::CancelledByUser => {
|
||||||
master_process.kill().ok();
|
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
|
AskPassResult::Timedout => {
|
||||||
// their password, in which case we don't want to timeout anymore,
|
Err(anyhow!("connecting to host timed out"))?
|
||||||
// since we know a connection has been established.
|
|
||||||
result = stdout.read_to_end(&mut output).fuse() => {
|
|
||||||
result?;
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ = stdout.read_to_end(&mut output).fuse() => {
|
_ = stdout.read_to_end(&mut output).fuse() => {
|
||||||
Ok(())
|
anyhow::Ok(())
|
||||||
}
|
|
||||||
_ = futures::FutureExt::fuse(smol::Timer::after(connection_timeout)) => {
|
|
||||||
Err(anyhow!("Exceeded {:?} timeout trying to connect to host", connection_timeout))
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1592,8 +1521,6 @@ impl SshRemoteConnection {
|
||||||
return Err(e.context("Failed to connect to host"));
|
return Err(e.context("Failed to connect to host"));
|
||||||
}
|
}
|
||||||
|
|
||||||
drop(askpass_task);
|
|
||||||
|
|
||||||
if master_process.try_status()?.is_some() {
|
if master_process.try_status()?.is_some() {
|
||||||
output.clear();
|
output.clear();
|
||||||
let mut stderr = master_process.stderr.take().unwrap();
|
let mut stderr = master_process.stderr.take().unwrap();
|
||||||
|
@ -1606,6 +1533,8 @@ impl SshRemoteConnection {
|
||||||
Err(anyhow!(error_message))?;
|
Err(anyhow!(error_message))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
drop(askpass);
|
||||||
|
|
||||||
let socket = SshSocket {
|
let socket = SshSocket {
|
||||||
connection_options,
|
connection_options,
|
||||||
socket_path,
|
socket_path,
|
||||||
|
@ -2558,7 +2487,7 @@ mod fake {
|
||||||
pub(super) struct Delegate;
|
pub(super) struct Delegate;
|
||||||
|
|
||||||
impl SshClientDelegate for Delegate {
|
impl SshClientDelegate for Delegate {
|
||||||
fn ask_password(&self, _: String, _: &mut AsyncApp) -> oneshot::Receiver<Result<String>> {
|
fn ask_password(&self, _: String, _: oneshot::Sender<String>, _: &mut AsyncApp) {
|
||||||
unreachable!()
|
unreachable!()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -87,8 +87,15 @@ impl HeadlessProject {
|
||||||
buffer_store
|
buffer_store
|
||||||
});
|
});
|
||||||
|
|
||||||
let git_store =
|
let git_store = cx.new(|cx| {
|
||||||
cx.new(|cx| GitStore::new(&worktree_store, buffer_store.clone(), None, None, cx));
|
GitStore::new(
|
||||||
|
&worktree_store,
|
||||||
|
buffer_store.clone(),
|
||||||
|
session.clone().into(),
|
||||||
|
None,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
let prettier_store = cx.new(|cx| {
|
let prettier_store = cx.new(|cx| {
|
||||||
PrettierStore::new(
|
PrettierStore::new(
|
||||||
node_runtime.clone(),
|
node_runtime.clone(),
|
||||||
|
|
|
@ -508,7 +508,6 @@ fn main() {
|
||||||
outline::init(cx);
|
outline::init(cx);
|
||||||
project_symbols::init(cx);
|
project_symbols::init(cx);
|
||||||
project_panel::init(cx);
|
project_panel::init(cx);
|
||||||
git_ui::git_panel::init(cx);
|
|
||||||
outline_panel::init(cx);
|
outline_panel::init(cx);
|
||||||
component_preview::init(cx);
|
component_preview::init(cx);
|
||||||
tasks_ui::init(cx);
|
tasks_ui::init(cx);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue