Compare commits

...

13 Commits

Author SHA1 Message Date
Cole Miller
f5728ead57 remove some unrelated changes 2025-09-16 12:52:32 -04:00
Cole Miller
a564db269e Merge remote-tracking branch 'origin/main' into git-rename-branch 2025-09-11 09:50:09 -04:00
Cole Miller
4b568f4437 remote support 2025-09-11 09:38:15 -04:00
Cole Miller
bf3e738e91 add an argument to RenameBranch
Co-authored-by: Peter Tripp <petertripp@gmail.com>
2025-09-11 09:36:07 -04:00
Cole Miller
a1a8ae3c0a clean up modal
Co-authored-by: Peter Tripp <petertripp@gmail.com>
2025-09-11 09:34:59 -04:00
Cole Miller
52d3cbeb89 remove rename branch from remote operations context menu 2025-09-11 09:34:59 -04:00
Guillaume Launay
0a8a50a34b formatting 2025-08-14 15:15:12 +02:00
Guillaume Launay
ebf0143041 fix import 2025-08-14 15:12:28 +02:00
Guillaume Launay
4c0c6ffe12 linting 2025-08-14 14:06:05 +02:00
Guillaume Launay
db92d6ab5c Create new branch before deleting the current one to prevent delete then panic 2025-08-14 14:06:05 +02:00
Guillaume Launay
cbc8394afb Better error message git 2025-08-14 14:06:05 +02:00
Guillaume Launay
79800c9523 Better error message git
In the error modal print the error that output the git command instead of a generic message
2025-08-14 14:06:05 +02:00
Guillaume Launay
03c02c02fd Add branch rename action to Git panel
Also add it to the menu next to branch name
2025-08-14 14:06:03 +02:00
8 changed files with 245 additions and 12 deletions

View File

