Allow viewing past commits in Zed (#27636)

This PR adds functionality for loading the diff for an arbitrary git
commit, and displaying it in a tab. To retrieve the diff for the commit,
I'm using a single `git cat-file --batch` invocation to efficiently load
both the old and new versions of each file that was changed in the
commit.

Todo

* Features
* [x] Open the commit view when clicking the most recent commit message
in the commit panel
  * [x] Open the commit view when clicking a SHA in a git blame column
  * [x] Open the commit view when clicking a SHA in a commit tooltip
  * [x] Make it work over RPC
  * [x] Allow buffer search in commit view
* [x] Command palette action to open the commit for the current blame
line
* Styling
* [x] Add a header that shows the author, timestamp, and the full commit
message
  * [x] Remove stage/unstage buttons in commit view
  * [x] Truncate the commit message in the tab
* Bugs
  * [x] Dedup commit tabs within a pane
  * [x] Add a tooltip to the tab

Release Notes:

- Added the ability to show past commits in Zed. You can view the most
recent commit by clicking its message in the commit panel. And when
viewing a git blame, you can show any commit by clicking its sha.
This commit is contained in:
Max Brunsfeld
2025-03-31 23:26:47 +00:00
committed by GitHub
parent 33912011b7
commit 8546dc101d
28 changed files with 1742 additions and 603 deletions
+104 -44
View File
@@ -1,22 +1,22 @@
use crate::Editor;
use anyhow::Result;
use collections::HashMap;
use git::{
GitHostingProvider, GitHostingProviderRegistry, Oid,
blame::{Blame, BlameEntry},
GitHostingProviderRegistry, GitRemote, Oid,
blame::{Blame, BlameEntry, ParsedCommitMessage},
parse_git_remote_url,
};
use gpui::{App, AppContext as _, Context, Entity, Subscription, Task};
use http_client::HttpClient;
use gpui::{
AnyElement, App, AppContext as _, Context, Entity, Hsla, Subscription, Task, TextStyle,
WeakEntity, Window,
};
use language::{Bias, Buffer, BufferSnapshot, Edit};
use multi_buffer::RowInfo;
use project::{Project, ProjectItem};
use project::{Project, ProjectItem, git_store::Repository};
use smallvec::SmallVec;
use std::{sync::Arc, time::Duration};
use sum_tree::SumTree;
use ui::SharedString;
use url::Url;
use crate::commit_tooltip::ParsedCommitMessage;
use workspace::Workspace;
#[derive(Clone, Debug, Default)]
pub struct GitBlameEntry {
@@ -59,45 +59,11 @@ impl<'a> sum_tree::Dimension<'a, GitBlameEntrySummary> for u32 {
}
}
#[derive(Clone)]
pub struct GitRemote {
pub host: Arc<dyn GitHostingProvider + Send + Sync + 'static>,
pub owner: String,
pub repo: String,
}
impl std::fmt::Debug for GitRemote {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("GitRemote")
.field("host", &self.host.name())
.field("owner", &self.owner)
.field("repo", &self.repo)
.finish()
}
}
impl GitRemote {
pub fn host_supports_avatars(&self) -> bool {
self.host.supports_avatars()
}
pub async fn avatar_url(
&self,
commit: SharedString,
client: Arc<dyn HttpClient>,
) -> Option<Url> {
self.host
.commit_author_avatar_url(&self.owner, &self.repo, commit, client)
.await
.ok()
.flatten()
}
}
pub struct GitBlame {
project: Entity<Project>,
buffer: Entity<Buffer>,
entries: SumTree<GitBlameEntry>,
commit_details: HashMap<Oid, crate::commit_tooltip::ParsedCommitMessage>,
commit_details: HashMap<Oid, ParsedCommitMessage>,
buffer_snapshot: BufferSnapshot,
buffer_edits: text::Subscription,
task: Task<Result<()>>,
@@ -109,6 +75,91 @@ pub struct GitBlame {
_regenerate_subscriptions: Vec<Subscription>,
}
pub trait BlameRenderer {
fn max_author_length(&self) -> usize;
fn render_blame_entry(
&self,
_: &TextStyle,
_: BlameEntry,
_: Option<ParsedCommitMessage>,
_: Entity<Repository>,
_: WeakEntity<Workspace>,
_: Entity<Editor>,
_: usize,
_: Hsla,
_: &mut App,
) -> Option<AnyElement>;
fn render_inline_blame_entry(
&self,
_: &TextStyle,
_: BlameEntry,
_: Option<ParsedCommitMessage>,
_: Entity<Repository>,
_: WeakEntity<Workspace>,
_: Entity<Editor>,
_: &mut App,
) -> Option<AnyElement>;
fn open_blame_commit(
&self,
_: BlameEntry,
_: Entity<Repository>,
_: WeakEntity<Workspace>,
_: &mut Window,
_: &mut App,
);
}
impl BlameRenderer for () {
fn max_author_length(&self) -> usize {
0
}
fn render_blame_entry(
&self,
_: &TextStyle,
_: BlameEntry,
_: Option<ParsedCommitMessage>,
_: Entity<Repository>,
_: WeakEntity<Workspace>,
_: Entity<Editor>,
_: usize,
_: Hsla,
_: &mut App,
) -> Option<AnyElement> {
None
}
fn render_inline_blame_entry(
&self,
_: &TextStyle,
_: BlameEntry,
_: Option<ParsedCommitMessage>,
_: Entity<Repository>,
_: WeakEntity<Workspace>,
_: Entity<Editor>,
_: &mut App,
) -> Option<AnyElement> {
None
}
fn open_blame_commit(
&self,
_: BlameEntry,
_: Entity<Repository>,
_: WeakEntity<Workspace>,
_: &mut Window,
_: &mut App,
) {
}
}
pub(crate) struct GlobalBlameRenderer(pub Arc<dyn BlameRenderer>);
impl gpui::Global for GlobalBlameRenderer {}
impl GitBlame {
pub fn new(
buffer: Entity<Buffer>,
@@ -181,6 +232,15 @@ impl GitBlame {
this
}
pub fn repository(&self, cx: &App) -> Option<Entity<Repository>> {
self.project
.read(cx)
.git_store()
.read(cx)
.repository_and_path_for_buffer_id(self.buffer.read(cx).remote_id(), cx)
.map(|(repo, _)| repo)
}
pub fn has_generated_entries(&self) -> bool {
self.generated
}