git: Pick which remote to fetch (#26897)
I don't want to fetch `--all` branch, we should can picker which remote to fetch. Release Notes: - Added the `git::FetchFrom` action to fetch from a single remote. --------- Co-authored-by: Cole Miller <cole@zed.dev>
This commit is contained in:
parent
a40ee74a1f
commit
edd40566b7
9 changed files with 155 additions and 42 deletions
|
@ -5,7 +5,7 @@ use futures::future::{self, BoxFuture};
|
||||||
use git::{
|
use git::{
|
||||||
blame::Blame,
|
blame::Blame,
|
||||||
repository::{
|
repository::{
|
||||||
AskPassDelegate, Branch, CommitDetails, CommitOptions, GitRepository,
|
AskPassDelegate, Branch, CommitDetails, CommitOptions, FetchOptions, GitRepository,
|
||||||
GitRepositoryCheckpoint, PushOptions, Remote, RepoPath, ResetMode,
|
GitRepositoryCheckpoint, PushOptions, Remote, RepoPath, ResetMode,
|
||||||
},
|
},
|
||||||
status::{FileStatus, GitStatus, StatusCode, TrackedStatus, UnmergedStatus},
|
status::{FileStatus, GitStatus, StatusCode, TrackedStatus, UnmergedStatus},
|
||||||
|
@ -405,6 +405,7 @@ impl GitRepository for FakeGitRepository {
|
||||||
|
|
||||||
fn fetch(
|
fn fetch(
|
||||||
&self,
|
&self,
|
||||||
|
_fetch_options: FetchOptions,
|
||||||
_askpass: AskPassDelegate,
|
_askpass: AskPassDelegate,
|
||||||
_env: Arc<HashMap<String, String>>,
|
_env: Arc<HashMap<String, String>>,
|
||||||
_cx: AsyncApp,
|
_cx: AsyncApp,
|
||||||
|
|
|
@ -49,6 +49,7 @@ actions!(
|
||||||
ForcePush,
|
ForcePush,
|
||||||
Pull,
|
Pull,
|
||||||
Fetch,
|
Fetch,
|
||||||
|
FetchFrom,
|
||||||
Commit,
|
Commit,
|
||||||
Amend,
|
Amend,
|
||||||
Cancel,
|
Cancel,
|
||||||
|
|
|
@ -193,6 +193,44 @@ pub enum ResetMode {
|
||||||
Mixed,
|
Mixed,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
|
||||||
|
pub enum FetchOptions {
|
||||||
|
All,
|
||||||
|
Remote(Remote),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FetchOptions {
|
||||||
|
pub fn to_proto(&self) -> Option<String> {
|
||||||
|
match self {
|
||||||
|
FetchOptions::All => None,
|
||||||
|
FetchOptions::Remote(remote) => Some(remote.clone().name.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_proto(remote_name: Option<String>) -> Self {
|
||||||
|
match remote_name {
|
||||||
|
Some(name) => FetchOptions::Remote(Remote { name: name.into() }),
|
||||||
|
None => FetchOptions::All,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn name(&self) -> SharedString {
|
||||||
|
match self {
|
||||||
|
Self::All => "Fetch all remotes".into(),
|
||||||
|
Self::Remote(remote) => remote.name.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for FetchOptions {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
FetchOptions::All => write!(f, "--all"),
|
||||||
|
FetchOptions::Remote(remote) => write!(f, "{}", remote.name),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Modifies .git/info/exclude temporarily
|
/// Modifies .git/info/exclude temporarily
|
||||||
pub struct GitExcludeOverride {
|
pub struct GitExcludeOverride {
|
||||||
git_exclude_path: PathBuf,
|
git_exclude_path: PathBuf,
|
||||||
|
@ -381,6 +419,7 @@ pub trait GitRepository: Send + Sync {
|
||||||
|
|
||||||
fn fetch(
|
fn fetch(
|
||||||
&self,
|
&self,
|
||||||
|
fetch_options: FetchOptions,
|
||||||
askpass: AskPassDelegate,
|
askpass: AskPassDelegate,
|
||||||
env: Arc<HashMap<String, String>>,
|
env: Arc<HashMap<String, String>>,
|
||||||
// This method takes an AsyncApp to ensure it's invoked on the main thread,
|
// This method takes an AsyncApp to ensure it's invoked on the main thread,
|
||||||
|
@ -1196,18 +1235,20 @@ impl GitRepository for RealGitRepository {
|
||||||
|
|
||||||
fn fetch(
|
fn fetch(
|
||||||
&self,
|
&self,
|
||||||
|
fetch_options: FetchOptions,
|
||||||
ask_pass: AskPassDelegate,
|
ask_pass: AskPassDelegate,
|
||||||
env: Arc<HashMap<String, String>>,
|
env: Arc<HashMap<String, String>>,
|
||||||
cx: AsyncApp,
|
cx: AsyncApp,
|
||||||
) -> BoxFuture<Result<RemoteCommandOutput>> {
|
) -> BoxFuture<Result<RemoteCommandOutput>> {
|
||||||
let working_directory = self.working_directory();
|
let working_directory = self.working_directory();
|
||||||
|
let remote_name = format!("{}", fetch_options);
|
||||||
let executor = cx.background_executor().clone();
|
let executor = cx.background_executor().clone();
|
||||||
async move {
|
async move {
|
||||||
let mut command = new_smol_command("git");
|
let mut command = new_smol_command("git");
|
||||||
command
|
command
|
||||||
.envs(env.iter())
|
.envs(env.iter())
|
||||||
.current_dir(&working_directory?)
|
.current_dir(&working_directory?)
|
||||||
.args(["fetch", "--all"])
|
.args(["fetch", &remote_name])
|
||||||
.stdout(smol::process::Stdio::piped())
|
.stdout(smol::process::Stdio::piped())
|
||||||
.stderr(smol::process::Stdio::piped());
|
.stderr(smol::process::Stdio::piped());
|
||||||
|
|
||||||
|
|
|
@ -20,8 +20,8 @@ use editor::{
|
||||||
use futures::StreamExt as _;
|
use futures::StreamExt as _;
|
||||||
use git::blame::ParsedCommitMessage;
|
use git::blame::ParsedCommitMessage;
|
||||||
use git::repository::{
|
use git::repository::{
|
||||||
Branch, CommitDetails, CommitOptions, CommitSummary, DiffType, PushOptions, Remote,
|
Branch, CommitDetails, CommitOptions, CommitSummary, DiffType, FetchOptions, PushOptions,
|
||||||
RemoteCommandOutput, ResetMode, Upstream, UpstreamTracking, UpstreamTrackingStatus,
|
Remote, RemoteCommandOutput, ResetMode, Upstream, UpstreamTracking, UpstreamTrackingStatus,
|
||||||
};
|
};
|
||||||
use git::status::StageStatus;
|
use git::status::StageStatus;
|
||||||
use git::{Amend, ToggleStaged, repository::RepoPath, status::FileStatus};
|
use git::{Amend, ToggleStaged, repository::RepoPath, status::FileStatus};
|
||||||
|
@ -1840,7 +1840,49 @@ impl GitPanel {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn fetch(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn get_fetch_options(
|
||||||
|
&self,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Task<Option<FetchOptions>> {
|
||||||
|
let repo = self.active_repository.clone();
|
||||||
|
let workspace = self.workspace.clone();
|
||||||
|
|
||||||
|
cx.spawn_in(window, async move |_, cx| {
|
||||||
|
let repo = repo?;
|
||||||
|
let remotes = repo
|
||||||
|
.update(cx, |repo, _| repo.get_remotes(None))
|
||||||
|
.ok()?
|
||||||
|
.await
|
||||||
|
.ok()?
|
||||||
|
.log_err()?;
|
||||||
|
|
||||||
|
let mut remotes: Vec<_> = remotes.into_iter().map(FetchOptions::Remote).collect();
|
||||||
|
if remotes.len() > 1 {
|
||||||
|
remotes.push(FetchOptions::All);
|
||||||
|
}
|
||||||
|
let selection = cx
|
||||||
|
.update(|window, cx| {
|
||||||
|
picker_prompt::prompt(
|
||||||
|
"Pick which remote to fetch",
|
||||||
|
remotes.iter().map(|r| r.name()).collect(),
|
||||||
|
workspace,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.ok()?
|
||||||
|
.await?;
|
||||||
|
remotes.get(selection).cloned()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn fetch(
|
||||||
|
&mut self,
|
||||||
|
is_fetch_all: bool,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
if !self.can_push_and_pull(cx) {
|
if !self.can_push_and_pull(cx) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -1851,13 +1893,28 @@ impl GitPanel {
|
||||||
telemetry::event!("Git Fetched");
|
telemetry::event!("Git Fetched");
|
||||||
let askpass = self.askpass_delegate("git fetch", window, cx);
|
let askpass = self.askpass_delegate("git fetch", window, cx);
|
||||||
let this = cx.weak_entity();
|
let this = cx.weak_entity();
|
||||||
|
|
||||||
|
let fetch_options = if is_fetch_all {
|
||||||
|
Task::ready(Some(FetchOptions::All))
|
||||||
|
} else {
|
||||||
|
self.get_fetch_options(window, cx)
|
||||||
|
};
|
||||||
|
|
||||||
window
|
window
|
||||||
.spawn(cx, async move |cx| {
|
.spawn(cx, async move |cx| {
|
||||||
let fetch = repo.update(cx, |repo, cx| repo.fetch(askpass, cx))?;
|
let Some(fetch_options) = fetch_options.await else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
let fetch = repo.update(cx, |repo, cx| {
|
||||||
|
repo.fetch(fetch_options.clone(), askpass, cx)
|
||||||
|
})?;
|
||||||
|
|
||||||
let remote_message = fetch.await?;
|
let remote_message = fetch.await?;
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
let action = RemoteAction::Fetch;
|
let action = match fetch_options {
|
||||||
|
FetchOptions::All => RemoteAction::Fetch(None),
|
||||||
|
FetchOptions::Remote(remote) => RemoteAction::Fetch(Some(remote)),
|
||||||
|
};
|
||||||
match remote_message {
|
match remote_message {
|
||||||
Ok(remote_message) => this.show_remote_output(action, remote_message, cx),
|
Ok(remote_message) => this.show_remote_output(action, remote_message, cx),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
@ -2123,38 +2180,32 @@ impl GitPanel {
|
||||||
|
|
||||||
async move {
|
async move {
|
||||||
let repo = repo.context("No active repository")?;
|
let repo = repo.context("No active repository")?;
|
||||||
let mut current_remotes: Vec<Remote> = repo
|
let current_remotes: Vec<Remote> = repo
|
||||||
.update(&mut cx, |repo, _| {
|
.update(&mut cx, |repo, _| {
|
||||||
let current_branch = repo.branch.as_ref().context("No active branch")?;
|
let current_branch = repo.branch.as_ref().context("No active branch")?;
|
||||||
anyhow::Ok(repo.get_remotes(Some(current_branch.name().to_string())))
|
anyhow::Ok(repo.get_remotes(Some(current_branch.name().to_string())))
|
||||||
})??
|
})??
|
||||||
.await??;
|
.await??;
|
||||||
|
|
||||||
if current_remotes.len() == 0 {
|
let current_remotes: Vec<_> = current_remotes
|
||||||
anyhow::bail!("No active remote");
|
.into_iter()
|
||||||
} else if current_remotes.len() == 1 {
|
.map(|remotes| remotes.name)
|
||||||
return Ok(Some(current_remotes.pop().unwrap()));
|
.collect();
|
||||||
} else {
|
let selection = cx
|
||||||
let current_remotes: Vec<_> = current_remotes
|
.update(|window, cx| {
|
||||||
.into_iter()
|
picker_prompt::prompt(
|
||||||
.map(|remotes| remotes.name)
|
"Pick which remote to push to",
|
||||||
.collect();
|
current_remotes.clone(),
|
||||||
let selection = cx
|
workspace,
|
||||||
.update(|window, cx| {
|
window,
|
||||||
picker_prompt::prompt(
|
cx,
|
||||||
"Pick which remote to push to",
|
)
|
||||||
current_remotes.clone(),
|
})?
|
||||||
workspace,
|
.await;
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})?
|
|
||||||
.await;
|
|
||||||
|
|
||||||
Ok(selection.map(|selection| Remote {
|
Ok(selection.map(|selection| Remote {
|
||||||
name: current_remotes[selection].clone(),
|
name: current_remotes[selection].clone(),
|
||||||
}))
|
}))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -59,7 +59,15 @@ pub fn init(cx: &mut App) {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
panel.update(cx, |panel, cx| {
|
panel.update(cx, |panel, cx| {
|
||||||
panel.fetch(window, cx);
|
panel.fetch(true, window, cx);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
workspace.register_action(|workspace, _: &git::FetchFrom, window, cx| {
|
||||||
|
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
panel.update(cx, |panel, cx| {
|
||||||
|
panel.fetch(false, window, cx);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
workspace.register_action(|workspace, _: &git::Push, window, cx| {
|
workspace.register_action(|workspace, _: &git::Push, window, cx| {
|
||||||
|
@ -367,6 +375,7 @@ mod remote_button {
|
||||||
el.context(keybinding_target.clone())
|
el.context(keybinding_target.clone())
|
||||||
})
|
})
|
||||||
.action("Fetch", git::Fetch.boxed_clone())
|
.action("Fetch", git::Fetch.boxed_clone())
|
||||||
|
.action("Fetch From", git::FetchFrom.boxed_clone())
|
||||||
.action("Pull", git::Pull.boxed_clone())
|
.action("Pull", git::Pull.boxed_clone())
|
||||||
.separator()
|
.separator()
|
||||||
.action("Push", git::Push.boxed_clone())
|
.action("Push", git::Push.boxed_clone())
|
||||||
|
|
|
@ -28,6 +28,8 @@ pub fn prompt(
|
||||||
) -> Task<Option<usize>> {
|
) -> Task<Option<usize>> {
|
||||||
if options.is_empty() {
|
if options.is_empty() {
|
||||||
return Task::ready(None);
|
return Task::ready(None);
|
||||||
|
} else if options.len() == 1 {
|
||||||
|
return Task::ready(Some(0));
|
||||||
}
|
}
|
||||||
let prompt = prompt.to_string().into();
|
let prompt = prompt.to_string().into();
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ use util::ResultExt as _;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub enum RemoteAction {
|
pub enum RemoteAction {
|
||||||
Fetch,
|
Fetch(Option<Remote>),
|
||||||
Pull(Remote),
|
Pull(Remote),
|
||||||
Push(SharedString, Remote),
|
Push(SharedString, Remote),
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ pub enum RemoteAction {
|
||||||
impl RemoteAction {
|
impl RemoteAction {
|
||||||
pub fn name(&self) -> &'static str {
|
pub fn name(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
RemoteAction::Fetch => "fetch",
|
RemoteAction::Fetch(_) => "fetch",
|
||||||
RemoteAction::Pull(_) => "pull",
|
RemoteAction::Pull(_) => "pull",
|
||||||
RemoteAction::Push(_, _) => "push",
|
RemoteAction::Push(_, _) => "push",
|
||||||
}
|
}
|
||||||
|
@ -34,15 +34,19 @@ pub struct SuccessMessage {
|
||||||
|
|
||||||
pub fn format_output(action: &RemoteAction, output: RemoteCommandOutput) -> SuccessMessage {
|
pub fn format_output(action: &RemoteAction, output: RemoteCommandOutput) -> SuccessMessage {
|
||||||
match action {
|
match action {
|
||||||
RemoteAction::Fetch => {
|
RemoteAction::Fetch(remote) => {
|
||||||
if output.stderr.is_empty() {
|
if output.stderr.is_empty() {
|
||||||
SuccessMessage {
|
SuccessMessage {
|
||||||
message: "Already up to date".into(),
|
message: "Already up to date".into(),
|
||||||
style: SuccessStyle::Toast,
|
style: SuccessStyle::Toast,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
let message = match remote {
|
||||||
|
Some(remote) => format!("Synchronized with {}", remote.name),
|
||||||
|
None => "Synchronized with remotes".into(),
|
||||||
|
};
|
||||||
SuccessMessage {
|
SuccessMessage {
|
||||||
message: "Synchronized with remotes".into(),
|
message,
|
||||||
style: SuccessStyle::ToastWithLog { output },
|
style: SuccessStyle::ToastWithLog { output },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,9 +23,9 @@ use git::{
|
||||||
blame::Blame,
|
blame::Blame,
|
||||||
parse_git_remote_url,
|
parse_git_remote_url,
|
||||||
repository::{
|
repository::{
|
||||||
Branch, CommitDetails, CommitDiff, CommitFile, CommitOptions, DiffType, GitRepository,
|
Branch, CommitDetails, CommitDiff, CommitFile, CommitOptions, DiffType, FetchOptions,
|
||||||
GitRepositoryCheckpoint, PushOptions, Remote, RemoteCommandOutput, RepoPath, ResetMode,
|
GitRepository, GitRepositoryCheckpoint, PushOptions, Remote, RemoteCommandOutput, RepoPath,
|
||||||
UpstreamTrackingStatus,
|
ResetMode, UpstreamTrackingStatus,
|
||||||
},
|
},
|
||||||
status::{
|
status::{
|
||||||
FileStatus, GitSummary, StatusCode, TrackedStatus, UnmergedStatus, UnmergedStatusCode,
|
FileStatus, GitSummary, StatusCode, TrackedStatus, UnmergedStatus, UnmergedStatusCode,
|
||||||
|
@ -1553,6 +1553,7 @@ impl GitStore {
|
||||||
) -> Result<proto::RemoteMessageResponse> {
|
) -> Result<proto::RemoteMessageResponse> {
|
||||||
let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
|
let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
|
||||||
let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
|
let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
|
||||||
|
let fetch_options = FetchOptions::from_proto(envelope.payload.remote);
|
||||||
let askpass_id = envelope.payload.askpass_id;
|
let askpass_id = envelope.payload.askpass_id;
|
||||||
|
|
||||||
let askpass = make_remote_delegate(
|
let askpass = make_remote_delegate(
|
||||||
|
@ -1565,7 +1566,7 @@ impl GitStore {
|
||||||
|
|
||||||
let remote_output = repository_handle
|
let remote_output = repository_handle
|
||||||
.update(&mut cx, |repository_handle, cx| {
|
.update(&mut cx, |repository_handle, cx| {
|
||||||
repository_handle.fetch(askpass, cx)
|
repository_handle.fetch(fetch_options, askpass, cx)
|
||||||
})?
|
})?
|
||||||
.await??;
|
.await??;
|
||||||
|
|
||||||
|
@ -3500,6 +3501,7 @@ impl Repository {
|
||||||
|
|
||||||
pub fn fetch(
|
pub fn fetch(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
fetch_options: FetchOptions,
|
||||||
askpass: AskPassDelegate,
|
askpass: AskPassDelegate,
|
||||||
_cx: &mut App,
|
_cx: &mut App,
|
||||||
) -> oneshot::Receiver<Result<RemoteCommandOutput>> {
|
) -> oneshot::Receiver<Result<RemoteCommandOutput>> {
|
||||||
|
@ -3513,7 +3515,7 @@ impl Repository {
|
||||||
backend,
|
backend,
|
||||||
environment,
|
environment,
|
||||||
..
|
..
|
||||||
} => backend.fetch(askpass, environment, cx).await,
|
} => backend.fetch(fetch_options, askpass, environment, cx).await,
|
||||||
RepositoryState::Remote { project_id, client } => {
|
RepositoryState::Remote { project_id, client } => {
|
||||||
askpass_delegates.lock().insert(askpass_id, askpass);
|
askpass_delegates.lock().insert(askpass_id, askpass);
|
||||||
let _defer = util::defer(|| {
|
let _defer = util::defer(|| {
|
||||||
|
@ -3526,6 +3528,7 @@ impl Repository {
|
||||||
project_id: project_id.0,
|
project_id: project_id.0,
|
||||||
repository_id: id.to_proto(),
|
repository_id: id.to_proto(),
|
||||||
askpass_id,
|
askpass_id,
|
||||||
|
remote: fetch_options.to_proto(),
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.context("sending fetch request")?;
|
.context("sending fetch request")?;
|
||||||
|
|
|
@ -326,6 +326,7 @@ message Fetch {
|
||||||
reserved 2;
|
reserved 2;
|
||||||
uint64 repository_id = 3;
|
uint64 repository_id = 3;
|
||||||
uint64 askpass_id = 4;
|
uint64 askpass_id = 4;
|
||||||
|
optional string remote = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
message GetRemotes {
|
message GetRemotes {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue