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
|
@ -16,6 +16,7 @@ test-support = []
|
|||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
askpass.workspace = true
|
||||
async-trait.workspace = true
|
||||
collections.workspace = true
|
||||
derive_more.workspace = true
|
||||
|
@ -34,7 +35,7 @@ text.workspace = true
|
|||
time.workspace = true
|
||||
url.workspace = true
|
||||
util.workspace = true
|
||||
tempfile.workspace = true
|
||||
futures.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions.workspace = true
|
||||
|
|
|
@ -8,9 +8,6 @@ pub mod status;
|
|||
use anyhow::{anyhow, Context as _, Result};
|
||||
use gpui::action_with_deprecated_aliases;
|
||||
use gpui::actions;
|
||||
use gpui::impl_actions;
|
||||
use repository::PushOptions;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::ffi::OsStr;
|
||||
use std::fmt;
|
||||
|
@ -31,13 +28,6 @@ pub static COMMIT_MESSAGE: LazyLock<&'static OsStr> =
|
|||
LazyLock::new(|| OsStr::new("COMMIT_EDITMSG"));
|
||||
pub static INDEX_LOCK: LazyLock<&'static OsStr> = LazyLock::new(|| OsStr::new("index.lock"));
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Deserialize, JsonSchema)]
|
||||
pub struct Push {
|
||||
pub options: Option<PushOptions>,
|
||||
}
|
||||
|
||||
impl_actions!(git, [Push]);
|
||||
|
||||
actions!(
|
||||
git,
|
||||
[
|
||||
|
@ -54,10 +44,12 @@ actions!(
|
|||
RestoreTrackedFiles,
|
||||
TrashUntrackedFiles,
|
||||
Uncommit,
|
||||
Push,
|
||||
ForcePush,
|
||||
Pull,
|
||||
Fetch,
|
||||
Commit,
|
||||
ShowCommitEditor,
|
||||
ExpandCommitEditor
|
||||
]
|
||||
);
|
||||
action_with_deprecated_aliases!(git, RestoreFile, ["editor::RevertFile"]);
|
||||
|
|
|
@ -2,7 +2,9 @@ use crate::status::FileStatus;
|
|||
use crate::GitHostingProviderRegistry;
|
||||
use crate::{blame::Blame, status::GitStatus};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use askpass::{AskPassResult, AskPassSession};
|
||||
use collections::{HashMap, HashSet};
|
||||
use futures::{select_biased, FutureExt as _};
|
||||
use git2::BranchType;
|
||||
use gpui::SharedString;
|
||||
use parking_lot::Mutex;
|
||||
|
@ -11,8 +13,6 @@ use schemars::JsonSchema;
|
|||
use serde::Deserialize;
|
||||
use std::borrow::Borrow;
|
||||
use std::io::Write as _;
|
||||
#[cfg(not(windows))]
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::process::Stdio;
|
||||
use std::sync::LazyLock;
|
||||
use std::{
|
||||
|
@ -21,9 +21,11 @@ use std::{
|
|||
sync::Arc,
|
||||
};
|
||||
use sum_tree::MapSeekTarget;
|
||||
use util::command::new_std_command;
|
||||
use util::command::{new_smol_command, new_std_command};
|
||||
use util::ResultExt;
|
||||
|
||||
pub const REMOTE_CANCELLED_BY_USER: &str = "Operation cancelled by user";
|
||||
|
||||
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
|
||||
pub struct Branch {
|
||||
pub is_head: bool,
|
||||
|
@ -200,9 +202,16 @@ pub trait GitRepository: Send + Sync {
|
|||
branch_name: &str,
|
||||
upstream_name: &str,
|
||||
options: Option<PushOptions>,
|
||||
askpass: AskPassSession,
|
||||
) -> 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>>;
|
||||
|
||||
|
@ -578,7 +587,6 @@ impl GitRepository for RealGitRepository {
|
|||
.args(paths.iter().map(|p| p.as_ref()))
|
||||
.output()?;
|
||||
|
||||
// TODO: Get remote response out of this and show it to the user
|
||||
if !output.status.success() {
|
||||
return Err(anyhow!(
|
||||
"Failed to stage paths:\n{}",
|
||||
|
@ -599,7 +607,6 @@ impl GitRepository for RealGitRepository {
|
|||
.args(paths.iter().map(|p| p.as_ref()))
|
||||
.output()?;
|
||||
|
||||
// TODO: Get remote response out of this and show it to the user
|
||||
if !output.status.success() {
|
||||
return Err(anyhow!(
|
||||
"Failed to unstage:\n{}",
|
||||
|
@ -625,7 +632,6 @@ impl GitRepository for RealGitRepository {
|
|||
|
||||
let output = cmd.output()?;
|
||||
|
||||
// TODO: Get remote response out of this and show it to the user
|
||||
if !output.status.success() {
|
||||
return Err(anyhow!(
|
||||
"Failed to commit:\n{}",
|
||||
|
@ -640,15 +646,15 @@ impl GitRepository for RealGitRepository {
|
|||
branch_name: &str,
|
||||
remote_name: &str,
|
||||
options: Option<PushOptions>,
|
||||
ask_pass: AskPassSession,
|
||||
) -> Result<RemoteCommandOutput> {
|
||||
let working_directory = self.working_directory()?;
|
||||
|
||||
// We do this on every operation to ensure that the askpass script exists and is executable.
|
||||
#[cfg(not(windows))]
|
||||
let (askpass_script_path, _temp_dir) = setup_askpass()?;
|
||||
|
||||
let mut command = new_std_command("git");
|
||||
let mut command = new_smol_command("git");
|
||||
command
|
||||
.env("GIT_ASKPASS", ask_pass.script_path())
|
||||
.env("SSH_ASKPASS", ask_pass.script_path())
|
||||
.env("SSH_ASKPASS_REQUIRE", "force")
|
||||
.current_dir(&working_directory)
|
||||
.args(["push"])
|
||||
.args(options.map(|option| match option {
|
||||
|
@ -657,91 +663,46 @@ impl GitRepository for RealGitRepository {
|
|||
}))
|
||||
.arg(remote_name)
|
||||
.arg(format!("{}:{}", branch_name, branch_name));
|
||||
let git_process = command.spawn()?;
|
||||
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
command.env("GIT_ASKPASS", askpass_script_path);
|
||||
}
|
||||
|
||||
let output = command.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(anyhow!(
|
||||
"Failed to push:\n{}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
));
|
||||
} else {
|
||||
return Ok(RemoteCommandOutput {
|
||||
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
|
||||
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
|
||||
});
|
||||
}
|
||||
run_remote_command(ask_pass, git_process)
|
||||
}
|
||||
|
||||
fn pull(&self, branch_name: &str, remote_name: &str) -> Result<RemoteCommandOutput> {
|
||||
fn pull(
|
||||
&self,
|
||||
branch_name: &str,
|
||||
remote_name: &str,
|
||||
ask_pass: AskPassSession,
|
||||
) -> Result<RemoteCommandOutput> {
|
||||
let working_directory = self.working_directory()?;
|
||||
|
||||
// We do this on every operation to ensure that the askpass script exists and is executable.
|
||||
#[cfg(not(windows))]
|
||||
let (askpass_script_path, _temp_dir) = setup_askpass()?;
|
||||
|
||||
let mut command = new_std_command("git");
|
||||
let mut command = new_smol_command("git");
|
||||
command
|
||||
.env("GIT_ASKPASS", ask_pass.script_path())
|
||||
.env("SSH_ASKPASS", ask_pass.script_path())
|
||||
.env("SSH_ASKPASS_REQUIRE", "force")
|
||||
.current_dir(&working_directory)
|
||||
.args(["pull"])
|
||||
.arg(remote_name)
|
||||
.arg(branch_name);
|
||||
let git_process = command.spawn()?;
|
||||
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
command.env("GIT_ASKPASS", askpass_script_path);
|
||||
}
|
||||
|
||||
let output = command.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(anyhow!(
|
||||
"Failed to pull:\n{}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
));
|
||||
} else {
|
||||
return Ok(RemoteCommandOutput {
|
||||
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
|
||||
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
|
||||
});
|
||||
}
|
||||
run_remote_command(ask_pass, git_process)
|
||||
}
|
||||
|
||||
fn fetch(&self) -> Result<RemoteCommandOutput> {
|
||||
fn fetch(&self, ask_pass: AskPassSession) -> Result<RemoteCommandOutput> {
|
||||
let working_directory = self.working_directory()?;
|
||||
|
||||
// We do this on every operation to ensure that the askpass script exists and is executable.
|
||||
#[cfg(not(windows))]
|
||||
let (askpass_script_path, _temp_dir) = setup_askpass()?;
|
||||
|
||||
let mut command = new_std_command("git");
|
||||
let mut command = new_smol_command("git");
|
||||
command
|
||||
.env("GIT_ASKPASS", ask_pass.script_path())
|
||||
.env("SSH_ASKPASS", ask_pass.script_path())
|
||||
.env("SSH_ASKPASS_REQUIRE", "force")
|
||||
.current_dir(&working_directory)
|
||||
.args(["fetch", "--all"]);
|
||||
let git_process = command.spawn()?;
|
||||
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
command.env("GIT_ASKPASS", askpass_script_path);
|
||||
}
|
||||
|
||||
let output = command.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(anyhow!(
|
||||
"Failed to fetch:\n{}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
));
|
||||
} else {
|
||||
return Ok(RemoteCommandOutput {
|
||||
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
|
||||
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
|
||||
});
|
||||
}
|
||||
run_remote_command(ask_pass, git_process)
|
||||
}
|
||||
|
||||
fn get_remotes(&self, branch_name: Option<&str>) -> Result<Vec<Remote>> {
|
||||
|
@ -835,16 +796,38 @@ impl GitRepository for RealGitRepository {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn setup_askpass() -> Result<(PathBuf, tempfile::TempDir), anyhow::Error> {
|
||||
let temp_dir = tempfile::Builder::new()
|
||||
.prefix("zed-git-askpass")
|
||||
.tempdir()?;
|
||||
let askpass_script = "#!/bin/sh\necho ''";
|
||||
let askpass_script_path = temp_dir.path().join("git-askpass.sh");
|
||||
std::fs::write(&askpass_script_path, askpass_script)?;
|
||||
std::fs::set_permissions(&askpass_script_path, std::fs::Permissions::from_mode(0o755))?;
|
||||
Ok((askpass_script_path, temp_dir))
|
||||
fn run_remote_command(
|
||||
mut ask_pass: AskPassSession,
|
||||
git_process: smol::process::Child,
|
||||
) -> std::result::Result<RemoteCommandOutput, anyhow::Error> {
|
||||
smol::block_on(async {
|
||||
select_biased! {
|
||||
result = ask_pass.run().fuse() => {
|
||||
match result {
|
||||
AskPassResult::CancelledByUser => {
|
||||
Err(anyhow!(REMOTE_CANCELLED_BY_USER))?
|
||||
}
|
||||
AskPassResult::Timedout => {
|
||||
Err(anyhow!("Connecting to host timed out"))?
|
||||
}
|
||||
}
|
||||
}
|
||||
output = git_process.output().fuse() => {
|
||||
let output = output?;
|
||||
if !output.status.success() {
|
||||
Err(anyhow!(
|
||||
"Operation failed:\n{}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
))
|
||||
} else {
|
||||
Ok(RemoteCommandOutput {
|
||||
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
|
||||
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
@ -1040,15 +1023,21 @@ impl GitRepository for FakeGitRepository {
|
|||
_branch: &str,
|
||||
_remote: &str,
|
||||
_options: Option<PushOptions>,
|
||||
_ask_pass: AskPassSession,
|
||||
) -> Result<RemoteCommandOutput> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn pull(&self, _branch: &str, _remote: &str) -> Result<RemoteCommandOutput> {
|
||||
fn pull(
|
||||
&self,
|
||||
_branch: &str,
|
||||
_remote: &str,
|
||||
_ask_pass: AskPassSession,
|
||||
) -> Result<RemoteCommandOutput> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn fetch(&self) -> Result<RemoteCommandOutput> {
|
||||
fn fetch(&self, _ask_pass: AskPassSession) -> Result<RemoteCommandOutput> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue