From 03c02c02fdf0cbec46e46b8812691c77d4c3912c Mon Sep 17 00:00:00 2001 From: Guillaume Launay Date: Sat, 26 Jul 2025 20:37:55 +0200 Subject: [PATCH] Add branch rename action to Git panel Also add it to the menu next to branch name --- crates/git/src/git.rs | 2 + crates/git/src/repository.rs | 16 ++++ crates/git_ui/src/git_ui.rs | 135 ++++++++++++++++++++++++++++++++ crates/project/src/git_store.rs | 18 +++++ crates/proto/proto/git.proto | 7 ++ crates/proto/proto/zed.proto | 5 +- 6 files changed, 182 insertions(+), 1 deletion(-) diff --git a/crates/git/src/git.rs b/crates/git/src/git.rs index e84014129cf5a423279b84bed897a4fac2528e02..f849384757d1a8378f2153dcaf83128687f24b3c 100644 --- a/crates/git/src/git.rs +++ b/crates/git/src/git.rs @@ -95,6 +95,8 @@ actions!( OpenModifiedFiles, /// Clones a repository. Clone, + /// Renames the current branch. + RenameBranch, ] ); diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index 49eee848404a0bc866c55fed404365da26538d8b..03de7cc28da82302f01bc4c991e3f87c9617ef0c 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -344,6 +344,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, new_name: String) -> BoxFuture<'_, Result<()>>; fn reset( &self, @@ -1094,6 +1095,21 @@ impl GitRepository for RealGitRepository { .boxed() } + fn rename_branch(&self, 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", &new_name]) + .await?; + Ok(()) + }) + .boxed() + } + fn blame(&self, path: RepoPath, content: Rope) -> BoxFuture<'_, Result> { let working_directory = self.working_directory(); let git_binary_path = self.git_binary_path.clone(); diff --git a/crates/git_ui/src/git_ui.rs b/crates/git_ui/src/git_ui.rs index 79aa4a6bd0f828ab28ea89dcd26e5ab9b7ef8c2d..e0334741b7845714a66dc3f47f2de00e12ced6f2 100644 --- a/crates/git_ui/src/git_ui.rs +++ b/crates/git_ui/src/git_ui.rs @@ -4,6 +4,13 @@ use ::settings::Settings; use command_palette_hooks::CommandPaletteFilter; use commit_modal::CommitModal; use editor::{Editor, EditorElement, EditorStyle, actions::DiffClipboardWithSelectionData}; +use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, SharedString}; +use ui::{ + Headline, HeadlineSize, Icon, IconName, IconSize, IntoElement, ParentElement, Render, Styled, + StyledExt, div, h_flex, rems, v_flex, +}; +use workspace::{ModalView, notifications::DetachAndPromptErr}; + mod blame_ui; use git::{ repository::{Branch, Upstream, UpstreamTracking, UpstreamTrackingStatus}, @@ -14,7 +21,9 @@ use gpui::{ Action, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, TextStyle, Window, actions, }; +use menu::{Cancel, Confirm}; use onboarding::GitOnboardingModal; +use project::git_store::Repository; use project_diff::ProjectDiff; use theme::ThemeSettings; use ui::prelude::*; @@ -185,6 +194,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) { @@ -228,6 +240,127 @@ pub fn git_status_icon(status: FileStatus) -> impl IntoElement { GitStatusIcon::new(status) } +struct RenameBranchModal { + current_branch: SharedString, + editor: Entity, + repo: Entity, +} + +impl RenameBranchModal { + fn new( + current_branch: String, + repo: Entity, + window: &mut Window, + cx: &mut Context, + ) -> 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) { + cx.emit(DismissEvent); + } + + fn confirm(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context) { + 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(); + cx.spawn(async move |_, cx| { + repo.update(cx, |repo, _| repo.rename_branch(new_name))? + .await??; + + Ok(()) + }) + .detach_and_prompt_err("Failed to rename branch", window, cx, |_, _, _| None); + cx.emit(DismissEvent); + } +} + +impl EventEmitter 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) -> 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("Rename Branch").size(HeadlineSize::XSmall)), + ) + .child( + div() + .px_3() + .pb_3() + .w_full() + .child( + div() + .mb_2() + .text_sm() + .text_color(cx.theme().colors().text_muted) + .child(format!("Current: {}", self.current_branch)), + ) + .child(self.editor.clone()), + ) + } +} + +fn rename_current_branch( + workspace: &mut Workspace, + window: &mut Window, + cx: &mut Context, +) { + let Some(panel) = workspace.panel::(cx) else { + return; + }; + let current_branch = panel.update(cx, |panel, cx| { + let Some(repo) = panel.active_repository.as_ref() else { + return None; + }; + 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, branch: &Branch, @@ -467,6 +600,8 @@ mod remote_button { .action("Push", git::Push.boxed_clone()) .action("Push To", git::PushTo.boxed_clone()) .action("Force Push", git::ForcePush.boxed_clone()) + .separator() + .action("Rename Branch", git::RenameBranch.boxed_clone()) })) }) .anchor(Corner::TopRight) diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index 32deb0dbc4f50b9c436e7b051f4c6332be348b1b..d294015499a9a3d6e783b081816d94ff6208d8b7 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -4199,6 +4199,24 @@ impl Repository { ) } + pub fn rename_branch(&mut self, new_name: String) -> oneshot::Receiver> { + let _id = self.id; + self.send_job( + Some(format!("git branch -m {new_name}").into()), + move |repo, _cx| async move { + match repo { + RepositoryState::Local { backend, .. } => backend.rename_branch(new_name).await, + RepositoryState::Remote { .. } => { + // Remote branch renaming not implemented yet + Err(anyhow::anyhow!( + "Branch renaming is not supported for remote repositories yet" + )) + } + } + }, + ) + } + pub fn check_for_pushed_commits(&mut self) -> oneshot::Receiver>> { let id = self.id; self.send_job(None, move |repo, _cx| async move { diff --git a/crates/proto/proto/git.proto b/crates/proto/proto/git.proto index f2c388a3a3db223e4a7dfabcd2b868c47dbbadb1..da6ee61c88d8ea23ce400ccef6ef81242a40e9ae 100644 --- a/crates/proto/proto/git.proto +++ b/crates/proto/proto/git.proto @@ -180,6 +180,13 @@ message GitChangeBranch { string branch_name = 4; } +message GitRenameBranch { + uint64 project_id = 1; + reserved 2; + uint64 repository_id = 3; + string new_name = 4; +} + message GitDiff { uint64 project_id = 1; reserved 2; diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index 856a793c2ffdb09366947e24c526f40ebb206407..223b31f73f7416de0c23305ac0e6221f675ab6c0 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -402,7 +402,10 @@ message Envelope { GetCrashFilesResponse get_crash_files_response = 362; GitClone git_clone = 363; - GitCloneResponse git_clone_response = 364; // current max + GitCloneResponse git_clone_response = 364; + + GitRenameBranch git_rename_branch = 365; // current max + } reserved 87 to 88;