This commit is contained in:
2026-08-12 11:12:09 +07:00
parent 54781e2ad9
commit 447888e2fb
2 changed files with 85 additions and 3 deletions
+47 -2
View File
@@ -309,6 +309,23 @@ pub fn worktree_all_commits(workdir: &Path) -> Result<Vec<FileCommit>> {
all_commits(&gix::open(workdir)?) all_commits(&gix::open(workdir)?)
} }
/// The commit HEAD points to, like `git log -1`. Returns `Ok(None)` for a
/// repository without commits yet (unborn HEAD).
pub fn head_commit(repo: &gix::Repository) -> Result<Option<FileCommit>> {
let Some(head) = repo.head_id().ok() else {
return Ok(None);
};
let commit = head.object()?.into_commit();
let author = commit.author()?;
let message = commit.message()?;
Ok(Some(FileCommit {
id: commit.id().shorten_or_id().to_string(),
summary: String::from_utf8_lossy(message.title).trim().to_string(),
author: String::from_utf8_lossy(author.name).trim().to_string(),
time: author.time()?.seconds,
}))
}
/// Short names of local branches (`refs/heads/*`), sorted alphabetically. /// Short names of local branches (`refs/heads/*`), sorted alphabetically.
pub fn worktree_branches(workdir: &Path) -> Result<Vec<String>> { pub fn worktree_branches(workdir: &Path) -> Result<Vec<String>> {
let repo = gix::open(workdir)?; let repo = gix::open(workdir)?;
@@ -353,10 +370,12 @@ pub struct WorktreeSnapshot {
pub readme: Option<Vec<u8>>, pub readme: Option<Vec<u8>>,
/// Branch HEAD points to (`None` when detached, e.g. on a tag). /// Branch HEAD points to (`None` when detached, e.g. on a tag).
pub current_branch: Option<String>, pub current_branch: Option<String>,
/// Commit HEAD points to, if any (see [`head_commit`]).
pub head_commit: Option<FileCommit>,
} }
/// Snapshot the worktree after a branch/tag switch: entries, README and the /// Snapshot the worktree after a branch/tag switch: entries, README, the
/// branch HEAD points to, opening the repository once. /// branch HEAD points to and its commit, opening the repository once.
pub fn worktree_snapshot(workdir: &Path) -> Result<WorktreeSnapshot> { pub fn worktree_snapshot(workdir: &Path) -> Result<WorktreeSnapshot> {
let repo = gix::open(workdir)?; let repo = gix::open(workdir)?;
let readme_path = find_readme(&repo)?; let readme_path = find_readme(&repo)?;
@@ -369,6 +388,7 @@ pub fn worktree_snapshot(workdir: &Path) -> Result<WorktreeSnapshot> {
readme_path, readme_path,
readme, readme,
current_branch: current_branch(&repo)?, current_branch: current_branch(&repo)?,
head_commit: head_commit(&repo)?,
}) })
} }
@@ -672,6 +692,23 @@ mod tests {
assert!(find_readme(&repo).expect("find").is_none()); assert!(find_readme(&repo).expect("find").is_none());
} }
#[test]
fn head_commit_reports_head() {
let (_dir, repo) = fixture(&[("a.txt", b"one")]);
// Unborn HEAD: no commit yet.
assert!(head_commit(&repo).expect("head").is_none());
commit_all(&repo, "initial");
let head = head_commit(&repo).expect("head").expect("commit");
assert_eq!(
head.id,
repo.head_id().expect("head id").shorten_or_id().to_string()
);
assert_eq!(head.summary, "initial");
assert_eq!(head.author, "Test Author");
}
#[test] #[test]
fn worktree_branches_and_tags_list_short_names() { fn worktree_branches_and_tags_list_short_names() {
let (dir, repo) = fixture(&[("a.txt", b"one")]); let (dir, repo) = fixture(&[("a.txt", b"one")]);
@@ -743,6 +780,10 @@ mod tests {
let snapshot = worktree_snapshot(dir).expect("snapshot"); let snapshot = worktree_snapshot(dir).expect("snapshot");
assert_eq!(snapshot.current_branch.as_deref(), Some("feature")); assert_eq!(snapshot.current_branch.as_deref(), Some("feature"));
assert_eq!(
snapshot.head_commit.as_ref().expect("head commit").summary,
"feature work"
);
assert_eq!( assert_eq!(
String::from_utf8(snapshot.readme.expect("readme")).expect("utf8"), String::from_utf8(snapshot.readme.expect("readme")).expect("utf8"),
"# feature" "# feature"
@@ -763,6 +804,10 @@ mod tests {
let snapshot = worktree_snapshot(dir).expect("snapshot"); let snapshot = worktree_snapshot(dir).expect("snapshot");
assert_eq!(snapshot.current_branch.as_deref(), Some(default.as_str())); assert_eq!(snapshot.current_branch.as_deref(), Some(default.as_str()));
assert_eq!(
snapshot.head_commit.as_ref().expect("head commit").summary,
"initial"
);
assert_eq!( assert_eq!(
String::from_utf8(snapshot.readme.expect("readme")).expect("utf8"), String::from_utf8(snapshot.readme.expect("readme")).expect("utf8"),
"# main" "# main"
+38 -1
View File
@@ -79,6 +79,8 @@ pub struct RepoDetailView {
/// A clone/fetch is in flight. /// A clone/fetch is in flight.
loading: bool, loading: bool,
error: Option<SharedString>, error: Option<SharedString>,
/// Commit HEAD currently points to, shown in the header button.
head_commit: Option<FileCommit>,
/// Branch selector (header): local branches, searchable. /// Branch selector (header): local branches, searchable.
branch_select: Entity<ComboboxState<SearchableVec<SharedString>>>, branch_select: Entity<ComboboxState<SearchableVec<SharedString>>>,
/// Tag selector (header): tags, searchable. /// Tag selector (header): tags, searchable.
@@ -164,6 +166,7 @@ impl RepoDetailView {
item_sizes: Rc::new(Vec::new()), item_sizes: Rc::new(Vec::new()),
loading: true, loading: true,
error: None, error: None,
head_commit: None,
branch_select, branch_select,
tag_select, tag_select,
switching_ref: false, switching_ref: false,
@@ -203,6 +206,7 @@ impl RepoDetailView {
), ),
None => (Vec::new(), Vec::new(), None), None => (Vec::new(), Vec::new(), None),
}; };
let head_commit = signed_git::head_commit(&repo).unwrap_or(None);
Ok::<_, Error>(( Ok::<_, Error>((
entries, entries,
@@ -212,6 +216,7 @@ impl RepoDetailView {
branches, branches,
tags, tags,
current_branch, current_branch,
head_commit,
)) ))
}); });
@@ -228,8 +233,10 @@ impl RepoDetailView {
branches, branches,
tags, tags,
current_branch, current_branch,
head_commit,
)) => { )) => {
this.worktree = Some(worktree); this.worktree = Some(worktree);
this.head_commit = head_commit;
this.tree_state.update(cx, |state, cx| { this.tree_state.update(cx, |state, cx| {
state.set_items(build_tree_items(&entries), cx); state.set_items(build_tree_items(&entries), cx);
}); });
@@ -259,7 +266,7 @@ impl RepoDetailView {
} }
} }
} }
Ok((_, _, _, None, _, _, _)) => { Ok((_, _, _, None, _, _, _, _)) => {
this.error = Some("Repository has no worktree".into()); this.error = Some("Repository has no worktree".into());
} }
Err(error) => { Err(error) => {
@@ -589,6 +596,7 @@ impl RepoDetailView {
this.switching_ref = false; this.switching_ref = false;
match result { match result {
Ok(snapshot) => { Ok(snapshot) => {
this.head_commit = snapshot.head_commit;
// Rebuild the tree from scratch: entries of the // Rebuild the tree from scratch: entries of the
// previous branch are gone, and with them the // previous branch are gone, and with them the
// expansion state. // expansion state.
@@ -619,6 +627,7 @@ impl RepoDetailView {
} }
Err(error) => { Err(error) => {
this.error = Some(error.to_string().into()); this.error = Some(error.to_string().into());
this.head_commit = None;
// The tree may show files that no longer exist. // The tree may show files that no longer exist.
this.tree_state.update(cx, |state, cx| { this.tree_state.update(cx, |state, cx| {
state.set_items(Vec::new(), cx); state.set_items(Vec::new(), cx);
@@ -870,6 +879,34 @@ impl Render for RepoDetailView {
) )
}), }),
), ),
)
.child(
Button::new("enc")
.secondary()
.when_some(self.head_commit.as_ref(), |this, commit| {
this.child(
div()
.text_xs()
.text_color(cx.theme().muted_foreground)
.child(SharedString::from(&commit.id)),
)
.child(
div()
.max_w(px(200.))
.overflow_hidden()
.text_ellipsis()
.whitespace_nowrap()
.text_xs()
.child(SharedString::from(&commit.summary)),
)
})
.tooltip(
self.head_commit
.as_ref()
.map_or_else(SharedString::default, |commit| {
commit.summary.clone().into()
}),
),
), ),
), ),
), ),