Skip to content

Commit f99eacf

Browse files
committed
Add gix blame -L start,end
This enables running blame for a portion of a file.
1 parent 8df5ba2 commit f99eacf

File tree

7 files changed

+85
-11
lines changed

7 files changed

+85
-11
lines changed

gitoxide-core/src/repository/blame.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +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>>,
89
out: impl std::io::Write,
910
err: Option<&mut dyn std::io::Write>,
1011
) -> anyhow::Result<()> {
@@ -40,7 +41,7 @@ pub fn blame_file(
4041
.with_commit_graph(repo.commit_graph_if_enabled()?)
4142
.build()?;
4243
let mut resource_cache = repo.diff_resource_cache_for_tree_diff()?;
43-
let outcome = gix::blame::file(&repo.objects, traverse, &mut resource_cache, file.as_bstr())?;
44+
let outcome = gix::blame::file(&repo.objects, traverse, &mut resource_cache, file.as_bstr(), range)?;
4445
let statistics = outcome.statistics;
4546
write_blame_entries(out, outcome)?;
4647

gix-blame/src/error.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,6 @@ pub enum Error {
2727
Traverse(#[source] Box<dyn std::error::Error + Send + Sync>),
2828
#[error(transparent)]
2929
DiffTree(#[from] gix_diff::tree::Error),
30+
#[error("Invalid line range was given")]
31+
InvalidLineRange,
3032
}

gix-blame/src/file/function.rs

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ pub fn file<E>(
6161
traverse: impl IntoIterator<Item = Result<gix_traverse::commit::Info, E>>,
6262
resource_cache: &mut gix_diff::blob::Platform,
6363
file_path: &BStr,
64+
range: Option<Range<u32>>,
6465
) -> Result<Outcome, Error>
6566
where
6667
E: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
@@ -85,19 +86,34 @@ where
8586
.tokenize()
8687
.map(|token| interner.intern(token))
8788
.count()
88-
};
89+
} as u32;
8990

9091
// Binary or otherwise empty?
9192
if num_lines_in_blamed == 0 {
9293
return Ok(Outcome::default());
9394
}
9495

95-
let mut hunks_to_blame = vec![{
96-
let range_in_blamed_file = 0..num_lines_in_blamed as u32;
97-
UnblamedHunk {
98-
range_in_blamed_file: range_in_blamed_file.clone(),
99-
suspects: [(suspect, range_in_blamed_file)].into(),
96+
let range_in_blamed_file = {
97+
// This block assumes that `range` has 1-based line numbers and converts it the the format
98+
// internally used: 0-based line numbers stored in ranges that are exclusive at the end.
99+
if let Some(range) = range {
100+
if range.start == 0 {
101+
return Err(Error::InvalidLineRange);
102+
}
103+
let start = range.start - 1;
104+
let end = range.end;
105+
if start >= num_lines_in_blamed || end > num_lines_in_blamed || start == end {
106+
return Err(Error::InvalidLineRange);
107+
}
108+
start..end
109+
} else {
110+
0..num_lines_in_blamed
100111
}
112+
};
113+
114+
let mut hunks_to_blame = vec![UnblamedHunk {
115+
range_in_blamed_file: range_in_blamed_file.clone(),
116+
suspects: [(suspect, range_in_blamed_file)].into(),
101117
}];
102118

103119
let mut out = Vec::new();

gix-blame/tests/blame.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ macro_rules! mktest {
194194
commits,
195195
&mut resource_cache,
196196
format!("{}.txt", $case).as_str().into(),
197+
None,
197198
)?
198199
.entries;
199200

@@ -254,6 +255,7 @@ fn diff_disparity() {
254255
commits,
255256
&mut resource_cache,
256257
format!("{case}.txt").as_str().into(),
258+
None,
257259
)
258260
.unwrap()
259261
.entries;

src/plumbing/main.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1542,15 +1542,25 @@ pub fn main() -> Result<()> {
15421542
},
15431543
),
15441544
},
1545-
Subcommands::Blame { statistics, file } => prepare_and_run(
1545+
Subcommands::Blame {
1546+
statistics,
1547+
file,
1548+
range,
1549+
} => prepare_and_run(
15461550
"blame",
15471551
trace,
15481552
verbose,
15491553
progress,
15501554
progress_keep_open,
15511555
None,
15521556
move |_progress, out, err| {
1553-
core::repository::blame::blame_file(repository(Mode::Lenient)?, &file, out, statistics.then_some(err))
1557+
core::repository::blame::blame_file(
1558+
repository(Mode::Lenient)?,
1559+
&file,
1560+
range,
1561+
out,
1562+
statistics.then_some(err),
1563+
)
15541564
},
15551565
),
15561566
Subcommands::Completions { shell, out_dir } => {

src/plumbing/options/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ use clap_complete::Shell;
44
use gitoxide_core as core;
55
use gix::bstr::BString;
66

7+
use crate::shared::AsRange;
8+
79
#[derive(Debug, clap::Parser)]
810
#[clap(name = "gix", about = "The git underworld", version = option_env!("GIX_VERSION"))]
911
#[clap(subcommand_required = true)]
@@ -162,6 +164,9 @@ pub enum Subcommands {
162164
statistics: bool,
163165
/// The file to create the blame information for.
164166
file: std::ffi::OsString,
167+
/// Only blame lines in the given range.
168+
#[clap(short='L', value_parser=AsRange)]
169+
range: Option<std::ops::Range<u32>>,
165170
},
166171
/// Generate shell completions to stdout or a directory.
167172
#[clap(visible_alias = "generate-completions", visible_alias = "shell-completions")]

src/shared.rs

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -408,14 +408,40 @@ mod clap {
408408
.parse_ref(cmd, arg, value)
409409
}
410410
}
411+
412+
#[derive(Clone)]
413+
pub struct AsRange;
414+
415+
impl TypedValueParser for AsRange {
416+
type Value = std::ops::Range<u32>;
417+
418+
fn parse_ref(&self, cmd: &Command, arg: Option<&Arg>, value: &OsStr) -> Result<Self::Value, Error> {
419+
StringValueParser::new()
420+
.try_map(|arg| -> Result<_, Box<dyn std::error::Error + Send + Sync>> {
421+
let parts = arg.split_once(',');
422+
if let Some((start, end)) = parts {
423+
let start = u32::from_str(start)?;
424+
let end = u32::from_str(end)?;
425+
426+
if start <= end {
427+
return Ok(start..end);
428+
}
429+
}
430+
431+
Err(Box::new(Error::new(ErrorKind::ValueValidation)))
432+
})
433+
.parse_ref(cmd, arg, value)
434+
}
435+
}
411436
}
412437
pub use self::clap::{
413-
AsBString, AsHashKind, AsOutputFormat, AsPartialRefName, AsPathSpec, AsTime, CheckPathSpec, ParseRenameFraction,
438+
AsBString, AsHashKind, AsOutputFormat, AsPartialRefName, AsPathSpec, AsRange, AsTime, CheckPathSpec,
439+
ParseRenameFraction,
414440
};
415441

416442
#[cfg(test)]
417443
mod value_parser_tests {
418-
use super::ParseRenameFraction;
444+
use super::{AsRange, ParseRenameFraction};
419445
use clap::Parser;
420446

421447
#[test]
@@ -441,4 +467,16 @@ mod value_parser_tests {
441467
let c = Cmd::parse_from(["cmd", "-a=75"]);
442468
assert_eq!(c.arg, Some(Some(0.75)));
443469
}
470+
471+
#[test]
472+
fn range() {
473+
#[derive(Debug, clap::Parser)]
474+
pub struct Cmd {
475+
#[clap(long, short='l', value_parser = AsRange)]
476+
pub arg: Option<std::ops::Range<u32>>,
477+
}
478+
479+
let c = Cmd::parse_from(["cmd", "-l=1,10"]);
480+
assert_eq!(c.arg, Some(1..10));
481+
}
444482
}

0 commit comments

Comments
 (0)