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:
parent
33912011b7
commit
8546dc101d
28 changed files with 1742 additions and 603 deletions
|
@ -1,18 +1,18 @@
|
|||
use crate::status::GitStatus;
|
||||
use crate::commit::parse_git_diff_name_status;
|
||||
use crate::status::{GitStatus, StatusCode};
|
||||
use crate::{Oid, SHORT_SHA_LENGTH};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use collections::HashMap;
|
||||
use futures::future::BoxFuture;
|
||||
use futures::{AsyncWriteExt, FutureExt as _, select_biased};
|
||||
use git2::BranchType;
|
||||
use gpui::{AsyncApp, BackgroundExecutor, SharedString};
|
||||
use gpui::{AppContext as _, AsyncApp, BackgroundExecutor, SharedString};
|
||||
use parking_lot::Mutex;
|
||||
use rope::Rope;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use std::borrow::{Borrow, Cow};
|
||||
use std::ffi::{OsStr, OsString};
|
||||
use std::future;
|
||||
use std::path::Component;
|
||||
use std::process::{ExitStatus, Stdio};
|
||||
use std::sync::LazyLock;
|
||||
|
@ -21,6 +21,10 @@ use std::{
|
|||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use std::{
|
||||
future,
|
||||
io::{BufRead, BufReader, BufWriter, Read},
|
||||
};
|
||||
use sum_tree::MapSeekTarget;
|
||||
use thiserror::Error;
|
||||
use util::ResultExt;
|
||||
|
@ -133,6 +137,18 @@ pub struct CommitDetails {
|
|||
pub committer_name: SharedString,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CommitDiff {
|
||||
pub files: Vec<CommitFile>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CommitFile {
|
||||
pub path: RepoPath,
|
||||
pub old_text: Option<String>,
|
||||
pub new_text: Option<String>,
|
||||
}
|
||||
|
||||
impl CommitDetails {
|
||||
pub fn short_sha(&self) -> SharedString {
|
||||
self.sha[..SHORT_SHA_LENGTH].to_string().into()
|
||||
|
@ -206,6 +222,7 @@ pub trait GitRepository: Send + Sync {
|
|||
|
||||
fn show(&self, commit: String) -> BoxFuture<Result<CommitDetails>>;
|
||||
|
||||
fn load_commit(&self, commit: String, cx: AsyncApp) -> BoxFuture<Result<CommitDiff>>;
|
||||
fn blame(&self, path: RepoPath, content: Rope) -> BoxFuture<Result<crate::blame::Blame>>;
|
||||
|
||||
/// Returns the absolute path to the repository. For worktrees, this will be the path to the
|
||||
|
@ -405,6 +422,108 @@ impl GitRepository for RealGitRepository {
|
|||
.boxed()
|
||||
}
|
||||
|
||||
fn load_commit(&self, commit: String, cx: AsyncApp) -> BoxFuture<Result<CommitDiff>> {
|
||||
let Some(working_directory) = self.repository.lock().workdir().map(ToOwned::to_owned)
|
||||
else {
|
||||
return future::ready(Err(anyhow!("no working directory"))).boxed();
|
||||
};
|
||||
cx.background_spawn(async move {
|
||||
let show_output = util::command::new_std_command("git")
|
||||
.current_dir(&working_directory)
|
||||
.args([
|
||||
"--no-optional-locks",
|
||||
"show",
|
||||
"--format=%P",
|
||||
"-z",
|
||||
"--no-renames",
|
||||
"--name-status",
|
||||
])
|
||||
.arg(&commit)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.output()
|
||||
.map_err(|e| anyhow!("Failed to start git show process: {e}"))?;
|
||||
|
||||
let show_stdout = String::from_utf8_lossy(&show_output.stdout);
|
||||
let mut lines = show_stdout.split('\n');
|
||||
let parent_sha = lines.next().unwrap().trim().trim_end_matches('\0');
|
||||
let changes = parse_git_diff_name_status(lines.next().unwrap_or(""));
|
||||
|
||||
let mut cat_file_process = util::command::new_std_command("git")
|
||||
.current_dir(&working_directory)
|
||||
.args(["--no-optional-locks", "cat-file", "--batch=%(objectsize)"])
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.map_err(|e| anyhow!("Failed to start git cat-file process: {e}"))?;
|
||||
|
||||
use std::io::Write as _;
|
||||
let mut files = Vec::<CommitFile>::new();
|
||||
let mut stdin = BufWriter::with_capacity(512, cat_file_process.stdin.take().unwrap());
|
||||
let mut stdout = BufReader::new(cat_file_process.stdout.take().unwrap());
|
||||
let mut info_line = String::new();
|
||||
let mut newline = [b'\0'];
|
||||
for (path, status_code) in changes {
|
||||
match status_code {
|
||||
StatusCode::Modified => {
|
||||
writeln!(&mut stdin, "{commit}:{}", path.display())?;
|
||||
writeln!(&mut stdin, "{parent_sha}:{}", path.display())?;
|
||||
}
|
||||
StatusCode::Added => {
|
||||
writeln!(&mut stdin, "{commit}:{}", path.display())?;
|
||||
}
|
||||
StatusCode::Deleted => {
|
||||
writeln!(&mut stdin, "{parent_sha}:{}", path.display())?;
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
stdin.flush()?;
|
||||
|
||||
info_line.clear();
|
||||
stdout.read_line(&mut info_line)?;
|
||||
|
||||
let len = info_line.trim_end().parse().with_context(|| {
|
||||
format!("invalid object size output from cat-file {info_line}")
|
||||
})?;
|
||||
let mut text = vec![0; len];
|
||||
stdout.read_exact(&mut text)?;
|
||||
stdout.read_exact(&mut newline)?;
|
||||
let text = String::from_utf8_lossy(&text).to_string();
|
||||
|
||||
let mut old_text = None;
|
||||
let mut new_text = None;
|
||||
match status_code {
|
||||
StatusCode::Modified => {
|
||||
info_line.clear();
|
||||
stdout.read_line(&mut info_line)?;
|
||||
let len = info_line.trim_end().parse().with_context(|| {
|
||||
format!("invalid object size output from cat-file {}", info_line)
|
||||
})?;
|
||||
let mut parent_text = vec![0; len];
|
||||
stdout.read_exact(&mut parent_text)?;
|
||||
stdout.read_exact(&mut newline)?;
|
||||
old_text = Some(String::from_utf8_lossy(&parent_text).to_string());
|
||||
new_text = Some(text);
|
||||
}
|
||||
StatusCode::Added => new_text = Some(text),
|
||||
StatusCode::Deleted => old_text = Some(text),
|
||||
_ => continue,
|
||||
}
|
||||
|
||||
files.push(CommitFile {
|
||||
path: path.into(),
|
||||
old_text,
|
||||
new_text,
|
||||
})
|
||||
}
|
||||
|
||||
Ok(CommitDiff { files })
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn reset(
|
||||
&self,
|
||||
commit: String,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue