diff --git a/crates/fs/src/fake_git_repo.rs b/crates/fs/src/fake_git_repo.rs index 41ca9e641f..c6e0afe294 100644 --- a/crates/fs/src/fake_git_repo.rs +++ b/crates/fs/src/fake_git_repo.rs @@ -5,7 +5,7 @@ use futures::future::{self, BoxFuture}; use git::{ blame::Blame, repository::{ - AskPassDelegate, Branch, CommitDetails, CommitOptions, GitRepository, + AskPassDelegate, Branch, CommitDetails, CommitOptions, FetchOptions, GitRepository, GitRepositoryCheckpoint, PushOptions, Remote, RepoPath, ResetMode, }, status::{FileStatus, GitStatus, StatusCode, TrackedStatus, UnmergedStatus}, @@ -405,6 +405,7 @@ impl GitRepository for FakeGitRepository { fn fetch( &self, + _fetch_options: FetchOptions, _askpass: AskPassDelegate, _env: Arc>, _cx: AsyncApp, diff --git a/crates/git/src/git.rs b/crates/git/src/git.rs index c11f9330a9..003d455d87 100644 --- a/crates/git/src/git.rs +++ b/crates/git/src/git.rs @@ -49,6 +49,7 @@ actions!( ForcePush, Pull, Fetch, + FetchFrom, Commit, Amend, Cancel, diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index 72f24b7285..fbde692fdb 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -193,6 +193,44 @@ pub enum ResetMode { Mixed, } +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub enum FetchOptions { + All, + Remote(Remote), +} + +impl FetchOptions { + pub fn to_proto(&self) -> Option { + match self { + FetchOptions::All => None, + FetchOptions::Remote(remote) => Some(remote.clone().name.into()), + } + } + + pub fn from_proto(remote_name: Option) -> 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 pub struct GitExcludeOverride { git_exclude_path: PathBuf, @@ -381,6 +419,7 @@ pub trait GitRepository: Send + Sync { fn fetch( &self, + fetch_options: FetchOptions, askpass: AskPassDelegate, env: Arc>, // This method takes an AsyncApp to ensure it's invoked on the main thread, @@ -1196,18 +1235,20 @@ impl GitRepository for RealGitRepository { fn fetch( &self, + fetch_options: FetchOptions, ask_pass: AskPassDelegate, env: Arc>, cx: AsyncApp, ) -> BoxFuture> { let working_directory = self.working_directory(); + let remote_name = format!("{}", fetch_options); let executor = cx.background_executor().clone(); async move { let mut command = new_smol_command("git"); command .envs(env.iter()) .current_dir(&working_directory?) - .args(["fetch", "--all"]) + .args(["fetch", &remote_name]) .stdout(smol::process::Stdio::piped()) .stderr(smol::process::Stdio::piped()); diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 0d93d8aa2a..4c3255e2da 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -20,8 +20,8 @@ use editor::{ use futures::StreamExt as _; use git::blame::ParsedCommitMessage; use git::repository::{ - Branch, CommitDetails, CommitOptions, CommitSummary, DiffType, PushOptions, Remote, - RemoteCommandOutput, ResetMode, Upstream, UpstreamTracking, UpstreamTrackingStatus, + Branch, CommitDetails, CommitOptions, CommitSummary, DiffType, FetchOptions, PushOptions, + Remote, RemoteCommandOutput, ResetMode, Upstream, UpstreamTracking, UpstreamTrackingStatus, }; use git::status::StageStatus; 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) { + fn get_fetch_options( + &self, + window: &mut Window, + cx: &mut Context, + ) -> Task> { + 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, + ) { if !self.can_push_and_pull(cx) { return; } @@ -1851,13 +1893,28 @@ impl GitPanel { telemetry::event!("Git Fetched"); let askpass = self.askpass_delegate("git fetch", window, cx); 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 .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?; 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 { Ok(remote_message) => this.show_remote_output(action, remote_message, cx), Err(e) => { @@ -2123,38 +2180,32 @@ impl GitPanel { async move { let repo = repo.context("No active repository")?; - let mut current_remotes: Vec = repo + let current_remotes: Vec = repo .update(&mut cx, |repo, _| { let current_branch = repo.branch.as_ref().context("No active branch")?; anyhow::Ok(repo.get_remotes(Some(current_branch.name().to_string()))) })?? .await??; - if current_remotes.len() == 0 { - anyhow::bail!("No active remote"); - } else if current_remotes.len() == 1 { - return Ok(Some(current_remotes.pop().unwrap())); - } else { - let current_remotes: Vec<_> = current_remotes - .into_iter() - .map(|remotes| remotes.name) - .collect(); - let selection = cx - .update(|window, cx| { - picker_prompt::prompt( - "Pick which remote to push to", - current_remotes.clone(), - workspace, - window, - cx, - ) - })? - .await; + let current_remotes: Vec<_> = current_remotes + .into_iter() + .map(|remotes| remotes.name) + .collect(); + let selection = cx + .update(|window, cx| { + picker_prompt::prompt( + "Pick which remote to push to", + current_remotes.clone(), + workspace, + window, + cx, + ) + })? + .await; - Ok(selection.map(|selection| Remote { - name: current_remotes[selection].clone(), - })) - } + Ok(selection.map(|selection| Remote { + name: current_remotes[selection].clone(), + })) } } diff --git a/crates/git_ui/src/git_ui.rs b/crates/git_ui/src/git_ui.rs index 0790a07de3..6da3fe8bb5 100644 --- a/crates/git_ui/src/git_ui.rs +++ b/crates/git_ui/src/git_ui.rs @@ -59,7 +59,15 @@ pub fn init(cx: &mut App) { return; }; 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::(cx) else { + return; + }; + panel.update(cx, |panel, cx| { + panel.fetch(false, window, cx); }); }); workspace.register_action(|workspace, _: &git::Push, window, cx| { @@ -367,6 +375,7 @@ mod remote_button { el.context(keybinding_target.clone()) }) .action("Fetch", git::Fetch.boxed_clone()) + .action("Fetch From", git::FetchFrom.boxed_clone()) .action("Pull", git::Pull.boxed_clone()) .separator() .action("Push", git::Push.boxed_clone()) diff --git a/crates/git_ui/src/picker_prompt.rs b/crates/git_ui/src/picker_prompt.rs index 46be756bce..74b2a63c31 100644 --- a/crates/git_ui/src/picker_prompt.rs +++ b/crates/git_ui/src/picker_prompt.rs @@ -28,6 +28,8 @@ pub fn prompt( ) -> Task> { if options.is_empty() { return Task::ready(None); + } else if options.len() == 1 { + return Task::ready(Some(0)); } let prompt = prompt.to_string().into(); diff --git a/crates/git_ui/src/remote_output.rs b/crates/git_ui/src/remote_output.rs index 787fe17be9..657402aa03 100644 --- a/crates/git_ui/src/remote_output.rs +++ b/crates/git_ui/src/remote_output.rs @@ -6,7 +6,7 @@ use util::ResultExt as _; #[derive(Clone)] pub enum RemoteAction { - Fetch, + Fetch(Option), Pull(Remote), Push(SharedString, Remote), } @@ -14,7 +14,7 @@ pub enum RemoteAction { impl RemoteAction { pub fn name(&self) -> &'static str { match self { - RemoteAction::Fetch => "fetch", + RemoteAction::Fetch(_) => "fetch", RemoteAction::Pull(_) => "pull", RemoteAction::Push(_, _) => "push", } @@ -34,15 +34,19 @@ pub struct SuccessMessage { pub fn format_output(action: &RemoteAction, output: RemoteCommandOutput) -> SuccessMessage { match action { - RemoteAction::Fetch => { + RemoteAction::Fetch(remote) => { if output.stderr.is_empty() { SuccessMessage { message: "Already up to date".into(), style: SuccessStyle::Toast, } } else { + let message = match remote { + Some(remote) => format!("Synchronized with {}", remote.name), + None => "Synchronized with remotes".into(), + }; SuccessMessage { - message: "Synchronized with remotes".into(), + message, style: SuccessStyle::ToastWithLog { output }, } } diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index 0be12c30cc..852d809f27 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -23,9 +23,9 @@ use git::{ blame::Blame, parse_git_remote_url, repository::{ - Branch, CommitDetails, CommitDiff, CommitFile, CommitOptions, DiffType, GitRepository, - GitRepositoryCheckpoint, PushOptions, Remote, RemoteCommandOutput, RepoPath, ResetMode, - UpstreamTrackingStatus, + Branch, CommitDetails, CommitDiff, CommitFile, CommitOptions, DiffType, FetchOptions, + GitRepository, GitRepositoryCheckpoint, PushOptions, Remote, RemoteCommandOutput, RepoPath, + ResetMode, UpstreamTrackingStatus, }, status::{ FileStatus, GitSummary, StatusCode, TrackedStatus, UnmergedStatus, UnmergedStatusCode, @@ -1553,6 +1553,7 @@ impl GitStore { ) -> Result { let repository_id = RepositoryId::from_proto(envelope.payload.repository_id); 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 = make_remote_delegate( @@ -1565,7 +1566,7 @@ impl GitStore { let remote_output = repository_handle .update(&mut cx, |repository_handle, cx| { - repository_handle.fetch(askpass, cx) + repository_handle.fetch(fetch_options, askpass, cx) })? .await??; @@ -3500,6 +3501,7 @@ impl Repository { pub fn fetch( &mut self, + fetch_options: FetchOptions, askpass: AskPassDelegate, _cx: &mut App, ) -> oneshot::Receiver> { @@ -3513,7 +3515,7 @@ impl Repository { backend, environment, .. - } => backend.fetch(askpass, environment, cx).await, + } => backend.fetch(fetch_options, askpass, environment, cx).await, RepositoryState::Remote { project_id, client } => { askpass_delegates.lock().insert(askpass_id, askpass); let _defer = util::defer(|| { @@ -3526,6 +3528,7 @@ impl Repository { project_id: project_id.0, repository_id: id.to_proto(), askpass_id, + remote: fetch_options.to_proto(), }) .await .context("sending fetch request")?; diff --git a/crates/proto/proto/git.proto b/crates/proto/proto/git.proto index 9f6ebf4ba7..1fdef2eea6 100644 --- a/crates/proto/proto/git.proto +++ b/crates/proto/proto/git.proto @@ -326,6 +326,7 @@ message Fetch { reserved 2; uint64 repository_id = 3; uint64 askpass_id = 4; + optional string remote = 5; } message GetRemotes {