Detailed changes
@@ -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::{
@@ -354,6 +354,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
@@ -11,6 +11,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};
@@ -101,6 +102,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"])]
@@ -346,6 +346,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,
@@ -1095,11 +1096,11 @@ impl GitRepository for RealGitRepository {
let (_, branch_name) = name.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)?;
+ let mut branch = repo.branch(&branch_name, &branch_commit, false)?;
branch.set_upstream(Some(&name))?;
branch
} else {
- anyhow::bail!("Branch not found");
+ anyhow::bail!("Branch '{}' not found", name);
};
Ok(branch
@@ -1115,7 +1116,6 @@ impl GitRepository for RealGitRepository {
GitBinary::new(git_binary_path, working_directory?, executor)
.run(&["checkout", &branch])
.await?;
-
anyhow::Ok(())
})
.boxed()
@@ -1133,6 +1133,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();
@@ -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};
@@ -202,6 +210,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) {
@@ -245,6 +256,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,
@@ -398,6 +398,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);
@@ -1944,6 +1945,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>,
@@ -4331,6 +4351,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 {
@@ -183,6 +183,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;
@@ -414,7 +414,9 @@ message Envelope {
NewExternalAgentVersionAvailable new_external_agent_version_available = 377;
StashDrop stash_drop = 378;
- StashApply stash_apply = 379; // current max
+ StashApply stash_apply = 379;
+
+ GitRenameBranch git_rename_branch = 380; // current max
}
reserved 87 to 88;
@@ -300,6 +300,7 @@ messages!(
(AskPassResponse, Background),
(GitCreateBranch, Background),
(GitChangeBranch, Background),
+ (GitRenameBranch, Background),
(CheckForPushedCommits, Background),
(CheckForPushedCommitsResponse, Background),
(GitDiff, Background),
@@ -483,6 +484,7 @@ request_messages!(
(AskPassRequest, AskPassResponse),
(GitCreateBranch, Ack),
(GitChangeBranch, Ack),
+ (GitRenameBranch, Ack),
(CheckForPushedCommits, CheckForPushedCommitsResponse),
(GitDiff, GitDiffResponse),
(GitInit, Ack),
@@ -637,6 +639,7 @@ entity_messages!(
Pull,
AskPassRequest,
GitChangeBranch,
+ GitRenameBranch,
GitCreateBranch,
CheckForPushedCommits,
GitDiff,