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:
---
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} |