git: Add option to branch from default branch in branch picker (#34663)
Closes #33700 The option shows up as an icon that appears on entries that would create a new branch. You can also branch from the default by secondary confirming, which the icon has a tooltip for as well. We based the default branch on the results from this command: `git symbolic-ref refs/remotes/upstream/HEAD` and fallback to `git symbolic-ref refs/remotes/origin/HEAD` Release Notes: - Add option to create a branch from a default branch in git branch picker --------- Co-authored-by: Cole Miller <cole@zed.dev>
This commit is contained in:
parent
fa8dd1c547
commit
9fa634f02f
7 changed files with 133 additions and 9 deletions
|
@ -10,7 +10,7 @@ use git::{
|
||||||
},
|
},
|
||||||
status::{FileStatus, GitStatus, StatusCode, TrackedStatus, UnmergedStatus},
|
status::{FileStatus, GitStatus, StatusCode, TrackedStatus, UnmergedStatus},
|
||||||
};
|
};
|
||||||
use gpui::{AsyncApp, BackgroundExecutor};
|
use gpui::{AsyncApp, BackgroundExecutor, SharedString};
|
||||||
use ignore::gitignore::GitignoreBuilder;
|
use ignore::gitignore::GitignoreBuilder;
|
||||||
use rope::Rope;
|
use rope::Rope;
|
||||||
use smol::future::FutureExt as _;
|
use smol::future::FutureExt as _;
|
||||||
|
@ -491,4 +491,8 @@ impl GitRepository for FakeGitRepository {
|
||||||
) -> BoxFuture<'_, Result<String>> {
|
) -> BoxFuture<'_, Result<String>> {
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_branch(&self) -> BoxFuture<'_, Result<Option<SharedString>>> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -463,6 +463,8 @@ pub trait GitRepository: Send + Sync {
|
||||||
base_checkpoint: GitRepositoryCheckpoint,
|
base_checkpoint: GitRepositoryCheckpoint,
|
||||||
target_checkpoint: GitRepositoryCheckpoint,
|
target_checkpoint: GitRepositoryCheckpoint,
|
||||||
) -> BoxFuture<'_, Result<String>>;
|
) -> BoxFuture<'_, Result<String>>;
|
||||||
|
|
||||||
|
fn default_branch(&self) -> BoxFuture<'_, Result<Option<SharedString>>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum DiffType {
|
pub enum DiffType {
|
||||||
|
@ -1607,6 +1609,37 @@ impl GitRepository for RealGitRepository {
|
||||||
})
|
})
|
||||||
.boxed()
|
.boxed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_branch(&self) -> BoxFuture<'_, Result<Option<SharedString>>> {
|
||||||
|
let working_directory = self.working_directory();
|
||||||
|
let git_binary_path = self.git_binary_path.clone();
|
||||||
|
|
||||||
|
let executor = self.executor.clone();
|
||||||
|
self.executor
|
||||||
|
.spawn(async move {
|
||||||
|
let working_directory = working_directory?;
|
||||||
|
let git = GitBinary::new(git_binary_path, working_directory, executor);
|
||||||
|
|
||||||
|
if let Ok(output) = git
|
||||||
|
.run(&["symbolic-ref", "refs/remotes/upstream/HEAD"])
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
let output = output
|
||||||
|
.strip_prefix("refs/remotes/upstream/")
|
||||||
|
.map(|s| SharedString::from(s.to_owned()));
|
||||||
|
return Ok(output);
|
||||||
|
}
|
||||||
|
|
||||||
|
let output = git
|
||||||
|
.run(&["symbolic-ref", "refs/remotes/origin/HEAD"])
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(output
|
||||||
|
.strip_prefix("refs/remotes/origin/")
|
||||||
|
.map(|s| SharedString::from(s.to_owned())))
|
||||||
|
})
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn git_status_args(path_prefixes: &[RepoPath]) -> Vec<OsString> {
|
fn git_status_args(path_prefixes: &[RepoPath]) -> Vec<OsString> {
|
||||||
|
|
|
@ -13,7 +13,7 @@ use project::git_store::Repository;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
use time_format::format_local_timestamp;
|
use time_format::format_local_timestamp;
|
||||||
use ui::{HighlightedLabel, ListItem, ListItemSpacing, prelude::*};
|
use ui::{HighlightedLabel, ListItem, ListItemSpacing, Tooltip, prelude::*};
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
use workspace::notifications::DetachAndPromptErr;
|
use workspace::notifications::DetachAndPromptErr;
|
||||||
use workspace::{ModalView, Workspace};
|
use workspace::{ModalView, Workspace};
|
||||||
|
@ -90,11 +90,21 @@ impl BranchList {
|
||||||
let all_branches_request = repository
|
let all_branches_request = repository
|
||||||
.clone()
|
.clone()
|
||||||
.map(|repository| repository.update(cx, |repository, _| repository.branches()));
|
.map(|repository| repository.update(cx, |repository, _| repository.branches()));
|
||||||
|
let default_branch_request = repository
|
||||||
|
.clone()
|
||||||
|
.map(|repository| repository.update(cx, |repository, _| repository.default_branch()));
|
||||||
|
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
let mut all_branches = all_branches_request
|
let mut all_branches = all_branches_request
|
||||||
.context("No active repository")?
|
.context("No active repository")?
|
||||||
.await??;
|
.await??;
|
||||||
|
let default_branch = default_branch_request
|
||||||
|
.context("No active repository")?
|
||||||
|
.await
|
||||||
|
.map(Result::ok)
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.flatten();
|
||||||
|
|
||||||
let all_branches = cx
|
let all_branches = cx
|
||||||
.background_spawn(async move {
|
.background_spawn(async move {
|
||||||
|
@ -124,6 +134,7 @@ impl BranchList {
|
||||||
|
|
||||||
this.update_in(cx, |this, window, cx| {
|
this.update_in(cx, |this, window, cx| {
|
||||||
this.picker.update(cx, |picker, cx| {
|
this.picker.update(cx, |picker, cx| {
|
||||||
|
picker.delegate.default_branch = default_branch;
|
||||||
picker.delegate.all_branches = Some(all_branches);
|
picker.delegate.all_branches = Some(all_branches);
|
||||||
picker.refresh(window, cx);
|
picker.refresh(window, cx);
|
||||||
})
|
})
|
||||||
|
@ -192,6 +203,7 @@ struct BranchEntry {
|
||||||
pub struct BranchListDelegate {
|
pub struct BranchListDelegate {
|
||||||
matches: Vec<BranchEntry>,
|
matches: Vec<BranchEntry>,
|
||||||
all_branches: Option<Vec<Branch>>,
|
all_branches: Option<Vec<Branch>>,
|
||||||
|
default_branch: Option<SharedString>,
|
||||||
repo: Option<Entity<Repository>>,
|
repo: Option<Entity<Repository>>,
|
||||||
style: BranchListStyle,
|
style: BranchListStyle,
|
||||||
selected_index: usize,
|
selected_index: usize,
|
||||||
|
@ -206,6 +218,7 @@ impl BranchListDelegate {
|
||||||
repo,
|
repo,
|
||||||
style,
|
style,
|
||||||
all_branches: None,
|
all_branches: None,
|
||||||
|
default_branch: None,
|
||||||
selected_index: 0,
|
selected_index: 0,
|
||||||
last_query: Default::default(),
|
last_query: Default::default(),
|
||||||
modifiers: Default::default(),
|
modifiers: Default::default(),
|
||||||
|
@ -214,6 +227,7 @@ impl BranchListDelegate {
|
||||||
|
|
||||||
fn create_branch(
|
fn create_branch(
|
||||||
&self,
|
&self,
|
||||||
|
from_branch: Option<SharedString>,
|
||||||
new_branch_name: SharedString,
|
new_branch_name: SharedString,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Picker<Self>>,
|
cx: &mut Context<Picker<Self>>,
|
||||||
|
@ -223,6 +237,11 @@ impl BranchListDelegate {
|
||||||
};
|
};
|
||||||
let new_branch_name = new_branch_name.to_string().replace(' ', "-");
|
let new_branch_name = new_branch_name.to_string().replace(' ', "-");
|
||||||
cx.spawn(async move |_, cx| {
|
cx.spawn(async move |_, cx| {
|
||||||
|
if let Some(based_branch) = from_branch {
|
||||||
|
repo.update(cx, |repo, _| repo.change_branch(based_branch.to_string()))?
|
||||||
|
.await??;
|
||||||
|
}
|
||||||
|
|
||||||
repo.update(cx, |repo, _| {
|
repo.update(cx, |repo, _| {
|
||||||
repo.create_branch(new_branch_name.to_string())
|
repo.create_branch(new_branch_name.to_string())
|
||||||
})?
|
})?
|
||||||
|
@ -353,12 +372,22 @@ impl PickerDelegate for BranchListDelegate {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||||
let Some(entry) = self.matches.get(self.selected_index()) else {
|
let Some(entry) = self.matches.get(self.selected_index()) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
if entry.is_new {
|
if entry.is_new {
|
||||||
self.create_branch(entry.branch.name().to_owned().into(), window, cx);
|
let from_branch = if secondary {
|
||||||
|
self.default_branch.clone()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
self.create_branch(
|
||||||
|
from_branch,
|
||||||
|
entry.branch.name().to_owned().into(),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -439,6 +468,28 @@ impl PickerDelegate for BranchListDelegate {
|
||||||
})
|
})
|
||||||
.unwrap_or_else(|| (None, None));
|
.unwrap_or_else(|| (None, None));
|
||||||
|
|
||||||
|
let icon = if let Some(default_branch) = self.default_branch.clone()
|
||||||
|
&& entry.is_new
|
||||||
|
{
|
||||||
|
Some(
|
||||||
|
IconButton::new("branch-from-default", IconName::GitBranchSmall)
|
||||||
|
.on_click(cx.listener(move |this, _, window, cx| {
|
||||||
|
this.delegate.set_selected_index(ix, window, cx);
|
||||||
|
this.delegate.confirm(true, window, cx);
|
||||||
|
}))
|
||||||
|
.tooltip(move |window, cx| {
|
||||||
|
Tooltip::for_action(
|
||||||
|
format!("Create branch based off default: {default_branch}"),
|
||||||
|
&menu::SecondaryConfirm,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let branch_name = if entry.is_new {
|
let branch_name = if entry.is_new {
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
|
@ -504,7 +555,8 @@ impl PickerDelegate for BranchListDelegate {
|
||||||
.color(Color::Muted)
|
.color(Color::Muted)
|
||||||
}))
|
}))
|
||||||
}),
|
}),
|
||||||
),
|
)
|
||||||
|
.end_slot::<IconButton>(icon),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4025,6 +4025,25 @@ impl Repository {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn default_branch(&mut self) -> oneshot::Receiver<Result<Option<SharedString>>> {
|
||||||
|
let id = self.id;
|
||||||
|
self.send_job(None, move |repo, _| async move {
|
||||||
|
match repo {
|
||||||
|
RepositoryState::Local { backend, .. } => backend.default_branch().await,
|
||||||
|
RepositoryState::Remote { project_id, client } => {
|
||||||
|
let response = client
|
||||||
|
.request(proto::GetDefaultBranch {
|
||||||
|
project_id: project_id.0,
|
||||||
|
repository_id: id.to_proto(),
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
anyhow::Ok(response.branch.map(SharedString::from))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn diff(&mut self, diff_type: DiffType, _cx: &App) -> oneshot::Receiver<Result<String>> {
|
pub fn diff(&mut self, diff_type: DiffType, _cx: &App) -> oneshot::Receiver<Result<String>> {
|
||||||
let id = self.id;
|
let id = self.id;
|
||||||
self.send_job(None, move |repo, _cx| async move {
|
self.send_job(None, move |repo, _cx| async move {
|
||||||
|
|
|
@ -422,3 +422,12 @@ message BlameBufferResponse {
|
||||||
|
|
||||||
reserved 1 to 4;
|
reserved 1 to 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message GetDefaultBranch {
|
||||||
|
uint64 project_id = 1;
|
||||||
|
uint64 repository_id = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetDefaultBranchResponse {
|
||||||
|
optional string branch = 1;
|
||||||
|
}
|
||||||
|
|
|
@ -399,7 +399,10 @@ message Envelope {
|
||||||
GetColorPresentationResponse get_color_presentation_response = 356;
|
GetColorPresentationResponse get_color_presentation_response = 356;
|
||||||
|
|
||||||
Stash stash = 357;
|
Stash stash = 357;
|
||||||
StashPop stash_pop = 358; // current max
|
StashPop stash_pop = 358;
|
||||||
|
|
||||||
|
GetDefaultBranch get_default_branch = 359;
|
||||||
|
GetDefaultBranchResponse get_default_branch_response = 360; // current max
|
||||||
}
|
}
|
||||||
|
|
||||||
reserved 87 to 88;
|
reserved 87 to 88;
|
||||||
|
|
|
@ -315,7 +315,9 @@ messages!(
|
||||||
(LogToDebugConsole, Background),
|
(LogToDebugConsole, Background),
|
||||||
(GetDocumentDiagnostics, Background),
|
(GetDocumentDiagnostics, Background),
|
||||||
(GetDocumentDiagnosticsResponse, Background),
|
(GetDocumentDiagnosticsResponse, Background),
|
||||||
(PullWorkspaceDiagnostics, Background)
|
(PullWorkspaceDiagnostics, Background),
|
||||||
|
(GetDefaultBranch, Background),
|
||||||
|
(GetDefaultBranchResponse, Background),
|
||||||
);
|
);
|
||||||
|
|
||||||
request_messages!(
|
request_messages!(
|
||||||
|
@ -483,7 +485,8 @@ request_messages!(
|
||||||
(GetDebugAdapterBinary, DebugAdapterBinary),
|
(GetDebugAdapterBinary, DebugAdapterBinary),
|
||||||
(RunDebugLocators, DebugRequest),
|
(RunDebugLocators, DebugRequest),
|
||||||
(GetDocumentDiagnostics, GetDocumentDiagnosticsResponse),
|
(GetDocumentDiagnostics, GetDocumentDiagnosticsResponse),
|
||||||
(PullWorkspaceDiagnostics, Ack)
|
(PullWorkspaceDiagnostics, Ack),
|
||||||
|
(GetDefaultBranch, GetDefaultBranchResponse),
|
||||||
);
|
);
|
||||||
|
|
||||||
entity_messages!(
|
entity_messages!(
|
||||||
|
@ -615,7 +618,8 @@ entity_messages!(
|
||||||
GetDebugAdapterBinary,
|
GetDebugAdapterBinary,
|
||||||
LogToDebugConsole,
|
LogToDebugConsole,
|
||||||
GetDocumentDiagnostics,
|
GetDocumentDiagnostics,
|
||||||
PullWorkspaceDiagnostics
|
PullWorkspaceDiagnostics,
|
||||||
|
GetDefaultBranch
|
||||||
);
|
);
|
||||||
|
|
||||||
entity_messages!(
|
entity_messages!(
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue