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 16:26:47 -07:00 committed by GitHub
parent 33912011b7
commit 8546dc101d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 1742 additions and 603 deletions

View file

@ -21,8 +21,8 @@ use git::{
blame::Blame,
parse_git_remote_url,
repository::{
Branch, CommitDetails, DiffType, GitRepository, GitRepositoryCheckpoint, PushOptions,
Remote, RemoteCommandOutput, RepoPath, ResetMode,
Branch, CommitDetails, CommitDiff, CommitFile, DiffType, GitRepository,
GitRepositoryCheckpoint, PushOptions, Remote, RemoteCommandOutput, RepoPath, ResetMode,
},
status::FileStatus,
};
@ -289,6 +289,7 @@ impl GitStore {
client.add_entity_request_handler(Self::handle_commit);
client.add_entity_request_handler(Self::handle_reset);
client.add_entity_request_handler(Self::handle_show);
client.add_entity_request_handler(Self::handle_load_commit_diff);
client.add_entity_request_handler(Self::handle_checkout_files);
client.add_entity_request_handler(Self::handle_open_commit_message_buffer);
client.add_entity_request_handler(Self::handle_set_index_text);
@ -1885,6 +1886,32 @@ impl GitStore {
})
}
async fn handle_load_commit_diff(
this: Entity<Self>,
envelope: TypedEnvelope<proto::LoadCommitDiff>,
mut cx: AsyncApp,
) -> Result<proto::LoadCommitDiffResponse> {
let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
let repository_handle = Self::repository_for_request(&this, work_directory_id, &mut cx)?;
let commit_diff = repository_handle
.update(&mut cx, |repository_handle, _| {
repository_handle.load_commit_diff(envelope.payload.commit)
})?
.await??;
Ok(proto::LoadCommitDiffResponse {
files: commit_diff
.files
.into_iter()
.map(|file| proto::CommitFile {
path: file.path.to_string(),
old_text: file.old_text,
new_text: file.new_text,
})
.collect(),
})
}
async fn handle_reset(
this: Entity<Self>,
envelope: TypedEnvelope<proto::GitReset>,
@ -2389,7 +2416,10 @@ impl BufferDiffState {
unstaged_diff.as_ref().zip(new_unstaged_diff.clone())
{
unstaged_diff.update(cx, |diff, cx| {
diff.set_snapshot(&buffer, new_unstaged_diff, language_changed, None, cx)
if language_changed {
diff.language_changed(cx);
}
diff.set_snapshot(new_unstaged_diff, &buffer, None, cx)
})?
} else {
None
@ -2398,14 +2428,11 @@ impl BufferDiffState {
if let Some((uncommitted_diff, new_uncommitted_diff)) =
uncommitted_diff.as_ref().zip(new_uncommitted_diff.clone())
{
uncommitted_diff.update(cx, |uncommitted_diff, cx| {
uncommitted_diff.set_snapshot(
&buffer,
new_uncommitted_diff,
language_changed,
unstaged_changed_range,
cx,
);
uncommitted_diff.update(cx, |diff, cx| {
if language_changed {
diff.language_changed(cx);
}
diff.set_snapshot(new_uncommitted_diff, &buffer, unstaged_changed_range, cx);
})?;
}
@ -2869,6 +2896,40 @@ impl Repository {
})
}
pub fn load_commit_diff(&self, commit: String) -> oneshot::Receiver<Result<CommitDiff>> {
self.send_job(|git_repo, cx| async move {
match git_repo {
RepositoryState::Local(git_repository) => {
git_repository.load_commit(commit, cx).await
}
RepositoryState::Remote {
client,
project_id,
work_directory_id,
} => {
let response = client
.request(proto::LoadCommitDiff {
project_id: project_id.0,
work_directory_id: work_directory_id.to_proto(),
commit,
})
.await?;
Ok(CommitDiff {
files: response
.files
.into_iter()
.map(|file| CommitFile {
path: PathBuf::from(file.path).into(),
old_text: file.old_text,
new_text: file.new_text,
})
.collect(),
})
}
}
})
}
fn buffer_store(&self, cx: &App) -> Option<Entity<BufferStore>> {
Some(self.git_store.upgrade()?.read(cx).buffer_store.clone())
}