From 447888e2fbd23033cb4a31dc94fd0b8ff3b49a1e Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Wed, 12 Aug 2026 11:12:09 +0700 Subject: [PATCH] . --- crates/signed_git/src/lib.rs | 49 ++++++++++++++++++- crates/workspace/src/views/repo_detail/mod.rs | 39 ++++++++++++++- 2 files changed, 85 insertions(+), 3 deletions(-) diff --git a/crates/signed_git/src/lib.rs b/crates/signed_git/src/lib.rs index 454a09c..17c16f7 100644 --- a/crates/signed_git/src/lib.rs +++ b/crates/signed_git/src/lib.rs @@ -309,6 +309,23 @@ pub fn worktree_all_commits(workdir: &Path) -> Result> { 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> { + 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. pub fn worktree_branches(workdir: &Path) -> Result> { let repo = gix::open(workdir)?; @@ -353,10 +370,12 @@ pub struct WorktreeSnapshot { pub readme: Option>, /// Branch HEAD points to (`None` when detached, e.g. on a tag). pub current_branch: Option, + /// Commit HEAD points to, if any (see [`head_commit`]). + pub head_commit: Option, } -/// Snapshot the worktree after a branch/tag switch: entries, README and the -/// branch HEAD points to, opening the repository once. +/// Snapshot the worktree after a branch/tag switch: entries, README, the +/// branch HEAD points to and its commit, opening the repository once. pub fn worktree_snapshot(workdir: &Path) -> Result { let repo = gix::open(workdir)?; let readme_path = find_readme(&repo)?; @@ -369,6 +388,7 @@ pub fn worktree_snapshot(workdir: &Path) -> Result { readme_path, readme, current_branch: current_branch(&repo)?, + head_commit: head_commit(&repo)?, }) } @@ -672,6 +692,23 @@ mod tests { 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] fn worktree_branches_and_tags_list_short_names() { let (dir, repo) = fixture(&[("a.txt", b"one")]); @@ -743,6 +780,10 @@ mod tests { let snapshot = worktree_snapshot(dir).expect("snapshot"); assert_eq!(snapshot.current_branch.as_deref(), Some("feature")); + assert_eq!( + snapshot.head_commit.as_ref().expect("head commit").summary, + "feature work" + ); assert_eq!( String::from_utf8(snapshot.readme.expect("readme")).expect("utf8"), "# feature" @@ -763,6 +804,10 @@ mod tests { let snapshot = worktree_snapshot(dir).expect("snapshot"); 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!( String::from_utf8(snapshot.readme.expect("readme")).expect("utf8"), "# main" diff --git a/crates/workspace/src/views/repo_detail/mod.rs b/crates/workspace/src/views/repo_detail/mod.rs index 3e5d130..038e98a 100644 --- a/crates/workspace/src/views/repo_detail/mod.rs +++ b/crates/workspace/src/views/repo_detail/mod.rs @@ -79,6 +79,8 @@ pub struct RepoDetailView { /// A clone/fetch is in flight. loading: bool, error: Option, + /// Commit HEAD currently points to, shown in the header button. + head_commit: Option, /// Branch selector (header): local branches, searchable. branch_select: Entity>>, /// Tag selector (header): tags, searchable. @@ -164,6 +166,7 @@ impl RepoDetailView { item_sizes: Rc::new(Vec::new()), loading: true, error: None, + head_commit: None, branch_select, tag_select, switching_ref: false, @@ -203,6 +206,7 @@ impl RepoDetailView { ), None => (Vec::new(), Vec::new(), None), }; + let head_commit = signed_git::head_commit(&repo).unwrap_or(None); Ok::<_, Error>(( entries, @@ -212,6 +216,7 @@ impl RepoDetailView { branches, tags, current_branch, + head_commit, )) }); @@ -228,8 +233,10 @@ impl RepoDetailView { branches, tags, current_branch, + head_commit, )) => { this.worktree = Some(worktree); + this.head_commit = head_commit; this.tree_state.update(cx, |state, 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()); } Err(error) => { @@ -589,6 +596,7 @@ impl RepoDetailView { this.switching_ref = false; match result { Ok(snapshot) => { + this.head_commit = snapshot.head_commit; // Rebuild the tree from scratch: entries of the // previous branch are gone, and with them the // expansion state. @@ -619,6 +627,7 @@ impl RepoDetailView { } Err(error) => { this.error = Some(error.to_string().into()); + this.head_commit = None; // The tree may show files that no longer exist. this.tree_state.update(cx, |state, 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() + }), + ), ), ), ),