diff --git a/crates/fs/src/fake_git_repo.rs b/crates/fs/src/fake_git_repo.rs index 378a8fb7df..04ba656232 100644 --- a/crates/fs/src/fake_git_repo.rs +++ b/crates/fs/src/fake_git_repo.rs @@ -10,7 +10,7 @@ use git::{ }, status::{FileStatus, GitStatus, StatusCode, TrackedStatus, UnmergedStatus}, }; -use gpui::{AsyncApp, BackgroundExecutor}; +use gpui::{AsyncApp, BackgroundExecutor, SharedString}; use ignore::gitignore::GitignoreBuilder; use rope::Rope; use smol::future::FutureExt as _; @@ -491,4 +491,8 @@ impl GitRepository for FakeGitRepository { ) -> BoxFuture<'_, Result> { unimplemented!() } + + fn default_branch(&self) -> BoxFuture<'_, Result>> { + unimplemented!() + } } diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index a63315e69e..b536bed710 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -463,6 +463,8 @@ pub trait GitRepository: Send + Sync { base_checkpoint: GitRepositoryCheckpoint, target_checkpoint: GitRepositoryCheckpoint, ) -> BoxFuture<'_, Result>; + + fn default_branch(&self) -> BoxFuture<'_, Result>>; } pub enum DiffType { @@ -1607,6 +1609,37 @@ impl GitRepository for RealGitRepository { }) .boxed() } + + fn default_branch(&self) -> BoxFuture<'_, Result>> { + 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 { diff --git a/crates/git_ui/src/branch_picker.rs b/crates/git_ui/src/branch_picker.rs index 9eac3ce5af..1092ba33d1 100644 --- a/crates/git_ui/src/branch_picker.rs +++ b/crates/git_ui/src/branch_picker.rs @@ -13,7 +13,7 @@ use project::git_store::Repository; use std::sync::Arc; use time::OffsetDateTime; use time_format::format_local_timestamp; -use ui::{HighlightedLabel, ListItem, ListItemSpacing, prelude::*}; +use ui::{HighlightedLabel, ListItem, ListItemSpacing, Tooltip, prelude::*}; use util::ResultExt; use workspace::notifications::DetachAndPromptErr; use workspace::{ModalView, Workspace}; @@ -90,11 +90,21 @@ impl BranchList { let all_branches_request = repository .clone() .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| { let mut all_branches = all_branches_request .context("No active repository")? .await??; + let default_branch = default_branch_request + .context("No active repository")? + .await + .map(Result::ok) + .ok() + .flatten() + .flatten(); let all_branches = cx .background_spawn(async move { @@ -124,6 +134,7 @@ impl BranchList { this.update_in(cx, |this, window, cx| { this.picker.update(cx, |picker, cx| { + picker.delegate.default_branch = default_branch; picker.delegate.all_branches = Some(all_branches); picker.refresh(window, cx); }) @@ -192,6 +203,7 @@ struct BranchEntry { pub struct BranchListDelegate { matches: Vec, all_branches: Option>, + default_branch: Option, repo: Option>, style: BranchListStyle, selected_index: usize, @@ -206,6 +218,7 @@ impl BranchListDelegate { repo, style, all_branches: None, + default_branch: None, selected_index: 0, last_query: Default::default(), modifiers: Default::default(), @@ -214,6 +227,7 @@ impl BranchListDelegate { fn create_branch( &self, + from_branch: Option, new_branch_name: SharedString, window: &mut Window, cx: &mut Context>, @@ -223,6 +237,11 @@ impl BranchListDelegate { }; let new_branch_name = new_branch_name.to_string().replace(' ', "-"); 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.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>) { + fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context>) { let Some(entry) = self.matches.get(self.selected_index()) else { return; }; 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; } @@ -439,6 +468,28 @@ impl PickerDelegate for BranchListDelegate { }) .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 { h_flex() .gap_1() @@ -504,7 +555,8 @@ impl PickerDelegate for BranchListDelegate { .color(Color::Muted) })) }), - ), + ) + .end_slot::(icon), ) } diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index 28dd0e91e3..c9f0fc7959 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -4025,6 +4025,25 @@ impl Repository { }) } + pub fn default_branch(&mut self) -> oneshot::Receiver>> { + 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> { 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 ea08d36371..c32da9b110 100644 --- a/crates/proto/proto/git.proto +++ b/crates/proto/proto/git.proto @@ -422,3 +422,12 @@ message BlameBufferResponse { reserved 1 to 4; } + +message GetDefaultBranch { + uint64 project_id = 1; + uint64 repository_id = 2; +} + +message GetDefaultBranchResponse { + optional string branch = 1; +} diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index 29ab2b1e90..d511ea5e8f 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -399,7 +399,10 @@ message Envelope { GetColorPresentationResponse get_color_presentation_response = 356; 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; diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index 83e5a77c86..72b3807deb 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -315,7 +315,9 @@ messages!( (LogToDebugConsole, Background), (GetDocumentDiagnostics, Background), (GetDocumentDiagnosticsResponse, Background), - (PullWorkspaceDiagnostics, Background) + (PullWorkspaceDiagnostics, Background), + (GetDefaultBranch, Background), + (GetDefaultBranchResponse, Background), ); request_messages!( @@ -483,7 +485,8 @@ request_messages!( (GetDebugAdapterBinary, DebugAdapterBinary), (RunDebugLocators, DebugRequest), (GetDocumentDiagnostics, GetDocumentDiagnosticsResponse), - (PullWorkspaceDiagnostics, Ack) + (PullWorkspaceDiagnostics, Ack), + (GetDefaultBranch, GetDefaultBranchResponse), ); entity_messages!( @@ -615,7 +618,8 @@ entity_messages!( GetDebugAdapterBinary, LogToDebugConsole, GetDocumentDiagnostics, - PullWorkspaceDiagnostics + PullWorkspaceDiagnostics, + GetDefaultBranch ); entity_messages!(