Add support for git branches on remote projects (#19755)

Release Notes:

- Fixed a bug where the branch switcher could not be used remotely.
This commit is contained in:
Mikayla Maki 2024-10-27 15:50:54 -07:00 committed by GitHub
parent 5506669b06
commit c69da2df70
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 993 additions and 127 deletions

View file

@ -14,6 +14,7 @@ fuzzy.workspace = true
git.workspace = true
gpui.workspace = true
picker.workspace = true
project.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true

View file

@ -2,24 +2,23 @@ use anyhow::{Context, Result};
use fuzzy::{StringMatch, StringMatchCandidate};
use git::repository::Branch;
use gpui::{
actions, rems, AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView,
InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
Task, View, ViewContext, VisualContext, WindowContext,
actions, rems, AnyElement, AppContext, AsyncAppContext, DismissEvent, EventEmitter,
FocusHandle, FocusableView, InteractiveElement, IntoElement, ParentElement, Render,
SharedString, Styled, Subscription, Task, View, ViewContext, VisualContext, WindowContext,
};
use picker::{Picker, PickerDelegate};
use project::ProjectPath;
use std::{ops::Not, sync::Arc};
use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
use util::ResultExt;
use workspace::notifications::NotificationId;
use workspace::{ModalView, Toast, Workspace};
use workspace::notifications::DetachAndPromptErr;
use workspace::{ModalView, Workspace};
actions!(branches, [OpenRecent]);
pub fn init(cx: &mut AppContext) {
cx.observe_new_views(|workspace: &mut Workspace, _| {
workspace.register_action(|workspace, action, cx| {
BranchList::open(workspace, action, cx).log_err();
});
workspace.register_action(BranchList::open);
})
.detach();
}
@ -31,6 +30,21 @@ pub struct BranchList {
}
impl BranchList {
pub fn open(_: &mut Workspace, _: &OpenRecent, cx: &mut ViewContext<Workspace>) {
let this = cx.view().clone();
cx.spawn(|_, mut cx| async move {
// Modal branch picker has a longer trailoff than a popover one.
let delegate = BranchListDelegate::new(this.clone(), 70, &cx).await?;
this.update(&mut cx, |workspace, cx| {
workspace.toggle_modal(cx, |cx| BranchList::new(delegate, 34., cx))
})?;
Ok(())
})
.detach_and_prompt_err("Failed to read branches", cx, |_, _| None)
}
fn new(delegate: BranchListDelegate, rem_width: f32, cx: &mut ViewContext<Self>) -> Self {
let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
let _subscription = cx.subscribe(&picker, |_, _, _, cx| cx.emit(DismissEvent));
@ -40,17 +54,6 @@ impl BranchList {
_subscription,
}
}
pub fn open(
workspace: &mut Workspace,
_: &OpenRecent,
cx: &mut ViewContext<Workspace>,
) -> Result<()> {
// Modal branch picker has a longer trailoff than a popover one.
let delegate = BranchListDelegate::new(workspace, cx.view().clone(), 70, cx)?;
workspace.toggle_modal(cx, |cx| BranchList::new(delegate, 34., cx));
Ok(())
}
}
impl ModalView for BranchList {}
impl EventEmitter<DismissEvent> for BranchList {}
@ -100,36 +103,32 @@ pub struct BranchListDelegate {
}
impl BranchListDelegate {
fn new(
workspace: &Workspace,
handle: View<Workspace>,
async fn new(
workspace: View<Workspace>,
branch_name_trailoff_after: usize,
cx: &AppContext,
cx: &AsyncAppContext,
) -> Result<Self> {
let project = workspace.project().read(cx);
let repo = project
.get_first_worktree_root_repo(cx)
.context("failed to get root repository for first worktree")?;
let all_branches_request = cx.update(|cx| {
let project = workspace.read(cx).project().read(cx);
let first_worktree = project
.visible_worktrees(cx)
.next()
.context("No worktrees found")?;
let project_path = ProjectPath::root_path(first_worktree.read(cx).id());
anyhow::Ok(project.branches(project_path, cx))
})??;
let all_branches = all_branches_request.await?;
let all_branches = repo.branches()?;
Ok(Self {
matches: vec![],
workspace: handle,
workspace,
all_branches,
selected_index: 0,
last_query: Default::default(),
branch_name_trailoff_after,
})
}
fn display_error_toast(&self, message: String, cx: &mut WindowContext<'_>) {
self.workspace.update(cx, |model, ctx| {
struct GitCheckoutFailure;
let id = NotificationId::unique::<GitCheckoutFailure>();
model.show_toast(Toast::new(id, message), ctx)
});
}
}
impl PickerDelegate for BranchListDelegate {
@ -235,40 +234,32 @@ impl PickerDelegate for BranchListDelegate {
cx.spawn({
let branch = branch.clone();
|picker, mut cx| async move {
picker
.update(&mut cx, |this, cx| {
let project = this.delegate.workspace.read(cx).project().read(cx);
let repo = project
.get_first_worktree_root_repo(cx)
.context("failed to get root repository for first worktree")?;
let branch_change_task = picker.update(&mut cx, |this, cx| {
let project = this.delegate.workspace.read(cx).project().read(cx);
let branch_to_checkout = match branch {
BranchEntry::Branch(branch) => branch.string,
BranchEntry::NewBranch { name: branch_name } => {
let status = repo.create_branch(&branch_name);
if status.is_err() {
this.delegate.display_error_toast(format!("Failed to create branch '{branch_name}', check for conflicts or unstashed files"), cx);
status?;
}
let branch_to_checkout = match branch {
BranchEntry::Branch(branch) => branch.string,
BranchEntry::NewBranch { name: branch_name } => branch_name,
};
let worktree = project
.worktrees(cx)
.next()
.context("worktree disappeared")?;
let repository = ProjectPath::root_path(worktree.read(cx).id());
branch_name
}
};
anyhow::Ok(project.update_or_create_branch(repository, branch_to_checkout, cx))
})??;
let status = repo.change_branch(&branch_to_checkout);
if status.is_err() {
this.delegate.display_error_toast(format!("Failed to checkout branch '{branch_to_checkout}', check for conflicts or unstashed files"), cx);
status?;
}
branch_change_task.await?;
cx.emit(DismissEvent);
picker.update(&mut cx, |_, cx| {
cx.emit(DismissEvent);
Ok::<(), anyhow::Error>(())
})
.log_err();
Ok::<(), anyhow::Error>(())
})
}
})
.detach();
.detach_and_prompt_err("Failed to change branch", cx, |_, _| None);
}
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {