Add file history view (#42441)
Closes #16827 Release Notes: - Added: File history view accessible via right-click context menu on files in the editor or project panel. Shows commit history for the selected file with author, timestamp, and commit message. Clicking a commit opens a diff view filtered to show only changes for that specific file. <img width="1293" height="834" alt="Screenshot 2025-11-11 at 16 31 32" src="https://github.com/user-attachments/assets/3780d21b-a719-40b3-955c-d928c45a47cc" /> <img width="1283" height="836" alt="Screenshot 2025-11-11 at 16 31 24" src="https://github.com/user-attachments/assets/1dc4e56b-b225-4ffa-a2af-c5dcfb2efaa0" /> --------- Co-authored-by: cameron <cameron.studdstreet@gmail.com>
This commit is contained in:
@@ -43,6 +43,8 @@ actions!(
|
||||
/// Shows git blame information for the current file.
|
||||
#[action(deprecated_aliases = ["editor::ToggleGitBlame"])]
|
||||
Blame,
|
||||
/// Shows the git history for the current file.
|
||||
FileHistory,
|
||||
/// Stages the current file.
|
||||
StageFile,
|
||||
/// Unstages the current file.
|
||||
|
||||
@@ -207,6 +207,22 @@ pub struct CommitDetails {
|
||||
pub author_name: SharedString,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
|
||||
pub struct FileHistoryEntry {
|
||||
pub sha: SharedString,
|
||||
pub subject: SharedString,
|
||||
pub message: SharedString,
|
||||
pub commit_timestamp: i64,
|
||||
pub author_name: SharedString,
|
||||
pub author_email: SharedString,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FileHistory {
|
||||
pub entries: Vec<FileHistoryEntry>,
|
||||
pub path: RepoPath,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CommitDiff {
|
||||
pub files: Vec<CommitFile>,
|
||||
@@ -464,6 +480,13 @@ pub trait GitRepository: Send + Sync {
|
||||
|
||||
fn load_commit(&self, commit: String, cx: AsyncApp) -> BoxFuture<'_, Result<CommitDiff>>;
|
||||
fn blame(&self, path: RepoPath, content: Rope) -> BoxFuture<'_, Result<crate::blame::Blame>>;
|
||||
fn file_history(&self, path: RepoPath) -> BoxFuture<'_, Result<FileHistory>>;
|
||||
fn file_history_paginated(
|
||||
&self,
|
||||
path: RepoPath,
|
||||
skip: usize,
|
||||
limit: Option<usize>,
|
||||
) -> BoxFuture<'_, Result<FileHistory>>;
|
||||
|
||||
/// Returns the absolute path to the repository. For worktrees, this will be the path to the
|
||||
/// worktree's gitdir within the main repository (typically `.git/worktrees/<name>`).
|
||||
@@ -1452,6 +1475,94 @@ impl GitRepository for RealGitRepository {
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn file_history(&self, path: RepoPath) -> BoxFuture<'_, Result<FileHistory>> {
|
||||
self.file_history_paginated(path, 0, None)
|
||||
}
|
||||
|
||||
fn file_history_paginated(
|
||||
&self,
|
||||
path: RepoPath,
|
||||
skip: usize,
|
||||
limit: Option<usize>,
|
||||
) -> BoxFuture<'_, Result<FileHistory>> {
|
||||
let working_directory = self.working_directory();
|
||||
let git_binary_path = self.any_git_binary_path.clone();
|
||||
self.executor
|
||||
.spawn(async move {
|
||||
let working_directory = working_directory?;
|
||||
// Use a unique delimiter with a hardcoded UUID to separate commits
|
||||
// This essentially eliminates any chance of encountering the delimiter in actual commit data
|
||||
let commit_delimiter =
|
||||
concat!("<<COMMIT_END-", "3f8a9c2e-7d4b-4e1a-9f6c-8b5d2a1e4c3f>>",);
|
||||
|
||||
let format_string = format!(
|
||||
"--pretty=format:%H%x00%s%x00%B%x00%at%x00%an%x00%ae{}",
|
||||
commit_delimiter
|
||||
);
|
||||
|
||||
let mut args = vec!["--no-optional-locks", "log", "--follow", &format_string];
|
||||
|
||||
let skip_str;
|
||||
let limit_str;
|
||||
if skip > 0 {
|
||||
skip_str = skip.to_string();
|
||||
args.push("--skip");
|
||||
args.push(&skip_str);
|
||||
}
|
||||
if let Some(n) = limit {
|
||||
limit_str = n.to_string();
|
||||
args.push("-n");
|
||||
args.push(&limit_str);
|
||||
}
|
||||
|
||||
args.push("--");
|
||||
|
||||
let output = new_smol_command(&git_binary_path)
|
||||
.current_dir(&working_directory)
|
||||
.args(&args)
|
||||
.arg(path.as_unix_str())
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
bail!("git log failed: {stderr}");
|
||||
}
|
||||
|
||||
let stdout = std::str::from_utf8(&output.stdout)?;
|
||||
let mut entries = Vec::new();
|
||||
|
||||
for commit_block in stdout.split(commit_delimiter) {
|
||||
let commit_block = commit_block.trim();
|
||||
if commit_block.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let fields: Vec<&str> = commit_block.split('\0').collect();
|
||||
if fields.len() >= 6 {
|
||||
let sha = fields[0].trim().to_string().into();
|
||||
let subject = fields[1].trim().to_string().into();
|
||||
let message = fields[2].trim().to_string().into();
|
||||
let commit_timestamp = fields[3].trim().parse().unwrap_or(0);
|
||||
let author_name = fields[4].trim().to_string().into();
|
||||
let author_email = fields[5].trim().to_string().into();
|
||||
|
||||
entries.push(FileHistoryEntry {
|
||||
sha,
|
||||
subject,
|
||||
message,
|
||||
commit_timestamp,
|
||||
author_name,
|
||||
author_email,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(FileHistory { entries, path })
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn diff(&self, diff: DiffType) -> BoxFuture<'_, Result<String>> {
|
||||
let working_directory = self.working_directory();
|
||||
let git_binary_path = self.any_git_binary_path.clone();
|
||||
|
||||
Reference in New Issue
Block a user