diff --git a/Cargo.lock b/Cargo.lock index 90992bd2178..5e6cb57104e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1540,6 +1540,7 @@ name = "gix-blame" version = "0.0.0" dependencies = [ "gix-commitgraph 0.26.0", + "gix-date 0.9.3", "gix-diff", "gix-filter", "gix-fs 0.13.0", diff --git a/gitoxide-core/src/repository/blame.rs b/gitoxide-core/src/repository/blame.rs index 2d2930b30dd..04744515343 100644 --- a/gitoxide-core/src/repository/blame.rs +++ b/gitoxide-core/src/repository/blame.rs @@ -5,7 +5,7 @@ use std::ffi::OsStr; pub fn blame_file( mut repo: gix::Repository, file: &OsStr, - range: Option>, + options: gix::blame::Options, out: impl std::io::Write, err: Option<&mut dyn std::io::Write>, ) -> anyhow::Result<()> { @@ -44,7 +44,7 @@ pub fn blame_file( cache, &mut resource_cache, file.as_bstr(), - range, + options, )?; let statistics = outcome.statistics; write_blame_entries(out, outcome)?; diff --git a/gix-blame/Cargo.toml b/gix-blame/Cargo.toml index f934c521da0..5bf21c65f11 100644 --- a/gix-blame/Cargo.toml +++ b/gix-blame/Cargo.toml @@ -17,6 +17,7 @@ doctest = false gix-commitgraph = { version = "^0.26.0", path = "../gix-commitgraph" } gix-revwalk = { version = "^0.18.0", path = "../gix-revwalk" } gix-trace = { version = "^0.1.12", path = "../gix-trace" } +gix-date = { version = "^0.9.3", path = "../gix-date" } gix-diff = { version = "^0.50.0", path = "../gix-diff", default-features = false, features = ["blob"] } gix-object = { version = "^0.47.0", path = "../gix-object" } gix-hash = { version = "^0.16.0", path = "../gix-hash" } diff --git a/gix-blame/src/file/function.rs b/gix-blame/src/file/function.rs index 576611896d6..c78cdc4d9e1 100644 --- a/gix-blame/src/file/function.rs +++ b/gix-blame/src/file/function.rs @@ -1,5 +1,5 @@ use super::{process_changes, Change, UnblamedHunk}; -use crate::{BlameEntry, Error, Outcome, Statistics}; +use crate::{BlameEntry, Error, Options, Outcome, Statistics}; use gix_diff::blob::intern::TokenSource; use gix_diff::tree::Visit; use gix_hash::ObjectId; @@ -67,7 +67,7 @@ pub fn file( cache: Option, resource_cache: &mut gix_diff::blob::Platform, file_path: &BStr, - range: Option>, + options: Options, ) -> Result { let _span = gix_trace::coarse!("gix_blame::file()", ?file_path, ?suspect); @@ -94,7 +94,7 @@ pub fn file( return Ok(Outcome::default()); } - let range_in_blamed_file = one_based_inclusive_to_zero_based_exclusive_range(range, num_lines_in_blamed)?; + let range_in_blamed_file = one_based_inclusive_to_zero_based_exclusive_range(options.range, num_lines_in_blamed)?; let mut hunks_to_blame = vec![UnblamedHunk { range_in_blamed_file: range_in_blamed_file.clone(), suspects: [(suspect, range_in_blamed_file)].into(), @@ -103,7 +103,7 @@ pub fn file( let (mut buf, mut buf2) = (Vec::new(), Vec::new()); let commit = find_commit(cache.as_ref(), &odb, &suspect, &mut buf)?; let mut queue: gix_revwalk::PriorityQueue = gix_revwalk::PriorityQueue::new(); - queue.insert(commit_time(commit)?, suspect); + queue.insert(commit_time(&commit)?, suspect); let mut out = Vec::new(); let mut diff_state = gix_diff::tree::State::default(); @@ -122,7 +122,20 @@ pub fn file( } let commit = find_commit(cache.as_ref(), &odb, &suspect, &mut buf)?; + let commit_time = commit_time(&commit)?; + + if let Some(since) = options.since { + if commit_time < since.seconds { + if queue.is_empty() && unblamed_to_out_is_done(&mut hunks_to_blame, &mut out, suspect) { + break 'outer; + } + + continue; + } + } + let parent_ids: ParentIds = collect_parents(commit, &odb, cache.as_ref(), &mut buf2)?; + if parent_ids.is_empty() { if queue.is_empty() { // I’m not entirely sure if this is correct yet. `suspect`, at this point, is the @@ -647,7 +660,7 @@ fn find_path_entry_in_commit( type CommitTime = i64; -fn commit_time(commit: gix_traverse::commit::Either<'_, '_>) -> Result { +fn commit_time(commit: &gix_traverse::commit::Either<'_, '_>) -> Result { match commit { gix_traverse::commit::Either::CommitRefIter(commit_ref_iter) => { commit_ref_iter.committer().map(|c| c.time.seconds) diff --git a/gix-blame/src/lib.rs b/gix-blame/src/lib.rs index 489434b5b3d..d2c7a5243d7 100644 --- a/gix-blame/src/lib.rs +++ b/gix-blame/src/lib.rs @@ -17,7 +17,7 @@ mod error; pub use error::Error; mod types; -pub use types::{BlameEntry, Outcome, Statistics}; +pub use types::{BlameEntry, Options, Outcome, Statistics}; mod file; pub use file::function::file; diff --git a/gix-blame/src/types.rs b/gix-blame/src/types.rs index f539b11ed87..b1bf7cd8c0e 100644 --- a/gix-blame/src/types.rs +++ b/gix-blame/src/types.rs @@ -7,6 +7,17 @@ use std::{ ops::{AddAssign, Range, SubAssign}, }; +/// Options to be passed to [`file()`](crate::file()). +#[derive(Default, Debug, Clone)] +pub struct Options { + /// A 1-based inclusive range, in order to mirror `git`’s behaviour. `Some(20..40)` represents + /// 21 lines, spanning from line 20 up to and including line 40. This will be converted to + /// `19..40` internally as the algorithm uses 0-based ranges that are exclusive at the end. + pub range: Option>, + /// Don't consider commits before the given date. + pub since: Option, +} + /// The outcome of [`file()`](crate::file()). #[derive(Debug, Default, Clone)] pub struct Outcome { diff --git a/gix-blame/tests/blame.rs b/gix-blame/tests/blame.rs index 926118ef432..bdc9ae43133 100644 --- a/gix-blame/tests/blame.rs +++ b/gix-blame/tests/blame.rs @@ -191,7 +191,10 @@ macro_rules! mktest { None, &mut resource_cache, format!("{}.txt", $case).as_str().into(), - None, + gix_blame::Options { + range: None, + since: None, + }, )? .entries; @@ -258,7 +261,10 @@ fn diff_disparity() { None, &mut resource_cache, format!("{case}.txt").as_str().into(), - None, + gix_blame::Options { + range: None, + since: None, + }, ) .unwrap() .entries; @@ -286,7 +292,10 @@ fn line_range() { None, &mut resource_cache, "simple.txt".into(), - Some(1..2), + gix_blame::Options { + range: Some(1..2), + since: None, + }, ) .unwrap() .entries; @@ -299,6 +308,36 @@ fn line_range() { assert_eq!(lines_blamed, baseline); } +#[test] +fn since() { + let Fixture { + odb, + mut resource_cache, + suspect, + } = Fixture::new().unwrap(); + + let lines_blamed = gix_blame::file( + &odb, + suspect, + None, + &mut resource_cache, + "simple.txt".into(), + gix_blame::Options { + range: None, + since: Some(gix_date::parse("2025-01-31", None).unwrap()), + }, + ) + .unwrap() + .entries; + + assert_eq!(lines_blamed.len(), 1); + + let git_dir = fixture_path().join(".git"); + let baseline = Baseline::collect(git_dir.join("simple-since.baseline")).unwrap(); + + assert_eq!(lines_blamed, baseline); +} + fn fixture_path() -> PathBuf { gix_testtools::scripted_fixture_read_only("make_blame_repo.sh").unwrap() } diff --git a/gix-blame/tests/fixtures/make_blame_repo.sh b/gix-blame/tests/fixtures/make_blame_repo.sh index a09a5bd73cc..aa366230fff 100755 --- a/gix-blame/tests/fixtures/make_blame_repo.sh +++ b/gix-blame/tests/fixtures/make_blame_repo.sh @@ -227,6 +227,7 @@ git merge branch-that-has-earlier-commit || true git blame --porcelain simple.txt > .git/simple.baseline git blame --porcelain -L 1,2 simple.txt > .git/simple-lines-1-2.baseline +git blame --porcelain --since 2025-01-31 simple.txt > .git/simple-since.baseline git blame --porcelain multiline-hunks.txt > .git/multiline-hunks.baseline git blame --porcelain deleted-lines.txt > .git/deleted-lines.baseline git blame --porcelain deleted-lines-multiple-hunks.txt > .git/deleted-lines-multiple-hunks.baseline diff --git a/src/plumbing/main.rs b/src/plumbing/main.rs index b17a6b3a1e2..337bbce66bf 100644 --- a/src/plumbing/main.rs +++ b/src/plumbing/main.rs @@ -1546,6 +1546,7 @@ pub fn main() -> Result<()> { statistics, file, range, + since, } => prepare_and_run( "blame", trace, @@ -1557,7 +1558,7 @@ pub fn main() -> Result<()> { core::repository::blame::blame_file( repository(Mode::Lenient)?, &file, - range, + gix::blame::Options { range, since }, out, statistics.then_some(err), ) diff --git a/src/plumbing/options/mod.rs b/src/plumbing/options/mod.rs index 8c7698052a0..70e80c56740 100644 --- a/src/plumbing/options/mod.rs +++ b/src/plumbing/options/mod.rs @@ -4,7 +4,7 @@ use clap_complete::Shell; use gitoxide_core as core; use gix::bstr::BString; -use crate::shared::AsRange; +use crate::shared::{AsRange, AsTime}; #[derive(Debug, clap::Parser)] #[clap(name = "gix", about = "The git underworld", version = option_env!("GIX_VERSION"))] @@ -167,6 +167,9 @@ pub enum Subcommands { /// Only blame lines in the given 1-based inclusive range ',', e.g. '20,40'. #[clap(short='L', value_parser=AsRange)] range: Option>, + /// Don't consider commits before the given date. + #[clap(long, value_parser=AsTime, value_name = "DATE")] + since: Option, }, /// Generate shell completions to stdout or a directory. #[clap(visible_alias = "generate-completions", visible_alias = "shell-completions")]