From 4b6690fc0a5a500d96ff00ac1731480bd214399b Mon Sep 17 00:00:00 2001 From: Nik Revenco <154856872+NikitaRevenco@users.noreply.github.com> Date: Tue, 18 Mar 2025 01:11:57 +0000 Subject: [PATCH] feat: implement `blame` command in `helix-vcs` --- Cargo.lock | 16 +++++++++++++ helix-vcs/Cargo.toml | 2 +- helix-vcs/src/git.rs | 54 +++++++++++++++++++++++++++++++++++++++++++- helix-vcs/src/lib.rs | 15 ++++++++++++ 4 files changed, 85 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e731eeaf..260c516e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/helix-vcs/Cargo.toml b/helix-vcs/Cargo.toml index 289c334a..905e1247 100644 --- a/helix-vcs/Cargo.toml +++ b/helix-vcs/Cargo.toml @@ -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" diff --git a/helix-vcs/src/git.rs b/helix-vcs/src/git.rs index b8ddd79f..c395bd1d 100644 --- a/helix-vcs/src/git.rs +++ b/helix-vcs/src/git.rs @@ -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 diff --git a/helix-vcs/src/lib.rs b/helix-vcs/src/lib.rs index 539be779..a4782718 100644 --- a/helix-vcs/src/lib.rs +++ b/helix-vcs/src/lib.rs @@ -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,