@@ -1,5 +1,5 @@
use crate::{FakeFs, FakeFsEntry, Fs};
use anyhow::{Context as _, Result};
use anyhow::{Context as _, Result, bail};
use collections::{HashMap, HashSet};
use futures::future::{self, BoxFuture, join_all};
use git::{
@@ -350,6 +350,19 @@ impl GitRepository for FakeGitRepository {
})
}
fn rename_branch(&self, branch: String, new_name: String) -> BoxFuture<'_, Result<()>> {
self.with_state_async(true, move |state| {
if !state.branches.remove(&branch) {
bail!("no such branch: {branch}");
}
state.branches.insert(new_name.clone());
if state.current_branch_name == Some(branch) {
state.current_branch_name = Some(new_name);
}
Ok(())
})
}
fn blame(&self, path: RepoPath, _content: Rope) -> BoxFuture<'_, Result<git::blame::Blame>> {
self.with_state_async(false, move |state| {
state

View File

@@ -10,6 +10,7 @@ pub use crate::remote::*;
use anyhow::{Context as _, Result};
pub use git2 as libgit;
use gpui::{Action, actions};
pub use repository::RemoteCommandOutput;
pub use repository::WORK_DIRECTORY_REPO_PATH;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -98,6 +99,18 @@ actions!(
]
);
/// Renames a git branch.
#[derive(Clone, Debug, Default, PartialEq, Deserialize, JsonSchema, Action)]
#[action(namespace = git)]
#[serde(deny_unknown_fields)]
pub struct RenameBranch {
/// The branch to rename.
///
/// Default: the current branch.
#[serde(default)]
pub branch: Option<String>,
}
/// Restores a file to its last committed state, discarding local changes.
#[derive(Clone, Debug, Default, PartialEq, Deserialize, JsonSchema, Action)]
#[action(namespace = git, deprecated_aliases = ["editor::RevertFile"])]

View File

@@ -343,6 +343,7 @@ pub trait GitRepository: Send + Sync {
fn change_branch(&self, name: String) -> BoxFuture<'_, Result<()>>;
fn create_branch(&self, name: String) -> BoxFuture<'_, Result<()>>;
fn rename_branch(&self, branch: String, new_name: String) -> BoxFuture<'_, Result<()>>;
fn reset(
&self,
@@ -1048,19 +1049,22 @@ impl GitRepository for RealGitRepository {
let working_directory = self.working_directory();
let git_binary_path = self.git_binary_path.clone();
let executor = self.executor.clone();
let name_clone = name.clone();
let branch = self.executor.spawn(async move {
let repo = repo.lock();
let branch = if let Ok(branch) = repo.find_branch(&name, BranchType::Local) {
let branch = if let Ok(branch) = repo.find_branch(&name_clone, BranchType::Local) {
branch
} else if let Ok(revision) = repo.find_branch(&name, BranchType::Remote) {
let (_, branch_name) = name.split_once("/").context("Unexpected branch format")?;
} else if let Ok(revision) = repo.find_branch(&name_clone, BranchType::Remote) {
let (_, branch_name) = name_clone
.split_once("/")
.context("Unexpected branch format")?;
let revision = revision.get();
let branch_commit = revision.peel_to_commit()?;
let mut branch = repo.branch(branch_name, &branch_commit, false)?;
branch.set_upstream(Some(&name))?;
let mut branch = repo.branch(&branch_name, &branch_commit, false)?;
branch.set_upstream(Some(&name_clone))?;
branch
} else {
anyhow::bail!("Branch not found");
anyhow::bail!("Branch '{}' not found", name_clone);
};
Ok(branch
@@ -1076,7 +1080,6 @@ impl GitRepository for RealGitRepository {
GitBinary::new(git_binary_path, working_directory?, executor)
.run(&["checkout", &branch])
.await?;
anyhow::Ok(())
})
.boxed()
@@ -1094,6 +1097,21 @@ impl GitRepository for RealGitRepository {
.boxed()
}
fn rename_branch(&self, branch: String, new_name: String) -> BoxFuture<'_, Result<()>> {
let git_binary_path = self.git_binary_path.clone();
let working_directory = self.working_directory();
let executor = self.executor.clone();
self.executor
.spawn(async move {
GitBinary::new(git_binary_path, working_directory?, executor)
.run(&["branch", "-m", &branch, &new_name])
.await?;
anyhow::Ok(())
})
.boxed()
}
fn blame(&self, path: RepoPath, content: Rope) -> BoxFuture<'_, Result<crate::blame::Blame>> {
let working_directory = self.working_directory();
let git_binary_path = self.git_binary_path.clone();

View File

@@ -4,20 +4,28 @@ use ::settings::Settings;
use command_palette_hooks::CommandPaletteFilter;
use commit_modal::CommitModal;
use editor::{Editor, actions::DiffClipboardWithSelectionData};
use ui::{
Headline, HeadlineSize, Icon, IconName, IconSize, IntoElement, ParentElement, Render, Styled,
StyledExt, div, h_flex, rems, v_flex,
};
mod blame_ui;
use git::{
repository::{Branch, Upstream, UpstreamTracking, UpstreamTrackingStatus},
status::{FileStatus, StatusCode, UnmergedStatus, UnmergedStatusCode},
};
use git_panel_settings::GitPanelSettings;
use gpui::{
Action, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Window,
actions,
Action, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, SharedString,
Window, actions,
};
use menu::{Cancel, Confirm};
use onboarding::GitOnboardingModal;
use project::git_store::Repository;
use project_diff::ProjectDiff;
use ui::prelude::*;
use workspace::{ModalView, Workspace};
use workspace::{ModalView, Workspace, notifications::DetachAndPromptErr};
use zed_actions;
use crate::{git_panel::GitPanel, text_diff_view::TextDiffView};
@@ -192,6 +200,9 @@ pub fn init(cx: &mut App) {
workspace.register_action(|workspace, _: &git::OpenModifiedFiles, window, cx| {
open_modified_files(workspace, window, cx);
});
workspace.register_action(|workspace, _: &git::RenameBranch, window, cx| {
rename_current_branch(workspace, window, cx);
});
workspace.register_action(
|workspace, action: &DiffClipboardWithSelectionData, window, cx| {
if let Some(task) = TextDiffView::open(action, workspace, window, cx) {
@@ -235,6 +246,122 @@ pub fn git_status_icon(status: FileStatus) -> impl IntoElement {
GitStatusIcon::new(status)
}
struct RenameBranchModal {
current_branch: SharedString,
editor: Entity<Editor>,
repo: Entity<Repository>,
}
impl RenameBranchModal {
fn new(
current_branch: String,
repo: Entity<Repository>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let editor = cx.new(|cx| {
let mut editor = Editor::single_line(window, cx);
editor.set_text(current_branch.clone(), window, cx);
editor
});
Self {
current_branch: current_branch.into(),
editor,
repo,
}
}
fn cancel(&mut self, _: &Cancel, _window: &mut Window, cx: &mut Context<Self>) {
cx.emit(DismissEvent);
}
fn confirm(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
let new_name = self.editor.read(cx).text(cx);
if new_name.is_empty() || new_name == self.current_branch.as_ref() {
cx.emit(DismissEvent);
return;
}
let repo = self.repo.clone();
let current_branch = self.current_branch.to_string();
cx.spawn(async move |_, cx| {
match repo
.update(cx, |repo, _| {
repo.rename_branch(current_branch, new_name.clone())
})?
.await
{
Ok(Ok(_)) => Ok(()),
Ok(Err(error)) => Err(error),
Err(_) => Err(anyhow::anyhow!("Operation was canceled")),
}
})
.detach_and_prompt_err("Failed to rename branch", window, cx, |_, _, _| None);
cx.emit(DismissEvent);
}
}
impl EventEmitter<DismissEvent> for RenameBranchModal {}
impl ModalView for RenameBranchModal {}
impl Focusable for RenameBranchModal {
fn focus_handle(&self, cx: &App) -> FocusHandle {
self.editor.focus_handle(cx)
}
}
impl Render for RenameBranchModal {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
.key_context("RenameBranchModal")
.on_action(cx.listener(Self::cancel))
.on_action(cx.listener(Self::confirm))
.elevation_2(cx)
.w(rems(34.))
.child(
h_flex()
.px_3()
.pt_2()
.pb_1()
.w_full()
.gap_1p5()
.child(Icon::new(IconName::GitBranch).size(IconSize::XSmall))
.child(
Headline::new(format!("Rename Branch ({})", self.current_branch))
.size(HeadlineSize::XSmall),
),
)
.child(div().px_3().pb_3().w_full().child(self.editor.clone()))
}
}
fn rename_current_branch(
workspace: &mut Workspace,
window: &mut Window,
cx: &mut Context<Workspace>,
) {
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
return;
};
let current_branch: Option<String> = panel.update(cx, |panel, cx| {
let repo = panel.active_repository.as_ref()?;
let repo = repo.read(cx);
repo.branch.as_ref().map(|branch| branch.name().to_string())
});
let Some(current_branch_name) = current_branch else {
return;
};
let repo = panel.read(cx).active_repository.clone();
let Some(repo) = repo else {
return;
};
workspace.toggle_modal(window, cx, |window, cx| {
RenameBranchModal::new(current_branch_name, repo, window, cx)
});
}
fn render_remote_button(
id: impl Into<SharedString>,
branch: &Branch,

View File

@@ -396,6 +396,7 @@ impl GitStore {
client.add_entity_request_handler(Self::handle_get_default_branch);
client.add_entity_request_handler(Self::handle_change_branch);
client.add_entity_request_handler(Self::handle_create_branch);
client.add_entity_request_handler(Self::handle_rename_branch);
client.add_entity_request_handler(Self::handle_git_init);
client.add_entity_request_handler(Self::handle_push);
client.add_entity_request_handler(Self::handle_pull);
@@ -1903,6 +1904,25 @@ impl GitStore {
Ok(proto::Ack {})
}
async fn handle_rename_branch(
this: Entity<Self>,
envelope: TypedEnvelope<proto::GitRenameBranch>,
mut cx: AsyncApp,
) -> Result<proto::Ack> {
let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
let branch = envelope.payload.branch;
let new_name = envelope.payload.new_name;
repository_handle
.update(&mut cx, |repository_handle, _| {
repository_handle.rename_branch(branch, new_name)
})?
.await??;
Ok(proto::Ack {})
}
async fn handle_show(
this: Entity<Self>,
envelope: TypedEnvelope<proto::GitShow>,
@@ -4155,6 +4175,36 @@ impl Repository {
)
}
pub fn rename_branch(
&mut self,
branch: String,
new_name: String,
) -> oneshot::Receiver<Result<()>> {
let id = self.id;
self.send_job(
Some(format!("git branch -m {branch} {new_name}").into()),
move |repo, _cx| async move {
match repo {
RepositoryState::Local { backend, .. } => {
backend.rename_branch(branch, new_name).await
}
RepositoryState::Remote { project_id, client } => {
client
.request(proto::GitRenameBranch {
project_id: project_id.0,
repository_id: id.to_proto(),
branch,
new_name,
})
.await?;
Ok(())
}
}
},
)
}
pub fn check_for_pushed_commits(&mut self) -> oneshot::Receiver<Result<Vec<SharedString>>> {
let id = self.id;
self.send_job(None, move |repo, _cx| async move {

View File

@@ -182,6 +182,13 @@ message GitChangeBranch {
string branch_name = 4;
}
message GitRenameBranch {
uint64 project_id = 1;
uint64 repository_id = 2;
string branch = 3;
string new_name = 4;
}
message GitDiff {
uint64 project_id = 1;
reserved 2;

View File

@@ -413,7 +413,9 @@ message Envelope {
ExternalAgentsUpdated external_agents_updated = 375;
ExternalAgentLoadingStatusUpdated external_agent_loading_status_updated = 376;
NewExternalAgentVersionAvailable new_external_agent_version_available = 377; // current max
NewExternalAgentVersionAvailable new_external_agent_version_available = 377;
GitRenameBranch git_rename_branch = 378; // current max
}
reserved 87 to 88;

View File

@@ -301,6 +301,7 @@ messages!(
(AskPassResponse, Background),
(GitCreateBranch, Background),
(GitChangeBranch, Background),
(GitRenameBranch, Background),
(CheckForPushedCommits, Background),
(CheckForPushedCommitsResponse, Background),
(GitDiff, Background),
@@ -484,6 +485,7 @@ request_messages!(
(AskPassRequest, AskPassResponse),
(GitCreateBranch, Ack),
(GitChangeBranch, Ack),
(GitRenameBranch, Ack),
(CheckForPushedCommits, CheckForPushedCommitsResponse),
(GitDiff, GitDiffResponse),
(GitInit, Ack),
@@ -638,6 +640,7 @@ entity_messages!(
Pull,
AskPassRequest,
GitChangeBranch,
GitRenameBranch,
GitCreateBranch,
CheckForPushedCommits,
GitDiff,