refactor: extract into a blame module

This commit is contained in:
Nik Revenco 2025-03-19 15:43:07 +00:00
parent 9278a68186
commit 452da44dc7
4 changed files with 130 additions and 90 deletions

View file

@ -41,23 +41,14 @@ impl helix_event::AsyncHook for BlameHandler {
// convert 0-based line numbers into 1-based line numbers
let cursor_line = cursor_line + 1;
// the line for which we compute the blame
// Because gix_blame doesn't care about stuff that is not commited, we have to "normalize" the
// line number to account for uncommited code.
//
// You'll notice that blame_line can be 0 when, for instance we have:
// - removed 0 lines
// - added 10 lines
// - cursor_line is 8
//
// So when our cursor is on the 10th added line or earlier, blame_line will be 0. This means
// the blame will be incorrect. But that's fine, because when the cursor_line is on some hunk,
// we can show to the user nothing at all
let blame_line = cursor_line.saturating_sub(added_lines_count) + removed_lines_count;
let worker = tokio::spawn(async move {
diff_providers
.blame_line(&file, blame_line)
.blame(
&file,
cursor_line..cursor_line,
added_lines_count,
removed_lines_count,
)
.map(|s| s.to_string())
});
self.worker = Some(worker);

View file

@ -1,12 +1,11 @@
use anyhow::{bail, Context, Result};
use arc_swap::ArcSwap;
use core::fmt;
use gix::bstr::ByteSlice as _;
use gix::filter::plumbing::driver::apply::Delay;
use std::io::Read;
use std::path::Path;
use std::sync::Arc;
use gix::bstr::{BStr, ByteSlice};
use gix::diff::Rewrites;
use gix::dir::entry::Status;
use gix::objs::tree::EntryKind;
@ -23,6 +22,9 @@ use crate::FileChange;
#[cfg(test)]
mod test;
mod blame;
pub use blame::*;
#[inline]
fn get_repo_dir(file: &Path) -> Result<&Path> {
file.parent().context("file has no parent directory")
@ -126,73 +128,6 @@ 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,
}
impl fmt::Display for BlameInformation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}, {} • {} • {}",
self.author_name, self.commit_date, self.commit_message, self.commit_hash
)
}
}
/// Emulates the result of running `git blame` from the command line.
pub fn blame_line(file: &Path, line: 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 relative_path = file
.strip_prefix(
repo.path()
.parent()
.context("Could not get parent path of repository")?,
)
.unwrap_or(file)
.to_str()
.context("Could not convert path to string")?;
let traverse_all_commits = 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_all_commits,
&mut resource_cache,
BStr::new(relative_path),
Some(line..line.saturating_add(0)),
)?
.entries
.first()
.context("No commits found")?
.commit_id;
let commit = repo.find_commit(latest_commit_id)?;
let author = commit.author()?;
Ok(BlameInformation {
commit_hash: commit.short_id()?.to_string(),
author_name: author.name.to_string(),
commit_date: author.time.format(gix::date::time::format::SHORT),
commit_message: commit.message()?.title.to_string(),
})
}
/// 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

@ -0,0 +1,93 @@
use anyhow::Context as _;
use core::fmt;
use gix::bstr::BStr;
use std::{ops::Range, path::Path};
use super::{get_repo_dir, open_repo};
pub struct BlameInformation {
pub commit_hash: String,
pub author_name: String,
pub commit_date: String,
pub commit_message: String,
}
impl fmt::Display for BlameInformation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}, {} • {} • {}",
self.author_name, self.commit_date, self.commit_message, self.commit_hash
)
}
}
/// `git blame` a range in a file
pub fn blame(
file: &Path,
range: Range<u32>,
added_lines_count: u32,
removed_lines_count: u32,
) -> anyhow::Result<BlameInformation> {
// Because gix_blame doesn't care about stuff that is not commited, we have to "normalize" the
// line number to account for uncommited code.
//
// You'll notice that blame_line can be 0 when, for instance we have:
// - removed 0 lines
// - added 10 lines
// - cursor_line is 8
//
// So when our cursor is on the 10th added line or earlier, blame_line will be 0. This means
// the blame will be incorrect. But that's fine, because when the cursor_line is on some hunk,
// we can show to the user nothing at all
let normalize = |line: u32| line.saturating_sub(added_lines_count) + removed_lines_count;
let blame_range = normalize(range.start)..normalize(range.end);
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 relative_path = file
.strip_prefix(
repo.path()
.parent()
.context("Could not get parent path of repository")?,
)
.unwrap_or(file)
.to_str()
.context("Could not convert path to string")?;
let traverse_all_commits = 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_all_commits,
&mut resource_cache,
BStr::new(relative_path),
Some(blame_range),
)?
.entries
.first()
.context("No commits found")?
.commit_id;
let commit = repo.find_commit(latest_commit_id)?;
let author = commit.author()?;
Ok(BlameInformation {
commit_hash: commit.short_id()?.to_string(),
author_name: author.name.to_string(),
commit_date: author.time.format(gix::date::time::format::SHORT),
commit_message: commit.message()?.title.to_string(),
})
}

View file

@ -1,6 +1,8 @@
use anyhow::{anyhow, bail, Context, Result};
use anyhow::Context as _;
use anyhow::{anyhow, bail, Result};
use arc_swap::ArcSwap;
use git::BlameInformation;
use std::ops::Range;
use std::{
path::{Path, PathBuf},
sync::Arc,
@ -49,11 +51,24 @@ impl DiffProviderRegistry {
})
}
/// Blame line in a file. Lines are 1-indexed
pub fn blame_line(&self, file: &Path, line: u32) -> anyhow::Result<BlameInformation> {
/// Blame range of lines in a file. Lines are 1-indexed
pub fn blame(
&self,
file: &Path,
range: Range<u32>,
added_lines_count: u32,
removed_lines_count: u32,
) -> anyhow::Result<BlameInformation> {
self.providers
.iter()
.map(|provider| provider.blame_line(file, line))
.map(|provider| {
provider.blame(
file,
range.start..range.end,
added_lines_count,
removed_lines_count,
)
})
.next()
.context("No provider found")?
}
@ -118,10 +133,16 @@ impl DiffProvider {
}
}
fn blame_line(&self, file: &Path, line: u32) -> Result<BlameInformation> {
fn blame(
&self,
file: &Path,
range: Range<u32>,
added_lines_count: u32,
removed_lines_count: u32,
) -> Result<BlameInformation> {
match self {
#[cfg(feature = "git")]
Self::Git => git::blame_line(file, line),
Self::Git => git::blame(file, range, added_lines_count, removed_lines_count),
Self::None => bail!("No blame support compiled in"),
}
}