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:
CharlesChen0823 2025-06-06 23:28:07 +08:00 committed by GitHub
parent a40ee74a1f
commit edd40566b7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 155 additions and 42 deletions

View file

@ -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<HashMap<String, String>>,
_cx: AsyncApp,

View file

@ -49,6 +49,7 @@ actions!(
ForcePush,
Pull,
Fetch,
FetchFrom,
Commit,
Amend,
Cancel,

View file

@ -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<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
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<HashMap<String, String>>,
// 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<HashMap<String, String>>,
cx: AsyncApp,
) -> BoxFuture<Result<RemoteCommandOutput>> {
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());

View file

@ -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<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) {
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<Remote> = repo
let current_remotes: Vec<Remote> = 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(),
}))
}
}

View file

@ -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::<git_panel::GitPanel>(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())

View file

@ -28,6 +28,8 @@ pub fn prompt(
) -> Task<Option<usize>> {
if options.is_empty() {
return Task::ready(None);
} else if options.len() == 1 {
return Task::ready(Some(0));
}
let prompt = prompt.to_string().into();

View file

@ -6,7 +6,7 @@ use util::ResultExt as _;
#[derive(Clone)]
pub enum RemoteAction {
Fetch,
Fetch(Option<Remote>),
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 },
}
}

View file

@ -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<proto::RemoteMessageResponse> {
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<Result<RemoteCommandOutput>> {
@ -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")?;

View file

@ -326,6 +326,7 @@ message Fetch {
reserved 2;
uint64 repository_id = 3;
uint64 askpass_id = 4;
optional string remote = 5;
}
message GetRemotes {