Skip to content

Commit 7059609

Browse files
authored
Merge pull request #1858 from cruessler/add-git-blame-since
Add `gix blame --since`
2 parents dc8bd63 + e7084d8 commit 7059609

File tree

10 files changed

+83
-13
lines changed

10 files changed

+83
-13
lines changed

Cargo.lock

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gitoxide-core/src/repository/blame.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use std::ffi::OsStr;
55
pub fn blame_file(
66
mut repo: gix::Repository,
77
file: &OsStr,
8-
range: Option<std::ops::Range<u32>>,
8+
options: gix::blame::Options,
99
out: impl std::io::Write,
1010
err: Option<&mut dyn std::io::Write>,
1111
) -> anyhow::Result<()> {
@@ -44,7 +44,7 @@ pub fn blame_file(
4444
cache,
4545
&mut resource_cache,
4646
file.as_bstr(),
47-
range,
47+
options,
4848
)?;
4949
let statistics = outcome.statistics;
5050
write_blame_entries(out, outcome)?;

gix-blame/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ doctest = false
1717
gix-commitgraph = { version = "^0.26.0", path = "../gix-commitgraph" }
1818
gix-revwalk = { version = "^0.18.0", path = "../gix-revwalk" }
1919
gix-trace = { version = "^0.1.12", path = "../gix-trace" }
20+
gix-date = { version = "^0.9.3", path = "../gix-date" }
2021
gix-diff = { version = "^0.50.0", path = "../gix-diff", default-features = false, features = ["blob"] }
2122
gix-object = { version = "^0.47.0", path = "../gix-object" }
2223
gix-hash = { version = "^0.16.0", path = "../gix-hash" }

gix-blame/src/file/function.rs

+18-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use super::{process_changes, Change, UnblamedHunk};
2-
use crate::{BlameEntry, Error, Outcome, Statistics};
2+
use crate::{BlameEntry, Error, Options, Outcome, Statistics};
33
use gix_diff::blob::intern::TokenSource;
44
use gix_diff::tree::Visit;
55
use gix_hash::ObjectId;
@@ -67,7 +67,7 @@ pub fn file(
6767
cache: Option<gix_commitgraph::Graph>,
6868
resource_cache: &mut gix_diff::blob::Platform,
6969
file_path: &BStr,
70-
range: Option<Range<u32>>,
70+
options: Options,
7171
) -> Result<Outcome, Error> {
7272
let _span = gix_trace::coarse!("gix_blame::file()", ?file_path, ?suspect);
7373

@@ -94,7 +94,7 @@ pub fn file(
9494
return Ok(Outcome::default());
9595
}
9696

97-
let range_in_blamed_file = one_based_inclusive_to_zero_based_exclusive_range(range, num_lines_in_blamed)?;
97+
let range_in_blamed_file = one_based_inclusive_to_zero_based_exclusive_range(options.range, num_lines_in_blamed)?;
9898
let mut hunks_to_blame = vec![UnblamedHunk {
9999
range_in_blamed_file: range_in_blamed_file.clone(),
100100
suspects: [(suspect, range_in_blamed_file)].into(),
@@ -103,7 +103,7 @@ pub fn file(
103103
let (mut buf, mut buf2) = (Vec::new(), Vec::new());
104104
let commit = find_commit(cache.as_ref(), &odb, &suspect, &mut buf)?;
105105
let mut queue: gix_revwalk::PriorityQueue<CommitTime, ObjectId> = gix_revwalk::PriorityQueue::new();
106-
queue.insert(commit_time(commit)?, suspect);
106+
queue.insert(commit_time(&commit)?, suspect);
107107

108108
let mut out = Vec::new();
109109
let mut diff_state = gix_diff::tree::State::default();
@@ -122,7 +122,20 @@ pub fn file(
122122
}
123123

124124
let commit = find_commit(cache.as_ref(), &odb, &suspect, &mut buf)?;
125+
let commit_time = commit_time(&commit)?;
126+
127+
if let Some(since) = options.since {
128+
if commit_time < since.seconds {
129+
if unblamed_to_out_is_done(&mut hunks_to_blame, &mut out, suspect) {
130+
break 'outer;
131+
}
132+
133+
continue;
134+
}
135+
}
136+
125137
let parent_ids: ParentIds = collect_parents(commit, &odb, cache.as_ref(), &mut buf2)?;
138+
126139
if parent_ids.is_empty() {
127140
if queue.is_empty() {
128141
// 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(
647660

648661
type CommitTime = i64;
649662

650-
fn commit_time(commit: gix_traverse::commit::Either<'_, '_>) -> Result<CommitTime, gix_object::decode::Error> {
663+
fn commit_time(commit: &gix_traverse::commit::Either<'_, '_>) -> Result<CommitTime, gix_object::decode::Error> {
651664
match commit {
652665
gix_traverse::commit::Either::CommitRefIter(commit_ref_iter) => {
653666
commit_ref_iter.committer().map(|c| c.time.seconds)

gix-blame/src/lib.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
mod error;
1818
pub use error::Error;
1919
mod types;
20-
pub use types::{BlameEntry, Outcome, Statistics};
20+
pub use types::{BlameEntry, Options, Outcome, Statistics};
2121

2222
mod file;
2323
pub use file::function::file;

gix-blame/src/types.rs

+11
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,17 @@ use std::{
77
ops::{AddAssign, Range, SubAssign},
88
};
99

10+
/// Options to be passed to [`file()`](crate::file()).
11+
#[derive(Default, Debug, Clone)]
12+
pub struct Options {
13+
/// A 1-based inclusive range, in order to mirror `git`’s behaviour. `Some(20..40)` represents
14+
/// 21 lines, spanning from line 20 up to and including line 40. This will be converted to
15+
/// `19..40` internally as the algorithm uses 0-based ranges that are exclusive at the end.
16+
pub range: Option<std::ops::Range<u32>>,
17+
/// Don't consider commits before the given date.
18+
pub since: Option<gix_date::Time>,
19+
}
20+
1021
/// The outcome of [`file()`](crate::file()).
1122
#[derive(Debug, Default, Clone)]
1223
pub struct Outcome {

gix-blame/tests/blame.rs

+42-3
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,10 @@ macro_rules! mktest {
191191
None,
192192
&mut resource_cache,
193193
format!("{}.txt", $case).as_str().into(),
194-
None,
194+
gix_blame::Options {
195+
range: None,
196+
since: None,
197+
},
195198
)?
196199
.entries;
197200

@@ -258,7 +261,10 @@ fn diff_disparity() {
258261
None,
259262
&mut resource_cache,
260263
format!("{case}.txt").as_str().into(),
261-
None,
264+
gix_blame::Options {
265+
range: None,
266+
since: None,
267+
},
262268
)
263269
.unwrap()
264270
.entries;
@@ -286,7 +292,10 @@ fn line_range() {
286292
None,
287293
&mut resource_cache,
288294
"simple.txt".into(),
289-
Some(1..2),
295+
gix_blame::Options {
296+
range: Some(1..2),
297+
since: None,
298+
},
290299
)
291300
.unwrap()
292301
.entries;
@@ -299,6 +308,36 @@ fn line_range() {
299308
assert_eq!(lines_blamed, baseline);
300309
}
301310

311+
#[test]
312+
fn since() {
313+
let Fixture {
314+
odb,
315+
mut resource_cache,
316+
suspect,
317+
} = Fixture::new().unwrap();
318+
319+
let lines_blamed = gix_blame::file(
320+
&odb,
321+
suspect,
322+
None,
323+
&mut resource_cache,
324+
"simple.txt".into(),
325+
gix_blame::Options {
326+
range: None,
327+
since: Some(gix_date::parse("2025-01-31", None).unwrap()),
328+
},
329+
)
330+
.unwrap()
331+
.entries;
332+
333+
assert_eq!(lines_blamed.len(), 1);
334+
335+
let git_dir = fixture_path().join(".git");
336+
let baseline = Baseline::collect(git_dir.join("simple-since.baseline")).unwrap();
337+
338+
assert_eq!(lines_blamed, baseline);
339+
}
340+
302341
fn fixture_path() -> PathBuf {
303342
gix_testtools::scripted_fixture_read_only("make_blame_repo.sh").unwrap()
304343
}

gix-blame/tests/fixtures/make_blame_repo.sh

+1
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@ git merge branch-that-has-earlier-commit || true
227227

228228
git blame --porcelain simple.txt > .git/simple.baseline
229229
git blame --porcelain -L 1,2 simple.txt > .git/simple-lines-1-2.baseline
230+
git blame --porcelain --since 2025-01-31 simple.txt > .git/simple-since.baseline
230231
git blame --porcelain multiline-hunks.txt > .git/multiline-hunks.baseline
231232
git blame --porcelain deleted-lines.txt > .git/deleted-lines.baseline
232233
git blame --porcelain deleted-lines-multiple-hunks.txt > .git/deleted-lines-multiple-hunks.baseline

src/plumbing/main.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -1546,6 +1546,7 @@ pub fn main() -> Result<()> {
15461546
statistics,
15471547
file,
15481548
range,
1549+
since,
15491550
} => prepare_and_run(
15501551
"blame",
15511552
trace,
@@ -1557,7 +1558,7 @@ pub fn main() -> Result<()> {
15571558
core::repository::blame::blame_file(
15581559
repository(Mode::Lenient)?,
15591560
&file,
1560-
range,
1561+
gix::blame::Options { range, since },
15611562
out,
15621563
statistics.then_some(err),
15631564
)

src/plumbing/options/mod.rs

+4-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use clap_complete::Shell;
44
use gitoxide_core as core;
55
use gix::bstr::BString;
66

7-
use crate::shared::AsRange;
7+
use crate::shared::{AsRange, AsTime};
88

99
#[derive(Debug, clap::Parser)]
1010
#[clap(name = "gix", about = "The git underworld", version = option_env!("GIX_VERSION"))]
@@ -167,6 +167,9 @@ pub enum Subcommands {
167167
/// Only blame lines in the given 1-based inclusive range '<start>,<end>', e.g. '20,40'.
168168
#[clap(short='L', value_parser=AsRange)]
169169
range: Option<std::ops::Range<u32>>,
170+
/// Don't consider commits before the given date.
171+
#[clap(long, value_parser=AsTime, value_name = "DATE")]
172+
since: Option<gix::date::Time>,
170173
},
171174
/// Generate shell completions to stdout or a directory.
172175
#[clap(visible_alias = "generate-completions", visible_alias = "shell-completions")]

0 commit comments

Comments
 (0)