project: Show detached head commit SHA in branch pickers (#29007)
When Git is in a detached HEAD state, the branch is `None`, and we can't get any meaningful information from it. This PR adds a `head_commit` field to the snapshot, which is always populated with the HEAD details, even when the branch is `None`. This also pave path to fix: https://github.com/zed-industries/zed/issues/28736 git panel branch picker (before, after): <img width="197" alt="image" src="https://github.com/user-attachments/assets/0b6abbba-2988-4890-a708-bcd8aad84f26" /> <img width="198" alt="image" src="https://github.com/user-attachments/assets/4b08b1a8-5e79-4aa3-a44e-932249602c18" /> title bar branch picker (before, after): <img width="183" alt="image" src="https://github.com/user-attachments/assets/d94357f8-a4da-4d60-8ddd-fdd978b99fdf" /> <img width="228" alt="image" src="https://github.com/user-attachments/assets/d20824a1-9279-44d6-afd1-bf9319fc50e4" /> Release Notes: - Added head commit SHA information to the Git branch picker in the title bar and Git panel.
This commit is contained in:
parent
c2cd4fd7a1
commit
ba7f886c62
8 changed files with 109 additions and 7 deletions
|
@ -117,6 +117,7 @@ CREATE TABLE "project_repositories" (
|
||||||
"is_deleted" BOOL NOT NULL,
|
"is_deleted" BOOL NOT NULL,
|
||||||
"current_merge_conflicts" VARCHAR,
|
"current_merge_conflicts" VARCHAR,
|
||||||
"branch_summary" VARCHAR,
|
"branch_summary" VARCHAR,
|
||||||
|
"head_commit_details" VARCHAR,
|
||||||
PRIMARY KEY (project_id, id)
|
PRIMARY KEY (project_id, id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -348,9 +348,10 @@ impl Database {
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
)),
|
)),
|
||||||
|
|
||||||
// Old clients do not use abs path or entry ids.
|
// Old clients do not use abs path, entry ids or head_commit_details.
|
||||||
abs_path: ActiveValue::set(String::new()),
|
abs_path: ActiveValue::set(String::new()),
|
||||||
entry_ids: ActiveValue::set("[]".into()),
|
entry_ids: ActiveValue::set("[]".into()),
|
||||||
|
head_commit_details: ActiveValue::set(None),
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
@ -490,6 +491,12 @@ impl Database {
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|summary| serde_json::to_string(summary).unwrap()),
|
.map(|summary| serde_json::to_string(summary).unwrap()),
|
||||||
),
|
),
|
||||||
|
head_commit_details: ActiveValue::Set(
|
||||||
|
update
|
||||||
|
.head_commit_details
|
||||||
|
.as_ref()
|
||||||
|
.map(|details| serde_json::to_string(details).unwrap()),
|
||||||
|
),
|
||||||
current_merge_conflicts: ActiveValue::Set(Some(
|
current_merge_conflicts: ActiveValue::Set(Some(
|
||||||
serde_json::to_string(&update.current_merge_conflicts).unwrap(),
|
serde_json::to_string(&update.current_merge_conflicts).unwrap(),
|
||||||
)),
|
)),
|
||||||
|
@ -505,6 +512,7 @@ impl Database {
|
||||||
project_repository::Column::EntryIds,
|
project_repository::Column::EntryIds,
|
||||||
project_repository::Column::AbsPath,
|
project_repository::Column::AbsPath,
|
||||||
project_repository::Column::CurrentMergeConflicts,
|
project_repository::Column::CurrentMergeConflicts,
|
||||||
|
project_repository::Column::HeadCommitDetails,
|
||||||
])
|
])
|
||||||
.to_owned(),
|
.to_owned(),
|
||||||
)
|
)
|
||||||
|
@ -928,6 +936,13 @@ impl Database {
|
||||||
.transpose()?
|
.transpose()?
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let head_commit_details = db_repository_entry
|
||||||
|
.head_commit_details
|
||||||
|
.as_ref()
|
||||||
|
.map(|head_commit_details| serde_json::from_str(&head_commit_details))
|
||||||
|
.transpose()?
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
let entry_ids = serde_json::from_str(&db_repository_entry.entry_ids)
|
let entry_ids = serde_json::from_str(&db_repository_entry.entry_ids)
|
||||||
.context("failed to deserialize repository's entry ids")?;
|
.context("failed to deserialize repository's entry ids")?;
|
||||||
|
|
||||||
|
@ -954,6 +969,7 @@ impl Database {
|
||||||
removed_statuses: Vec::new(),
|
removed_statuses: Vec::new(),
|
||||||
current_merge_conflicts,
|
current_merge_conflicts,
|
||||||
branch_summary,
|
branch_summary,
|
||||||
|
head_commit_details,
|
||||||
scan_id: db_repository_entry.scan_id as u64,
|
scan_id: db_repository_entry.scan_id as u64,
|
||||||
is_last_update: true,
|
is_last_update: true,
|
||||||
});
|
});
|
||||||
|
|
|
@ -755,6 +755,13 @@ impl Database {
|
||||||
.transpose()?
|
.transpose()?
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let head_commit_details = db_repository
|
||||||
|
.head_commit_details
|
||||||
|
.as_ref()
|
||||||
|
.map(|head_commit_details| serde_json::from_str(&head_commit_details))
|
||||||
|
.transpose()?
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
let entry_ids = serde_json::from_str(&db_repository.entry_ids)
|
let entry_ids = serde_json::from_str(&db_repository.entry_ids)
|
||||||
.context("failed to deserialize repository's entry ids")?;
|
.context("failed to deserialize repository's entry ids")?;
|
||||||
|
|
||||||
|
@ -778,6 +785,7 @@ impl Database {
|
||||||
removed_statuses,
|
removed_statuses,
|
||||||
current_merge_conflicts,
|
current_merge_conflicts,
|
||||||
branch_summary,
|
branch_summary,
|
||||||
|
head_commit_details,
|
||||||
project_id: project_id.to_proto(),
|
project_id: project_id.to_proto(),
|
||||||
id: db_repository.id as u64,
|
id: db_repository.id as u64,
|
||||||
abs_path: db_repository.abs_path,
|
abs_path: db_repository.abs_path,
|
||||||
|
|
|
@ -18,6 +18,8 @@ pub struct Model {
|
||||||
pub current_merge_conflicts: Option<String>,
|
pub current_merge_conflicts: Option<String>,
|
||||||
// A JSON object representing the current Branch values
|
// A JSON object representing the current Branch values
|
||||||
pub branch_summary: Option<String>,
|
pub branch_summary: Option<String>,
|
||||||
|
// A JSON object representing the current Head commit values
|
||||||
|
pub head_commit_details: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
|
|
@ -2938,6 +2938,7 @@ impl GitPanel {
|
||||||
let expand_tooltip_focus_handle = editor_focus_handle.clone();
|
let expand_tooltip_focus_handle = editor_focus_handle.clone();
|
||||||
|
|
||||||
let branch = active_repository.read(cx).branch.clone();
|
let branch = active_repository.read(cx).branch.clone();
|
||||||
|
let head_commit = active_repository.read(cx).head_commit.clone();
|
||||||
|
|
||||||
let footer_size = px(32.);
|
let footer_size = px(32.);
|
||||||
let gap = px(9.0);
|
let gap = px(9.0);
|
||||||
|
@ -2966,6 +2967,7 @@ impl GitPanel {
|
||||||
.child(PanelRepoFooter::new(
|
.child(PanelRepoFooter::new(
|
||||||
display_name,
|
display_name,
|
||||||
branch,
|
branch,
|
||||||
|
head_commit,
|
||||||
Some(git_panel.clone()),
|
Some(git_panel.clone()),
|
||||||
))
|
))
|
||||||
.child(
|
.child(
|
||||||
|
@ -4291,6 +4293,8 @@ impl Render for GitPanelMessageTooltip {
|
||||||
pub struct PanelRepoFooter {
|
pub struct PanelRepoFooter {
|
||||||
active_repository: SharedString,
|
active_repository: SharedString,
|
||||||
branch: Option<Branch>,
|
branch: Option<Branch>,
|
||||||
|
head_commit: Option<CommitDetails>,
|
||||||
|
|
||||||
// Getting a GitPanel in previews will be difficult.
|
// Getting a GitPanel in previews will be difficult.
|
||||||
//
|
//
|
||||||
// For now just take an option here, and we won't bind handlers to buttons in previews.
|
// For now just take an option here, and we won't bind handlers to buttons in previews.
|
||||||
|
@ -4301,11 +4305,13 @@ impl PanelRepoFooter {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
active_repository: SharedString,
|
active_repository: SharedString,
|
||||||
branch: Option<Branch>,
|
branch: Option<Branch>,
|
||||||
|
head_commit: Option<CommitDetails>,
|
||||||
git_panel: Option<Entity<GitPanel>>,
|
git_panel: Option<Entity<GitPanel>>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
active_repository,
|
active_repository,
|
||||||
branch,
|
branch,
|
||||||
|
head_commit,
|
||||||
git_panel,
|
git_panel,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4314,6 +4320,7 @@ impl PanelRepoFooter {
|
||||||
Self {
|
Self {
|
||||||
active_repository,
|
active_repository,
|
||||||
branch,
|
branch,
|
||||||
|
head_commit: None,
|
||||||
git_panel: None,
|
git_panel: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4339,11 +4346,26 @@ impl RenderOnce for PanelRepoFooter {
|
||||||
const MAX_BRANCH_LEN: usize = 16;
|
const MAX_BRANCH_LEN: usize = 16;
|
||||||
const MAX_REPO_LEN: usize = 16;
|
const MAX_REPO_LEN: usize = 16;
|
||||||
const LABEL_CHARACTER_BUDGET: usize = MAX_BRANCH_LEN + MAX_REPO_LEN;
|
const LABEL_CHARACTER_BUDGET: usize = MAX_BRANCH_LEN + MAX_REPO_LEN;
|
||||||
|
const MAX_SHORT_SHA_LEN: usize = 8;
|
||||||
|
|
||||||
let branch = self.branch.clone();
|
let branch_name = self
|
||||||
let branch_name = branch
|
.branch
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map_or(" (no branch)".into(), |branch| branch.name.clone());
|
.map(|branch| branch.name.clone())
|
||||||
|
.or_else(|| {
|
||||||
|
self.head_commit.as_ref().map(|commit| {
|
||||||
|
SharedString::from(
|
||||||
|
commit
|
||||||
|
.sha
|
||||||
|
.chars()
|
||||||
|
.take(MAX_SHORT_SHA_LEN)
|
||||||
|
.collect::<String>(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| SharedString::from(" (no branch)"));
|
||||||
|
let show_separator = self.branch.is_some() || self.head_commit.is_some();
|
||||||
|
|
||||||
let active_repo_name = self.active_repository.clone();
|
let active_repo_name = self.active_repository.clone();
|
||||||
|
|
||||||
let branch_actual_len = branch_name.len();
|
let branch_actual_len = branch_name.len();
|
||||||
|
@ -4449,7 +4471,7 @@ impl RenderOnce for PanelRepoFooter {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(repo_selector)
|
.child(repo_selector)
|
||||||
.when_some(branch.clone(), |this, _| {
|
.when(show_separator, |this| {
|
||||||
this.child(
|
this.child(
|
||||||
div()
|
div()
|
||||||
.text_color(cx.theme().colors().text_muted)
|
.text_color(cx.theme().colors().text_muted)
|
||||||
|
|
|
@ -231,6 +231,7 @@ pub struct RepositorySnapshot {
|
||||||
pub statuses_by_path: SumTree<StatusEntry>,
|
pub statuses_by_path: SumTree<StatusEntry>,
|
||||||
pub work_directory_abs_path: Arc<Path>,
|
pub work_directory_abs_path: Arc<Path>,
|
||||||
pub branch: Option<Branch>,
|
pub branch: Option<Branch>,
|
||||||
|
pub head_commit: Option<CommitDetails>,
|
||||||
pub merge_conflicts: TreeSet<RepoPath>,
|
pub merge_conflicts: TreeSet<RepoPath>,
|
||||||
pub merge_head_shas: Vec<SharedString>,
|
pub merge_head_shas: Vec<SharedString>,
|
||||||
pub scan_id: u64,
|
pub scan_id: u64,
|
||||||
|
@ -2426,6 +2427,7 @@ impl RepositorySnapshot {
|
||||||
statuses_by_path: Default::default(),
|
statuses_by_path: Default::default(),
|
||||||
work_directory_abs_path,
|
work_directory_abs_path,
|
||||||
branch: None,
|
branch: None,
|
||||||
|
head_commit: None,
|
||||||
merge_conflicts: Default::default(),
|
merge_conflicts: Default::default(),
|
||||||
merge_head_shas: Default::default(),
|
merge_head_shas: Default::default(),
|
||||||
scan_id: 0,
|
scan_id: 0,
|
||||||
|
@ -2435,6 +2437,7 @@ impl RepositorySnapshot {
|
||||||
fn initial_update(&self, project_id: u64) -> proto::UpdateRepository {
|
fn initial_update(&self, project_id: u64) -> proto::UpdateRepository {
|
||||||
proto::UpdateRepository {
|
proto::UpdateRepository {
|
||||||
branch_summary: self.branch.as_ref().map(branch_to_proto),
|
branch_summary: self.branch.as_ref().map(branch_to_proto),
|
||||||
|
head_commit_details: self.head_commit.as_ref().map(commit_details_to_proto),
|
||||||
updated_statuses: self
|
updated_statuses: self
|
||||||
.statuses_by_path
|
.statuses_by_path
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -2499,6 +2502,7 @@ impl RepositorySnapshot {
|
||||||
|
|
||||||
proto::UpdateRepository {
|
proto::UpdateRepository {
|
||||||
branch_summary: self.branch.as_ref().map(branch_to_proto),
|
branch_summary: self.branch.as_ref().map(branch_to_proto),
|
||||||
|
head_commit_details: self.head_commit.as_ref().map(commit_details_to_proto),
|
||||||
updated_statuses,
|
updated_statuses,
|
||||||
removed_statuses,
|
removed_statuses,
|
||||||
current_merge_conflicts: self
|
current_merge_conflicts: self
|
||||||
|
@ -3748,6 +3752,11 @@ impl Repository {
|
||||||
.map(|path| RepoPath(Path::new(&path).into())),
|
.map(|path| RepoPath(Path::new(&path).into())),
|
||||||
);
|
);
|
||||||
self.snapshot.branch = update.branch_summary.as_ref().map(proto_to_branch);
|
self.snapshot.branch = update.branch_summary.as_ref().map(proto_to_branch);
|
||||||
|
self.snapshot.head_commit = update
|
||||||
|
.head_commit_details
|
||||||
|
.as_ref()
|
||||||
|
.map(proto_to_commit_details);
|
||||||
|
|
||||||
self.snapshot.merge_conflicts = conflicted_paths;
|
self.snapshot.merge_conflicts = conflicted_paths;
|
||||||
|
|
||||||
let edits = update
|
let edits = update
|
||||||
|
@ -4322,6 +4331,26 @@ fn proto_to_branch(proto: &proto::Branch) -> git::repository::Branch {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn commit_details_to_proto(commit: &CommitDetails) -> proto::GitCommitDetails {
|
||||||
|
proto::GitCommitDetails {
|
||||||
|
sha: commit.sha.to_string(),
|
||||||
|
message: commit.message.to_string(),
|
||||||
|
commit_timestamp: commit.commit_timestamp,
|
||||||
|
author_email: commit.author_email.to_string(),
|
||||||
|
author_name: commit.author_name.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn proto_to_commit_details(proto: &proto::GitCommitDetails) -> CommitDetails {
|
||||||
|
CommitDetails {
|
||||||
|
sha: proto.sha.clone().into(),
|
||||||
|
message: proto.message.clone().into(),
|
||||||
|
commit_timestamp: proto.commit_timestamp,
|
||||||
|
author_email: proto.author_email.clone().into(),
|
||||||
|
author_name: proto.author_name.clone().into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn compute_snapshot(
|
async fn compute_snapshot(
|
||||||
id: RepositoryId,
|
id: RepositoryId,
|
||||||
work_directory_abs_path: Arc<Path>,
|
work_directory_abs_path: Arc<Path>,
|
||||||
|
@ -4377,6 +4406,12 @@ async fn compute_snapshot(
|
||||||
events.push(RepositoryEvent::MergeHeadsChanged);
|
events.push(RepositoryEvent::MergeHeadsChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Useful when branch is None in detached head state
|
||||||
|
let head_commit = match backend.head_sha() {
|
||||||
|
Some(head_sha) => backend.show(head_sha).await.ok(),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
let snapshot = RepositorySnapshot {
|
let snapshot = RepositorySnapshot {
|
||||||
id,
|
id,
|
||||||
merge_message,
|
merge_message,
|
||||||
|
@ -4384,6 +4419,7 @@ async fn compute_snapshot(
|
||||||
work_directory_abs_path,
|
work_directory_abs_path,
|
||||||
scan_id: prev_snapshot.scan_id + 1,
|
scan_id: prev_snapshot.scan_id + 1,
|
||||||
branch,
|
branch,
|
||||||
|
head_commit,
|
||||||
merge_conflicts,
|
merge_conflicts,
|
||||||
merge_head_shas,
|
merge_head_shas,
|
||||||
};
|
};
|
||||||
|
|
|
@ -120,6 +120,7 @@ message UpdateRepository {
|
||||||
repeated string current_merge_conflicts = 8;
|
repeated string current_merge_conflicts = 8;
|
||||||
uint64 scan_id = 9;
|
uint64 scan_id = 9;
|
||||||
bool is_last_update = 10;
|
bool is_last_update = 10;
|
||||||
|
optional GitCommitDetails head_commit_details = 11;
|
||||||
}
|
}
|
||||||
|
|
||||||
message RemoveRepository {
|
message RemoveRepository {
|
||||||
|
|
|
@ -46,6 +46,7 @@ pub use stories::*;
|
||||||
|
|
||||||
const MAX_PROJECT_NAME_LENGTH: usize = 40;
|
const MAX_PROJECT_NAME_LENGTH: usize = 40;
|
||||||
const MAX_BRANCH_NAME_LENGTH: usize = 40;
|
const MAX_BRANCH_NAME_LENGTH: usize = 40;
|
||||||
|
const MAX_SHORT_SHA_LENGTH: usize = 8;
|
||||||
|
|
||||||
const BOOK_ONBOARDING: &str = "https://dub.sh/zed-c-onboarding";
|
const BOOK_ONBOARDING: &str = "https://dub.sh/zed-c-onboarding";
|
||||||
|
|
||||||
|
@ -513,8 +514,23 @@ impl TitleBar {
|
||||||
pub fn render_project_branch(&self, cx: &mut Context<Self>) -> Option<impl IntoElement> {
|
pub fn render_project_branch(&self, cx: &mut Context<Self>) -> Option<impl IntoElement> {
|
||||||
let repository = self.project.read(cx).active_repository(cx)?;
|
let repository = self.project.read(cx).active_repository(cx)?;
|
||||||
let workspace = self.workspace.upgrade()?;
|
let workspace = self.workspace.upgrade()?;
|
||||||
let branch_name = repository.read(cx).branch.as_ref()?.name.clone();
|
let branch_name = {
|
||||||
let branch_name = util::truncate_and_trailoff(&branch_name, MAX_BRANCH_NAME_LENGTH);
|
let repo = repository.read(cx);
|
||||||
|
repo.branch
|
||||||
|
.as_ref()
|
||||||
|
.map(|branch| branch.name.clone())
|
||||||
|
.map(|name| util::truncate_and_trailoff(&name, MAX_BRANCH_NAME_LENGTH))
|
||||||
|
.or_else(|| {
|
||||||
|
repo.head_commit.as_ref().map(|commit| {
|
||||||
|
commit
|
||||||
|
.sha
|
||||||
|
.chars()
|
||||||
|
.take(MAX_SHORT_SHA_LENGTH)
|
||||||
|
.collect::<String>()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}?;
|
||||||
|
|
||||||
Some(
|
Some(
|
||||||
Button::new("project_branch_trigger", branch_name)
|
Button::new("project_branch_trigger", branch_name)
|
||||||
.color(Color::Muted)
|
.color(Color::Muted)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue