Skip to content

Commit

Permalink
Add gix blame -L start,end
Browse files Browse the repository at this point in the history
This enables running blame for a portion of a file.
  • Loading branch information
cruessler committed Jan 13, 2025
1 parent 8df5ba2 commit f99eacf
Show file tree
Hide file tree
Showing 7 changed files with 85 additions and 11 deletions.
3 changes: 2 additions & 1 deletion gitoxide-core/src/repository/blame.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use std::ffi::OsStr;
pub fn blame_file(
mut repo: gix::Repository,
file: &OsStr,
range: Option<std::ops::Range<u32>>,
out: impl std::io::Write,
err: Option<&mut dyn std::io::Write>,
) -> anyhow::Result<()> {
Expand Down Expand Up @@ -40,7 +41,7 @@ pub fn blame_file(
.with_commit_graph(repo.commit_graph_if_enabled()?)
.build()?;
let mut resource_cache = repo.diff_resource_cache_for_tree_diff()?;
let outcome = gix::blame::file(&repo.objects, traverse, &mut resource_cache, file.as_bstr())?;
let outcome = gix::blame::file(&repo.objects, traverse, &mut resource_cache, file.as_bstr(), range)?;
let statistics = outcome.statistics;
write_blame_entries(out, outcome)?;

Expand Down
2 changes: 2 additions & 0 deletions gix-blame/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,6 @@ pub enum Error {
Traverse(#[source] Box<dyn std::error::Error + Send + Sync>),
#[error(transparent)]
DiffTree(#[from] gix_diff::tree::Error),
#[error("Invalid line range was given")]
InvalidLineRange,
}
28 changes: 22 additions & 6 deletions gix-blame/src/file/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ pub fn file<E>(
traverse: impl IntoIterator<Item = Result<gix_traverse::commit::Info, E>>,
resource_cache: &mut gix_diff::blob::Platform,
file_path: &BStr,
range: Option<Range<u32>>,
) -> Result<Outcome, Error>
where
E: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
Expand All @@ -85,19 +86,34 @@ where
.tokenize()
.map(|token| interner.intern(token))
.count()
};
} as u32;

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

let mut hunks_to_blame = vec![{
let range_in_blamed_file = 0..num_lines_in_blamed as u32;
UnblamedHunk {
range_in_blamed_file: range_in_blamed_file.clone(),
suspects: [(suspect, range_in_blamed_file)].into(),
let range_in_blamed_file = {
// This block assumes that `range` has 1-based line numbers and converts it the the format
// internally used: 0-based line numbers stored in ranges that are exclusive at the end.
if let Some(range) = range {
if range.start == 0 {
return Err(Error::InvalidLineRange);
}
let start = range.start - 1;
let end = range.end;
if start >= num_lines_in_blamed || end > num_lines_in_blamed || start == end {
return Err(Error::InvalidLineRange);
}
start..end
} else {
0..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(),
}];

let mut out = Vec::new();
Expand Down
2 changes: 2 additions & 0 deletions gix-blame/tests/blame.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ macro_rules! mktest {
commits,
&mut resource_cache,
format!("{}.txt", $case).as_str().into(),
None,
)?
.entries;

Expand Down Expand Up @@ -254,6 +255,7 @@ fn diff_disparity() {
commits,
&mut resource_cache,
format!("{case}.txt").as_str().into(),
None,
)
.unwrap()
.entries;
Expand Down
14 changes: 12 additions & 2 deletions src/plumbing/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1542,15 +1542,25 @@ pub fn main() -> Result<()> {
},
),
},
Subcommands::Blame { statistics, file } => prepare_and_run(
Subcommands::Blame {
statistics,
file,
range,
} => prepare_and_run(
"blame",
trace,
verbose,
progress,
progress_keep_open,
None,
move |_progress, out, err| {
core::repository::blame::blame_file(repository(Mode::Lenient)?, &file, out, statistics.then_some(err))
core::repository::blame::blame_file(
repository(Mode::Lenient)?,
&file,
range,
out,
statistics.then_some(err),
)
},
),
Subcommands::Completions { shell, out_dir } => {
Expand Down
5 changes: 5 additions & 0 deletions src/plumbing/options/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ use clap_complete::Shell;
use gitoxide_core as core;
use gix::bstr::BString;

use crate::shared::AsRange;

#[derive(Debug, clap::Parser)]
#[clap(name = "gix", about = "The git underworld", version = option_env!("GIX_VERSION"))]
#[clap(subcommand_required = true)]
Expand Down Expand Up @@ -162,6 +164,9 @@ pub enum Subcommands {
statistics: bool,
/// The file to create the blame information for.
file: std::ffi::OsString,
/// Only blame lines in the given range.
#[clap(short='L', value_parser=AsRange)]
range: Option<std::ops::Range<u32>>,
},
/// Generate shell completions to stdout or a directory.
#[clap(visible_alias = "generate-completions", visible_alias = "shell-completions")]
Expand Down
42 changes: 40 additions & 2 deletions src/shared.rs
Original file line number Diff line number Diff line change
Expand Up @@ -408,14 +408,40 @@ mod clap {
.parse_ref(cmd, arg, value)
}
}

#[derive(Clone)]
pub struct AsRange;

impl TypedValueParser for AsRange {
type Value = std::ops::Range<u32>;

fn parse_ref(&self, cmd: &Command, arg: Option<&Arg>, value: &OsStr) -> Result<Self::Value, Error> {
StringValueParser::new()
.try_map(|arg| -> Result<_, Box<dyn std::error::Error + Send + Sync>> {
let parts = arg.split_once(',');
if let Some((start, end)) = parts {
let start = u32::from_str(start)?;
let end = u32::from_str(end)?;

if start <= end {
return Ok(start..end);
}
}

Err(Box::new(Error::new(ErrorKind::ValueValidation)))
})
.parse_ref(cmd, arg, value)
}
}
}
pub use self::clap::{
AsBString, AsHashKind, AsOutputFormat, AsPartialRefName, AsPathSpec, AsTime, CheckPathSpec, ParseRenameFraction,
AsBString, AsHashKind, AsOutputFormat, AsPartialRefName, AsPathSpec, AsRange, AsTime, CheckPathSpec,
ParseRenameFraction,
};

#[cfg(test)]
mod value_parser_tests {
use super::ParseRenameFraction;
use super::{AsRange, ParseRenameFraction};
use clap::Parser;

#[test]
Expand All @@ -441,4 +467,16 @@ mod value_parser_tests {
let c = Cmd::parse_from(["cmd", "-a=75"]);
assert_eq!(c.arg, Some(Some(0.75)));
}

#[test]
fn range() {
#[derive(Debug, clap::Parser)]
pub struct Cmd {
#[clap(long, short='l', value_parser = AsRange)]
pub arg: Option<std::ops::Range<u32>>,
}

let c = Cmd::parse_from(["cmd", "-l=1,10"]);
assert_eq!(c.arg, Some(1..10));
}
}

0 comments on commit f99eacf

Please sign in to comment.