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

@ -40,6 +40,7 @@ pub struct BlockMap {
buffer_header_height: u32,
excerpt_header_height: u32,
pub(super) folded_buffers: HashSet<BufferId>,
buffers_with_disabled_headers: HashSet<BufferId>,
}
pub struct BlockMapReader<'a> {
@ -422,6 +423,7 @@ impl BlockMap {
custom_blocks: Vec::new(),
custom_blocks_by_id: TreeMap::default(),
folded_buffers: HashSet::default(),
buffers_with_disabled_headers: HashSet::default(),
transforms: RefCell::new(transforms),
wrap_snapshot: RefCell::new(wrap_snapshot.clone()),
buffer_header_height,
@ -642,11 +644,8 @@ impl BlockMap {
);
if buffer.show_headers() {
blocks_in_edit.extend(BlockMap::header_and_footer_blocks(
self.buffer_header_height,
self.excerpt_header_height,
blocks_in_edit.extend(self.header_and_footer_blocks(
buffer,
&self.folded_buffers,
(start_bound, end_bound),
wrap_snapshot,
));
@ -714,10 +713,8 @@ impl BlockMap {
}
fn header_and_footer_blocks<'a, R, T>(
buffer_header_height: u32,
excerpt_header_height: u32,
&'a self,
buffer: &'a multi_buffer::MultiBufferSnapshot,
folded_buffers: &'a HashSet<BufferId>,
range: R,
wrap_snapshot: &'a WrapSnapshot,
) -> impl Iterator<Item = (BlockPlacement<WrapRow>, Block)> + 'a
@ -728,73 +725,78 @@ impl BlockMap {
let mut boundaries = buffer.excerpt_boundaries_in_range(range).peekable();
std::iter::from_fn(move || {
let excerpt_boundary = boundaries.next()?;
let wrap_row = wrap_snapshot
.make_wrap_point(Point::new(excerpt_boundary.row.0, 0), Bias::Left)
.row();
loop {
let excerpt_boundary = boundaries.next()?;
let wrap_row = wrap_snapshot
.make_wrap_point(Point::new(excerpt_boundary.row.0, 0), Bias::Left)
.row();
let new_buffer_id = match (&excerpt_boundary.prev, &excerpt_boundary.next) {
(None, next) => Some(next.buffer_id),
(Some(prev), next) => {
if prev.buffer_id != next.buffer_id {
Some(next.buffer_id)
} else {
None
}
}
};
let mut height = 0;
if let Some(new_buffer_id) = new_buffer_id {
let first_excerpt = excerpt_boundary.next.clone();
if folded_buffers.contains(&new_buffer_id) {
let mut last_excerpt_end_row = first_excerpt.end_row;
while let Some(next_boundary) = boundaries.peek() {
if next_boundary.next.buffer_id == new_buffer_id {
last_excerpt_end_row = next_boundary.next.end_row;
let new_buffer_id = match (&excerpt_boundary.prev, &excerpt_boundary.next) {
(None, next) => Some(next.buffer_id),
(Some(prev), next) => {
if prev.buffer_id != next.buffer_id {
Some(next.buffer_id)
} else {
break;
None
}
}
};
let mut height = 0;
if let Some(new_buffer_id) = new_buffer_id {
let first_excerpt = excerpt_boundary.next.clone();
if self.buffers_with_disabled_headers.contains(&new_buffer_id) {
continue;
}
if self.folded_buffers.contains(&new_buffer_id) {
let mut last_excerpt_end_row = first_excerpt.end_row;
while let Some(next_boundary) = boundaries.peek() {
if next_boundary.next.buffer_id == new_buffer_id {
last_excerpt_end_row = next_boundary.next.end_row;
} else {
break;
}
boundaries.next();
}
boundaries.next();
let wrap_end_row = wrap_snapshot
.make_wrap_point(
Point::new(
last_excerpt_end_row.0,
buffer.line_len(last_excerpt_end_row),
),
Bias::Right,
)
.row();
return Some((
BlockPlacement::Replace(WrapRow(wrap_row)..=WrapRow(wrap_end_row)),
Block::FoldedBuffer {
height: height + self.buffer_header_height,
first_excerpt,
},
));
}
let wrap_end_row = wrap_snapshot
.make_wrap_point(
Point::new(
last_excerpt_end_row.0,
buffer.line_len(last_excerpt_end_row),
),
Bias::Right,
)
.row();
return Some((
BlockPlacement::Replace(WrapRow(wrap_row)..=WrapRow(wrap_end_row)),
Block::FoldedBuffer {
height: height + buffer_header_height,
first_excerpt,
},
));
}
}
if new_buffer_id.is_some() {
height += buffer_header_height;
} else {
height += excerpt_header_height;
}
if new_buffer_id.is_some() {
height += self.buffer_header_height;
} else {
height += self.excerpt_header_height;
}
Some((
BlockPlacement::Above(WrapRow(wrap_row)),
Block::ExcerptBoundary {
excerpt: excerpt_boundary.next,
height,
starts_new_buffer: new_buffer_id.is_some(),
},
))
return Some((
BlockPlacement::Above(WrapRow(wrap_row)),
Block::ExcerptBoundary {
excerpt: excerpt_boundary.next,
height,
starts_new_buffer: new_buffer_id.is_some(),
},
));
}
})
}
@ -1168,6 +1170,10 @@ impl BlockMapWriter<'_> {
self.remove(blocks_to_remove);
}
pub fn disable_header_for_buffer(&mut self, buffer_id: BufferId) {
self.0.buffers_with_disabled_headers.insert(buffer_id);
}
pub fn fold_buffers(
&mut self,
buffer_ids: impl IntoIterator<Item = BufferId>,
@ -3159,11 +3165,8 @@ mod tests {
}));
// Note that this needs to be synced with the related section in BlockMap::sync
expected_blocks.extend(BlockMap::header_and_footer_blocks(
buffer_start_header_height,
excerpt_header_height,
expected_blocks.extend(block_map.header_and_footer_blocks(
&buffer_snapshot,
&block_map.folded_buffers,
0..,
&wraps_snapshot,
));