Add user-visible output for remote operations (#25849)

This PR adds toasts for reporting success and errors from remote git
operations. This PR also adds a focus handle to notifications, in
anticipation of making them keyboard accessible.

Release Notes:

- N/A

---------

Co-authored-by: julia <julia@zed.dev>
This commit is contained in:
Mikayla Maki 2025-03-03 01:20:15 -08:00 committed by GitHub
parent 508b9d3b5d
commit 73ac19958a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 713 additions and 192 deletions

View file

@ -34,6 +34,7 @@ text.workspace = true
time.workspace = true
url.workspace = true
util.workspace = true
tempfile.workspace = true
[dev-dependencies]
pretty_assertions.workspace = true

View file

@ -11,6 +11,8 @@ use schemars::JsonSchema;
use serde::Deserialize;
use std::borrow::Borrow;
use std::io::Write as _;
#[cfg(not(windows))]
use std::os::unix::fs::PermissionsExt;
use std::process::Stdio;
use std::sync::LazyLock;
use std::{
@ -61,6 +63,12 @@ pub enum UpstreamTracking {
Tracked(UpstreamTrackingStatus),
}
impl From<UpstreamTrackingStatus> for UpstreamTracking {
fn from(status: UpstreamTrackingStatus) -> Self {
UpstreamTracking::Tracked(status)
}
}
impl UpstreamTracking {
pub fn is_gone(&self) -> bool {
matches!(self, UpstreamTracking::Gone)
@ -74,9 +82,15 @@ impl UpstreamTracking {
}
}
impl From<UpstreamTrackingStatus> for UpstreamTracking {
fn from(status: UpstreamTrackingStatus) -> Self {
UpstreamTracking::Tracked(status)
#[derive(Debug)]
pub struct RemoteCommandOutput {
pub stdout: String,
pub stderr: String,
}
impl RemoteCommandOutput {
pub fn is_empty(&self) -> bool {
self.stdout.is_empty() && self.stderr.is_empty()
}
}
@ -185,10 +199,10 @@ pub trait GitRepository: Send + Sync {
branch_name: &str,
upstream_name: &str,
options: Option<PushOptions>,
) -> Result<()>;
fn pull(&self, branch_name: &str, upstream_name: &str) -> Result<()>;
) -> Result<RemoteCommandOutput>;
fn pull(&self, branch_name: &str, upstream_name: &str) -> Result<RemoteCommandOutput>;
fn get_remotes(&self, branch_name: Option<&str>) -> Result<Vec<Remote>>;
fn fetch(&self) -> Result<()>;
fn fetch(&self) -> Result<RemoteCommandOutput>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, JsonSchema)]
@ -611,19 +625,30 @@ impl GitRepository for RealGitRepository {
branch_name: &str,
remote_name: &str,
options: Option<PushOptions>,
) -> Result<()> {
) -> Result<RemoteCommandOutput> {
let working_directory = self.working_directory()?;
let output = new_std_command("git")
// We do this on every operation to ensure that the askpass script exists and is executable.
#[cfg(not(windows))]
let (askpass_script_path, _temp_dir) = setup_askpass()?;
let mut command = new_std_command("git");
command
.current_dir(&working_directory)
.args(["push", "--quiet"])
.args(["push"])
.args(options.map(|option| match option {
PushOptions::SetUpstream => "--set-upstream",
PushOptions::Force => "--force-with-lease",
}))
.arg(remote_name)
.arg(format!("{}:{}", branch_name, branch_name))
.output()?;
.arg(format!("{}:{}", branch_name, branch_name));
#[cfg(not(windows))]
{
command.env("GIT_ASKPASS", askpass_script_path);
}
let output = command.output()?;
if !output.status.success() {
return Err(anyhow!(
@ -631,19 +656,33 @@ impl GitRepository for RealGitRepository {
String::from_utf8_lossy(&output.stderr)
));
} else {
Ok(())
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<()> {
fn pull(&self, branch_name: &str, remote_name: &str) -> Result<RemoteCommandOutput> {
let working_directory = self.working_directory()?;
let output = new_std_command("git")
// We do this on every operation to ensure that the askpass script exists and is executable.
#[cfg(not(windows))]
let (askpass_script_path, _temp_dir) = setup_askpass()?;
let mut command = new_std_command("git");
command
.current_dir(&working_directory)
.args(["pull", "--quiet"])
.args(["pull"])
.arg(remote_name)
.arg(branch_name)
.output()?;
.arg(branch_name);
#[cfg(not(windows))]
{
command.env("GIT_ASKPASS", askpass_script_path);
}
let output = command.output()?;
if !output.status.success() {
return Err(anyhow!(
@ -651,17 +690,31 @@ impl GitRepository for RealGitRepository {
String::from_utf8_lossy(&output.stderr)
));
} else {
return Ok(());
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<()> {
fn fetch(&self) -> Result<RemoteCommandOutput> {
let working_directory = self.working_directory()?;
let output = new_std_command("git")
// We do this on every operation to ensure that the askpass script exists and is executable.
#[cfg(not(windows))]
let (askpass_script_path, _temp_dir) = setup_askpass()?;
let mut command = new_std_command("git");
command
.current_dir(&working_directory)
.args(["fetch", "--quiet", "--all"])
.output()?;
.args(["fetch", "--all"]);
#[cfg(not(windows))]
{
command.env("GIT_ASKPASS", askpass_script_path);
}
let output = command.output()?;
if !output.status.success() {
return Err(anyhow!(
@ -669,7 +722,10 @@ impl GitRepository for RealGitRepository {
String::from_utf8_lossy(&output.stderr)
));
} else {
return Ok(());
return Ok(RemoteCommandOutput {
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
});
}
}
@ -716,6 +772,18 @@ impl GitRepository for RealGitRepository {
}
}
#[cfg(not(windows))]
fn setup_askpass() -> Result<(PathBuf, tempfile::TempDir), anyhow::Error> {
let temp_dir = tempfile::Builder::new()
.prefix("zed-git-askpass")
.tempdir()?;
let askpass_script = "#!/bin/sh\necho ''";
let askpass_script_path = temp_dir.path().join("git-askpass.sh");
std::fs::write(&askpass_script_path, askpass_script)?;
std::fs::set_permissions(&askpass_script_path, std::fs::Permissions::from_mode(0o755))?;
Ok((askpass_script_path, temp_dir))
}
#[derive(Debug, Clone)]
pub struct FakeGitRepository {
state: Arc<Mutex<FakeGitRepositoryState>>,
@ -899,15 +967,20 @@ impl GitRepository for FakeGitRepository {
unimplemented!()
}
fn push(&self, _branch: &str, _remote: &str, _options: Option<PushOptions>) -> Result<()> {
fn push(
&self,
_branch: &str,
_remote: &str,
_options: Option<PushOptions>,
) -> Result<RemoteCommandOutput> {
unimplemented!()
}
fn pull(&self, _branch: &str, _remote: &str) -> Result<()> {
fn pull(&self, _branch: &str, _remote: &str) -> Result<RemoteCommandOutput> {
unimplemented!()
}
fn fetch(&self) -> Result<()> {
fn fetch(&self) -> Result<RemoteCommandOutput> {
unimplemented!()
}