feat: implement blame command in helix-vcs

This commit is contained in:
Nik Revenco 2025-03-18 01:11:57 +00:00
parent b38eae1f98
commit 4b6690fc0a
4 changed files with 85 additions and 2 deletions

16
Cargo.lock generated
View file

@ -509,6 +509,7 @@ checksum = "736f14636705f3a56ea52b553e67282519418d9a35bb1e90b3a9637a00296b68"
dependencies = [
"gix-actor",
"gix-attributes",
"gix-blame",
"gix-command",
"gix-commitgraph",
"gix-config",
@ -591,6 +592,21 @@ dependencies = [
"thiserror 2.0.12",
]
[[package]]
name = "gix-blame"
version = "0.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adc795e239a2347eb50ed18b8c529382dd8b62439c57277f79af3d8f8928a986"
dependencies = [
"gix-diff",
"gix-hash",
"gix-object",
"gix-trace",
"gix-traverse",
"gix-worktree",
"thiserror 2.0.12",
]
[[package]]
name = "gix-chunk"
version = "0.4.11"

View file

@ -19,7 +19,7 @@ tokio = { version = "1", features = ["rt", "rt-multi-thread", "time", "sync", "p
parking_lot = "0.12"
arc-swap = { version = "1.7.1" }
gix = { version = "0.70.0", features = ["attributes", "status"], default-features = false, optional = true }
gix = { version = "0.70.0", features = ["attributes", "status", "blame"], default-features = false, optional = true }
imara-diff = "0.1.8"
anyhow = "1"

View file

@ -2,10 +2,11 @@ use anyhow::{bail, Context, Result};
use arc_swap::ArcSwap;
use gix::filter::plumbing::driver::apply::Delay;
use std::io::Read;
use std::os::unix::ffi::OsStrExt;
use std::path::Path;
use std::sync::Arc;
use gix::bstr::ByteSlice;
use gix::bstr::{BStr, ByteSlice};
use gix::diff::Rewrites;
use gix::dir::entry::Status;
use gix::objs::tree::EntryKind;
@ -125,6 +126,57 @@ fn open_repo(path: &Path) -> Result<ThreadSafeRepository> {
Ok(res)
}
pub struct BlameInformation {
pub commit_hash: String,
pub author_name: String,
pub commit_date: String,
pub commit_message: String,
}
/// Emulates the result of running `git blame` from the command line.
pub fn blame(file: &Path, range: std::ops::Range<u32>) -> Result<BlameInformation> {
let repo_dir = get_repo_dir(file)?;
let repo = open_repo(repo_dir)
.context("failed to open git repo")?
.to_thread_local();
let suspect = repo.head()?.peel_to_commit_in_place()?;
let traverse = gix::traverse::commit::topo::Builder::from_iters(
&repo.objects,
[suspect.id],
None::<Vec<gix::ObjectId>>,
)
.build()?;
let mut resource_cache = repo.diff_resource_cache_for_tree_diff()?;
let latest_commit_id = gix::blame::file(
&repo.objects,
traverse,
&mut resource_cache,
BStr::new(file.as_os_str().as_bytes()),
Some(range),
)?
.entries
.first()
.context("No commits found")?
.commit_id;
let commit = repo.find_commit(latest_commit_id)?;
let author = commit.author()?;
let commit_date = author.time.format(gix::date::time::format::SHORT);
let author_name = author.name.to_string();
let commit_hash = commit.short_id()?.to_string();
let commit_message = commit.message()?.title.to_string();
Ok(BlameInformation {
commit_hash,
author_name,
commit_date,
commit_message,
})
}
/// Emulates the result of running `git status` from the command line.
fn status(repo: &Repository, f: impl Fn(Result<FileChange>) -> bool) -> Result<()> {
let work_dir = repo

View file

@ -1,5 +1,6 @@
use anyhow::{anyhow, bail, Result};
use arc_swap::ArcSwap;
use git::BlameInformation;
use std::{
path::{Path, PathBuf},
sync::Arc,
@ -48,6 +49,12 @@ impl DiffProviderRegistry {
})
}
pub fn blame(&self, file: &Path, range: std::ops::Range<u32>) -> Option<BlameInformation> {
self.providers
.iter()
.find_map(|provider| provider.blame(file, range.clone()).ok())
}
/// Fire-and-forget changed file iteration. Runs everything in a background task. Keeps
/// iteration until `on_change` returns `false`.
pub fn for_each_changed_file(
@ -108,6 +115,14 @@ impl DiffProvider {
}
}
fn blame(&self, file: &Path, range: std::ops::Range<u32>) -> Result<BlameInformation> {
match self {
#[cfg(feature = "git")]
Self::Git => git::blame(file, range),
Self::None => bail!("No blame support compiled in"),
}
}
fn for_each_changed_file(
&self,
cwd: &Path,