diff --git a/gix-blame/src/file/function.rs b/gix-blame/src/file/function.rs index 98ed9790cd5..8d914d3b7ee 100644 --- a/gix-blame/src/file/function.rs +++ b/gix-blame/src/file/function.rs @@ -15,7 +15,7 @@ use crate::{types::BlamePathEntry, BlameEntry, Error, Options, Outcome, Statisti /// Produce a list of consecutive [`BlameEntry`] instances to indicate in which commits the ranges of the file /// at `suspect:` originated in. /// -/// ## Paramters +/// ## Parameters /// /// * `odb` /// - Access to database objects, also for used for diffing. @@ -55,13 +55,13 @@ use crate::{types::BlamePathEntry, BlameEntry, Error, Options, Outcome, Statisti /// /// The algorithm in `libgit2` works by going through parents and keeping a linked list of blame /// suspects. It can be visualized as follows: -// -// <----------------------------------------> -// <---------------><-----------------------> -// <---><----------><-----------------------> -// <---><----------><-------><-----><-------> -// <---><---><-----><-------><-----><-------> -// <---><---><-----><-------><-----><-><-><-> +/// +/// <----------------------------------------> +/// <---------------><-----------------------> +/// <---><----------><-----------------------> +/// <---><----------><-------><-----><-------> +/// <---><---><-----><-------><-----><-------> +/// <---><---><-----><-------><-----><-><-><-> pub fn file( odb: impl gix_object::Find + gix_object::FindHeader, suspect: ObjectId, diff --git a/gix/Cargo.toml b/gix/Cargo.toml index 38585d20ccd..232f538f9b5 100644 --- a/gix/Cargo.toml +++ b/gix/Cargo.toml @@ -67,6 +67,7 @@ extras = [ "interrupt", "status", "dirwalk", + "blame" ] ## A collection of features that need a larger MSRV, and thus are disabled by default. diff --git a/gix/src/repository/blame.rs b/gix/src/repository/blame.rs new file mode 100644 index 00000000000..84c0902f7de --- /dev/null +++ b/gix/src/repository/blame.rs @@ -0,0 +1,32 @@ +use gix_hash::ObjectId; +use gix_ref::bstr::BStr; + +use crate::{repository::blame_file, Repository}; + +impl Repository { + /// Produce a list of consecutive [`gix_blame::BlameEntry`] instances. Each `BlameEntry` + /// corresponds to a hunk of consecutive lines of the file at `suspect:` that got + /// introduced by a specific commit. + /// + /// For details, see the documentation of [`gix_blame::file()`]. + pub fn blame_file( + &self, + file_path: &BStr, + suspect: impl Into, + options: gix_blame::Options, + ) -> Result { + let cache: Option = self.commit_graph_if_enabled()?; + let mut resource_cache = self.diff_resource_cache_for_tree_diff()?; + + let outcome = gix_blame::file( + &self.objects, + suspect.into(), + cache, + &mut resource_cache, + file_path, + options, + )?; + + Ok(outcome) + } +} diff --git a/gix/src/repository/mod.rs b/gix/src/repository/mod.rs index 92a9545e3a6..869616f7948 100644 --- a/gix/src/repository/mod.rs +++ b/gix/src/repository/mod.rs @@ -19,6 +19,8 @@ pub enum Kind { #[cfg(any(feature = "attributes", feature = "excludes"))] pub mod attributes; +#[cfg(feature = "blame")] +mod blame; mod cache; #[cfg(feature = "worktree-mutation")] mod checkout; @@ -61,6 +63,22 @@ mod submodule; mod thread_safe; mod worktree; +/// +#[cfg(feature = "blame")] +pub mod blame_file { + /// The error returned by [Repository::blame_file()](crate::Repository::blame_file()). + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error(transparent)] + CommitGraphIfEnabled(#[from] super::commit_graph_if_enabled::Error), + #[error(transparent)] + DiffResourceCache(#[from] super::diff_resource_cache::Error), + #[error(transparent)] + Blame(#[from] gix_blame::Error), + } +} + /// #[cfg(feature = "blob-diff")] pub mod diff_tree_to_tree { diff --git a/gix/tests/fixtures/generated-archives/make_blame_repo.tar b/gix/tests/fixtures/generated-archives/make_blame_repo.tar new file mode 100644 index 00000000000..3aaf7980176 Binary files /dev/null and b/gix/tests/fixtures/generated-archives/make_blame_repo.tar differ diff --git a/gix/tests/fixtures/make_blame_repo.sh b/gix/tests/fixtures/make_blame_repo.sh new file mode 100755 index 00000000000..9cb9922b49d --- /dev/null +++ b/gix/tests/fixtures/make_blame_repo.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +set -eu -o pipefail + +git init -q + +echo "line 1" >> simple.txt +git add simple.txt +git commit -q -m c1 + +echo "line 2" >> simple.txt +git add simple.txt +git commit -q -m c2 + +echo "line 3" >> simple.txt +git add simple.txt +git commit -q -m c3 + +echo "line 4" >> simple.txt +git add simple.txt +git commit -q -m c4 diff --git a/gix/tests/gix/repository/blame.rs b/gix/tests/gix/repository/blame.rs new file mode 100644 index 00000000000..eb3f0d1ac0c --- /dev/null +++ b/gix/tests/gix/repository/blame.rs @@ -0,0 +1,63 @@ +use gix::bstr::BString; +use std::num::NonZero; + +#[test] +fn simple() -> crate::Result { + let repo = crate::named_repo("make_blame_repo.sh")?; + + let suspect = repo.head_id()?; + let outcome = repo.blame_file("simple.txt".into(), suspect, Default::default())?; + + assert_eq!(outcome.entries.len(), 4); + + Ok(()) +} + +#[test] +fn with_options() -> crate::Result { + let repo = crate::named_repo("make_blame_repo.sh")?; + + let options = gix::blame::Options { + range: gix::blame::BlameRanges::from_range(1..=2), + ..Default::default() + }; + + let suspect = repo.head_id()?; + let outcome = repo.blame_file("simple.txt".into(), suspect, options)?; + + assert_eq!(outcome.entries.len(), 2); + + let entries_with_lines: Vec<_> = outcome.entries_with_lines().collect(); + + assert!(matches!( + entries_with_lines.as_slice(), + &[ + ( + gix::blame::BlameEntry { + start_in_blamed_file: 0, + start_in_source_file: 0, + source_file_name: None, + .. + }, + _, + ), + ( + gix::blame::BlameEntry { + start_in_blamed_file: 1, + start_in_source_file: 1, + source_file_name: None, + .. + }, + _, + ) + ] + )); + + assert_eq!(entries_with_lines[0].0.len, NonZero::new(1).unwrap()); + assert_eq!(entries_with_lines[1].0.len, NonZero::new(1).unwrap()); + + assert_eq!(entries_with_lines[0].1, vec![BString::new("line 1\n".into())]); + assert_eq!(entries_with_lines[1].1, vec![BString::new("line 2\n".into())]); + + Ok(()) +} diff --git a/gix/tests/gix/repository/mod.rs b/gix/tests/gix/repository/mod.rs index 4253ff4b121..1c10014dcf5 100644 --- a/gix/tests/gix/repository/mod.rs +++ b/gix/tests/gix/repository/mod.rs @@ -1,5 +1,7 @@ use gix::Repository; +#[cfg(feature = "blame")] +mod blame; mod config; #[cfg(feature = "excludes")] mod excludes; diff --git a/justfile b/justfile index 8a4c4ffa95c..57f58d47c68 100755 --- a/justfile +++ b/justfile @@ -133,6 +133,7 @@ check: cargo check -p gix --no-default-features --features credentials --tests cargo check -p gix --no-default-features --features index --tests cargo check -p gix --no-default-features --features interrupt --tests + cargo check -p gix --no-default-features --features blame --tests cargo check -p gix --no-default-features cargo check -p gix-odb --features serde cargo check --no-default-features --features max-control