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:

<img width="234" height="215" alt="image"
src="https://github.com/user-attachments/assets/713d068f-5ea5-444f-8d66-444ca65affc8"
/>

---

Release Notes:

- Git: Added `git: pull rebase` for running `git pull --rebase`.
This commit is contained in:
Sathiyaraman M
2025-11-04 16:41:06 +00:00
committed by GitHub
parent 9f580464f0
commit 982f2418f4
11 changed files with 79 additions and 46 deletions
+1
View File
@@ -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",
+1
View File
@@ -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",
+1
View File
@@ -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",
+1
View File
@@ -543,6 +543,7 @@ impl GitRepository for FakeGitRepository {
&self,
_branch: String,
_remote: String,
_rebase: bool,
_askpass: AskPassDelegate,
_env: Arc<HashMap<String, String>>,
_cx: AsyncApp,
+2
View File
@@ -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.
+9 -1
View File
@@ -533,6 +533,7 @@ pub trait GitRepository: Send + Sync {
&self,
branch_name: String,
upstream_name: String,
rebase: bool,
askpass: AskPassDelegate,
env: Arc<HashMap<String, String>>,
// 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<HashMap<String, String>>,
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())
+2 -1
View File
@@ -2219,7 +2219,7 @@ impl GitPanel {
.detach();
}
pub(crate) fn pull(&mut self, window: &mut Window, cx: &mut Context<Self>) {
pub(crate) fn pull(&mut self, rebase: bool, window: &mut Window, cx: &mut Context<Self>) {
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,
)
+10 -1
View File
@@ -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::<git_panel::GitPanel>(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())
+50 -43
View File
@@ -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<Result<RemoteCommandOutput>> {
@@ -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(
+1
View File
@@ -405,6 +405,7 @@ message Pull {
string remote_name = 4;
string branch_name = 5;
uint64 askpass_id = 6;
bool rebase = 7;
}
message RemoteMessageResponse {
+1
View File
@@ -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} |