From 982f2418f414107c7b08767edc2e3debe5431328 Mon Sep 17 00:00:00 2001 From: Sathiyaraman M <45333880+Sathiyaraman-M@users.noreply.github.com> Date: Tue, 4 Nov 2025 22:11:06 +0530 Subject: [PATCH] git: Add support for git pull with rebase (#41117) - Adds a new action `git::PullRebase` which adds `--rebase` in the final command invoked by existing Git-Pull implementation. - Includes the new action in "Fetch/Push" button in the Git Panel (screenshot below) - Adds key-binding for `git::PullRebase` in all three platforms, following the existing key-binding patterns (`ctrl-g shift-down`) - Update git docs to include the new action. Sidenote: This is my first ever OSS contribution Screenshot: image --- Release Notes: - Git: Added `git: pull rebase` for running `git pull --rebase`. --- assets/keymaps/default-linux.json | 1 + assets/keymaps/default-macos.json | 1 + assets/keymaps/default-windows.json | 1 + crates/fs/src/fake_git_repo.rs | 1 + crates/git/src/git.rs | 2 + crates/git/src/repository.rs | 10 +++- crates/git_ui/src/git_panel.rs | 3 +- crates/git_ui/src/git_ui.rs | 11 +++- crates/project/src/git_store.rs | 93 ++++++++++++++++------------- crates/proto/proto/git.proto | 1 + docs/src/git.md | 1 + 11 files changed, 79 insertions(+), 46 deletions(-) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 1af01c5d23..2bacf4927e 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -945,6 +945,7 @@ "ctrl-g ctrl-g": "git::Fetch", "ctrl-g up": "git::Push", "ctrl-g down": "git::Pull", + "ctrl-g shift-down": "git::PullRebase", "ctrl-g shift-up": "git::ForcePush", "ctrl-g d": "git::Diff", "ctrl-g backspace": "git::RestoreTrackedFiles", diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 8c2f596fbd..97aa404071 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -1038,6 +1038,7 @@ "ctrl-g ctrl-g": "git::Fetch", "ctrl-g up": "git::Push", "ctrl-g down": "git::Pull", + "ctrl-g shift-down": "git::PullRebase", "ctrl-g shift-up": "git::ForcePush", "ctrl-g d": "git::Diff", "ctrl-g backspace": "git::RestoreTrackedFiles", diff --git a/assets/keymaps/default-windows.json b/assets/keymaps/default-windows.json index 59ea8398c5..35281ab824 100644 --- a/assets/keymaps/default-windows.json +++ b/assets/keymaps/default-windows.json @@ -954,6 +954,7 @@ "ctrl-g ctrl-g": "git::Fetch", "ctrl-g up": "git::Push", "ctrl-g down": "git::Pull", + "ctrl-g shift-down": "git::PullRebase", "ctrl-g shift-up": "git::ForcePush", "ctrl-g d": "git::Diff", "ctrl-g backspace": "git::RestoreTrackedFiles", diff --git a/crates/fs/src/fake_git_repo.rs b/crates/fs/src/fake_git_repo.rs index 81dbd28cd9..a6e9bcdac6 100644 --- a/crates/fs/src/fake_git_repo.rs +++ b/crates/fs/src/fake_git_repo.rs @@ -543,6 +543,7 @@ impl GitRepository for FakeGitRepository { &self, _branch: String, _remote: String, + _rebase: bool, _askpass: AskPassDelegate, _env: Arc>, _cx: AsyncApp, diff --git a/crates/git/src/git.rs b/crates/git/src/git.rs index 29fa50ddd2..50a1e1234b 100644 --- a/crates/git/src/git.rs +++ b/crates/git/src/git.rs @@ -72,6 +72,8 @@ actions!( ForcePush, /// Pulls changes from the remote repository. Pull, + /// Pulls changes from the remote repository with rebase. + PullRebase, /// Fetches changes from the remote repository. Fetch, /// Fetches changes from a specific remote. diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index 8db32dec69..6fcbbb919c 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -533,6 +533,7 @@ pub trait GitRepository: Send + Sync { &self, branch_name: String, upstream_name: String, + rebase: bool, askpass: AskPassDelegate, env: Arc>, // This method takes an AsyncApp to ensure it's invoked on the main thread, @@ -1690,6 +1691,7 @@ impl GitRepository for RealGitRepository { &self, branch_name: String, remote_name: String, + rebase: bool, ask_pass: AskPassDelegate, env: Arc>, cx: AsyncApp, @@ -1703,7 +1705,13 @@ impl GitRepository for RealGitRepository { command .envs(env.iter()) .current_dir(&working_directory?) - .args(["pull"]) + .arg("pull"); + + if rebase { + command.arg("--rebase"); + } + + command .arg(remote_name) .arg(branch_name) .stdout(smol::process::Stdio::piped()) diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 0c43058c06..33fd1ba9b6 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -2219,7 +2219,7 @@ impl GitPanel { .detach(); } - pub(crate) fn pull(&mut self, window: &mut Window, cx: &mut Context) { + pub(crate) fn pull(&mut self, rebase: bool, window: &mut Window, cx: &mut Context) { if !self.can_push_and_pull(cx) { return; } @@ -2254,6 +2254,7 @@ impl GitPanel { repo.pull( branch.name().to_owned().into(), remote.name.clone(), + rebase, askpass, cx, ) diff --git a/crates/git_ui/src/git_ui.rs b/crates/git_ui/src/git_ui.rs index ed99f2c2af..a08add7f37 100644 --- a/crates/git_ui/src/git_ui.rs +++ b/crates/git_ui/src/git_ui.rs @@ -126,7 +126,15 @@ pub fn init(cx: &mut App) { return; }; panel.update(cx, |panel, cx| { - panel.pull(window, cx); + panel.pull(false, window, cx); + }); + }); + workspace.register_action(|workspace, _: &git::PullRebase, window, cx| { + let Some(panel) = workspace.panel::(cx) else { + return; + }; + panel.update(cx, |panel, cx| { + panel.pull(true, window, cx); }); }); } @@ -597,6 +605,7 @@ mod remote_button { .action("Fetch", git::Fetch.boxed_clone()) .action("Fetch From", git::FetchFrom.boxed_clone()) .action("Pull", git::Pull.boxed_clone()) + .action("Pull (Rebase)", git::PullRebase.boxed_clone()) .separator() .action("Push", git::Push.boxed_clone()) .action("Push To", git::PushTo.boxed_clone()) diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index d5350125b0..486d51fef8 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -1725,10 +1725,11 @@ impl GitStore { let branch_name = envelope.payload.branch_name.into(); let remote_name = envelope.payload.remote_name.into(); + let rebase = envelope.payload.rebase; let remote_message = repository_handle .update(&mut cx, |repository_handle, cx| { - repository_handle.pull(branch_name, remote_name, askpass, cx) + repository_handle.pull(branch_name, remote_name, rebase, askpass, cx) })? .await??; @@ -4294,6 +4295,7 @@ impl Repository { &mut self, branch: SharedString, remote: SharedString, + rebase: bool, askpass: AskPassDelegate, _cx: &mut App, ) -> oneshot::Receiver> { @@ -4301,50 +4303,55 @@ impl Repository { let askpass_id = util::post_inc(&mut self.latest_askpass_id); let id = self.id; - self.send_job( - Some(format!("git pull {} {}", remote, branch).into()), - move |git_repo, cx| async move { - match git_repo { - RepositoryState::Local { - backend, - environment, - .. - } => { - backend - .pull( - branch.to_string(), - remote.to_string(), - askpass, - environment.clone(), - cx, - ) - .await - } - RepositoryState::Remote { project_id, client } => { - askpass_delegates.lock().insert(askpass_id, askpass); - let _defer = util::defer(|| { - let askpass_delegate = askpass_delegates.lock().remove(&askpass_id); - debug_assert!(askpass_delegate.is_some()); - }); - let response = client - .request(proto::Pull { - project_id: project_id.0, - repository_id: id.to_proto(), - askpass_id, - branch_name: branch.to_string(), - remote_name: remote.to_string(), - }) - .await - .context("sending pull request")?; + let status = if rebase { + Some(format!("git pull --rebase {} {}", remote, branch).into()) + } else { + Some(format!("git pull {} {}", remote, branch).into()) + }; - Ok(RemoteCommandOutput { - stdout: response.stdout, - stderr: response.stderr, - }) - } + self.send_job(status, move |git_repo, cx| async move { + match git_repo { + RepositoryState::Local { + backend, + environment, + .. + } => { + backend + .pull( + branch.to_string(), + remote.to_string(), + rebase, + askpass, + environment.clone(), + cx, + ) + .await } - }, - ) + RepositoryState::Remote { project_id, client } => { + askpass_delegates.lock().insert(askpass_id, askpass); + let _defer = util::defer(|| { + let askpass_delegate = askpass_delegates.lock().remove(&askpass_id); + debug_assert!(askpass_delegate.is_some()); + }); + let response = client + .request(proto::Pull { + project_id: project_id.0, + repository_id: id.to_proto(), + askpass_id, + rebase, + branch_name: branch.to_string(), + remote_name: remote.to_string(), + }) + .await + .context("sending pull request")?; + + Ok(RemoteCommandOutput { + stdout: response.stdout, + stderr: response.stderr, + }) + } + } + }) } fn spawn_set_index_text_job( diff --git a/crates/proto/proto/git.proto b/crates/proto/proto/git.proto index c56005ef29..a25934467a 100644 --- a/crates/proto/proto/git.proto +++ b/crates/proto/proto/git.proto @@ -405,6 +405,7 @@ message Pull { string remote_name = 4; string branch_name = 5; uint64 askpass_id = 6; + bool rebase = 7; } message RemoteMessageResponse { diff --git a/docs/src/git.md b/docs/src/git.md index d56de998c9..85781e37bc 100644 --- a/docs/src/git.md +++ b/docs/src/git.md @@ -179,6 +179,7 @@ When viewing files with changes, Zed displays diff hunks that can be expanded or | {#action git::Push} | {#kb git::Push} | | {#action git::ForcePush} | {#kb git::ForcePush} | | {#action git::Pull} | {#kb git::Pull} | +| {#action git::PullRebase} | {#kb git::PullRebase} | | {#action git::Fetch} | {#kb git::Fetch} | | {#action git::Diff} | {#kb git::Diff} | | {#action git::Restore} | {#kb git::Restore} |