This commit is contained in:
dvermd 2024-09-11 16:15:46 +02:00 committed by GitHub
commit d4f5230344
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 162 additions and 165 deletions

View File

@ -14,7 +14,7 @@ use crate::format::{self, FormatStringSimple, Placeholder};
use crate::format::{make_placeholder_regex, parse_line_number_format};
use crate::paint::{self, BgShouldFill, StyleSectionSpecifier};
use crate::style::Style;
use crate::utils;
use crate::utils::process;
#[derive(Clone, Debug)]
pub enum BlameLineNumbers {
@ -77,8 +77,7 @@ impl<'a> StateMachine<'a> {
// Emit syntax-highlighted code
if matches!(self.state, State::Unknown) {
self.painter
.set_syntax(utils::process::git_blame_filename().as_deref());
self.painter.set_syntax(self.get_filename().as_deref());
self.painter.set_highlighter();
}
self.state = State::Blame(key);
@ -94,6 +93,13 @@ impl<'a> StateMachine<'a> {
Ok(handled_line)
}
fn get_filename(&self) -> Option<String> {
match &*process::calling_process() {
process::CallingProcess::GitBlame(command_line) => command_line.last_arg.clone(),
_ => None,
}
}
fn blame_metadata_style(
&mut self,
key: &str,

View File

@ -14,6 +14,7 @@ pub enum CallingProcess {
GitLog(CommandLine),
GitReflog(CommandLine),
GitGrep(CommandLine),
GitBlame(CommandLine),
OtherGrep, // rg, grep, ag, ack, etc
None, // no matching process could be found
Pending, // calling process is currently being determined
@ -26,7 +27,9 @@ impl CallingProcess {
CallingProcess::GitDiff(cmd) if cmd.long_options.contains("--relative") => true,
CallingProcess::GitShow(cmd, _) if cmd.long_options.contains("--relative") => true,
CallingProcess::GitLog(cmd) if cmd.long_options.contains("--relative") => true,
CallingProcess::GitGrep(_) | CallingProcess::OtherGrep => true,
CallingProcess::GitGrep(_)
| CallingProcess::GitBlame(_)
| CallingProcess::OtherGrep => true,
_ => false,
}
}
@ -36,7 +39,7 @@ impl CallingProcess {
pub struct CommandLine {
pub long_options: HashSet<String>,
pub short_options: HashSet<String>,
last_arg: Option<String>,
pub last_arg: Option<String>,
}
lazy_static! {
@ -110,36 +113,6 @@ pub enum ProcessArgs<T> {
OtherProcess,
}
pub fn git_blame_filename() -> Option<String> {
calling_process_cmdline(ProcInfo::new(), guess_git_blame_filename)
}
pub fn guess_git_blame_filename(args: &[String]) -> ProcessArgs<String> {
let all_args = args.iter().map(|s| s.as_str());
// See git(1) and git-blame(1). Some arguments separate their parameter with space or '=', e.g.
// --date 2015 or --date=2015.
let git_blame_options_with_parameter =
"-C -c -L --since --ignore-rev --ignore-revs-file --contents --reverse --date";
let selected_args =
skip_uninteresting_args(all_args, git_blame_options_with_parameter.split(' '));
match selected_args.as_slice() {
[git, "blame", .., last_arg] if is_git_binary(git) => {
match Path::new(last_arg)
.file_name()
.map(|filename| filename.to_string_lossy().to_string())
{
Some(filename) => ProcessArgs::Args(filename),
None => ProcessArgs::ArgError,
}
}
[git, "blame"] if is_git_binary(git) => ProcessArgs::ArgError,
_ => ProcessArgs::OtherProcess,
}
}
pub fn describe_calling_process(args: &[String]) -> ProcessArgs<CallingProcess> {
let mut args = args.iter().map(|s| s.as_str());
@ -155,7 +128,12 @@ pub fn describe_calling_process(args: &[String]) -> ProcessArgs<CallingProcess>
Some(command) => match Path::new(command).file_stem() {
Some(s) if s.to_str().map(is_git_binary).unwrap_or(false) => {
let mut args = args.skip_while(|s| {
*s != "diff" && *s != "show" && *s != "log" && *s != "reflog" && *s != "grep"
*s != "diff"
&& *s != "show"
&& *s != "log"
&& *s != "reflog"
&& *s != "grep"
&& *s != "blame"
});
match args.next() {
Some("diff") => {
@ -184,6 +162,9 @@ pub fn describe_calling_process(args: &[String]) -> ProcessArgs<CallingProcess>
Some("grep") => {
ProcessArgs::Args(CallingProcess::GitGrep(parse_command_line(args)))
}
Some("blame") => {
ProcessArgs::Args(CallingProcess::GitBlame(parse_command_line(args)))
}
_ => {
// It's git, but not a subcommand that we parse. Don't
// look at any more processes.
@ -223,49 +204,19 @@ fn is_git_binary(git: &str) -> bool {
.unwrap_or(false)
}
// Skip all arguments starting with '-' from `args_it`. Also skip all arguments listed in
// `skip_this_plus_parameter` plus their respective next argument.
// Keep all arguments once a '--' is encountered.
// (Note that some arguments work with and without '=': '--foo' 'bar' / '--foo=bar')
fn skip_uninteresting_args<'a, 'b, ArgsI, SkipI>(
mut args_it: ArgsI,
skip_this_plus_parameter: SkipI,
) -> Vec<&'a str>
where
ArgsI: Iterator<Item = &'a str>,
SkipI: Iterator<Item = &'b str>,
{
let arg_follows_space: HashSet<&'b str> = skip_this_plus_parameter.into_iter().collect();
let mut result = Vec::new();
loop {
match args_it.next() {
None => break result,
Some("--") => {
result.extend(args_it);
break result;
}
Some(arg) if arg_follows_space.contains(arg) => {
let _skip_parameter = args_it.next();
}
Some(arg) if !arg.starts_with('-') => {
result.push(arg);
}
Some(_) => { /* skip: --these -and --also=this */ }
}
}
}
// Given `--aa val -bc -d val e f -- ...` return
// ({"--aa"}, {"-b", "-c", "-d"})
fn parse_command_line<'a>(args: impl Iterator<Item = &'a str>) -> CommandLine {
let mut long_options = HashSet::new();
let mut short_options = HashSet::new();
let mut last_arg = None;
let mut in_pos_args = false;
for s in args {
if s == "--" {
break;
if in_pos_args {
last_arg = Some(s);
} else if s == "--" {
in_pos_args = true;
} else if s.starts_with("--") {
long_options.insert(s.split('=').next().unwrap().to_owned());
} else if let Some(suffix) = s.strip_prefix('-') {
@ -686,53 +637,6 @@ pub mod tests {
}
}
#[test]
fn test_guess_git_blame_filename() {
use ProcessArgs::Args;
fn make_string_vec(args: &[&str]) -> Vec<String> {
args.iter().map(|&x| x.to_owned()).collect::<Vec<String>>()
}
let args = make_string_vec(&["git", "blame", "hello", "world.txt"]);
assert_eq!(guess_git_blame_filename(&args), Args("world.txt".into()));
let args = make_string_vec(&[
"git",
"blame",
"-s",
"-f",
"hello.txt",
"--date=2015",
"--date",
"now",
]);
assert_eq!(guess_git_blame_filename(&args), Args("hello.txt".into()));
let args = make_string_vec(&["git", "blame", "-s", "-f", "--", "hello.txt"]);
assert_eq!(guess_git_blame_filename(&args), Args("hello.txt".into()));
let args = make_string_vec(&["git", "blame", "--", "--not.an.argument"]);
assert_eq!(
guess_git_blame_filename(&args),
Args("--not.an.argument".into())
);
let args = make_string_vec(&["foo", "bar", "-a", "--123", "not.git"]);
assert_eq!(guess_git_blame_filename(&args), ProcessArgs::OtherProcess);
let args = make_string_vec(&["git", "blame", "--help.txt"]);
assert_eq!(guess_git_blame_filename(&args), ProcessArgs::ArgError);
let args = make_string_vec(&["git", "-c", "a=b", "blame", "main.rs"]);
assert_eq!(guess_git_blame_filename(&args), Args("main.rs".into()));
let args = make_string_vec(&["git", "blame", "README"]);
assert_eq!(guess_git_blame_filename(&args), Args("README".into()));
let args = make_string_vec(&["git", "blame", ""]);
assert_eq!(guess_git_blame_filename(&args), ProcessArgs::ArgError);
}
#[derive(Debug, Default)]
struct FakeProc {
#[allow(dead_code)]
@ -817,22 +721,34 @@ pub mod tests {
{
let _args = FakeParentArgs::once("git blame hello");
assert_eq!(
calling_process_cmdline(ProcInfo::new(), guess_git_blame_filename),
Some("hello".into())
calling_process_cmdline(ProcInfo::new(), describe_calling_process),
Some(CallingProcess::GitBlame(CommandLine {
long_options: [].into(),
short_options: [].into(),
last_arg: Some("hello".into())
}))
);
}
{
let _args = FakeParentArgs::once("git blame world.txt");
assert_eq!(
calling_process_cmdline(ProcInfo::new(), guess_git_blame_filename),
Some("world.txt".into())
calling_process_cmdline(ProcInfo::new(), describe_calling_process),
Some(CallingProcess::GitBlame(CommandLine {
long_options: [].into(),
short_options: [].into(),
last_arg: Some("world.txt".into())
}))
);
}
{
let _args = FakeParentArgs::for_scope("git blame hello world.txt");
assert_eq!(
calling_process_cmdline(ProcInfo::new(), guess_git_blame_filename),
Some("world.txt".into())
calling_process_cmdline(ProcInfo::new(), describe_calling_process),
Some(CallingProcess::GitBlame(CommandLine {
long_options: [].into(),
short_options: [].into(),
last_arg: Some("world.txt".into())
}))
);
}
}
@ -842,11 +758,14 @@ pub mod tests {
fn test_process_testing_assert() {
let _args = FakeParentArgs::once("git blame do.not.panic");
assert_eq!(
calling_process_cmdline(ProcInfo::new(), guess_git_blame_filename),
Some("do.not.panic".into())
calling_process_cmdline(ProcInfo::new(), describe_calling_process),
Some(CallingProcess::GitBlame(CommandLine {
long_options: [].into(),
short_options: [].into(),
last_arg: Some("do.not.panic".into())
}))
);
calling_process_cmdline(ProcInfo::new(), guess_git_blame_filename);
calling_process_cmdline(ProcInfo::new(), describe_calling_process);
}
#[test]
@ -865,16 +784,24 @@ pub mod tests {
}
#[test]
fn test_process_testing_n_times_panic() {
fn test_process_testing_n_times() {
let _args = FakeParentArgs::with(&["git blame once", "git blame twice"]);
assert_eq!(
calling_process_cmdline(ProcInfo::new(), guess_git_blame_filename),
Some("once".into())
calling_process_cmdline(ProcInfo::new(), describe_calling_process),
Some(CallingProcess::GitBlame(CommandLine {
long_options: [].into(),
short_options: [].into(),
last_arg: Some("once".into())
}))
);
assert_eq!(
calling_process_cmdline(ProcInfo::new(), guess_git_blame_filename),
Some("twice".into())
calling_process_cmdline(ProcInfo::new(), describe_calling_process),
Some(CallingProcess::GitBlame(CommandLine {
long_options: [].into(),
short_options: [].into(),
last_arg: Some("twice".into())
}))
);
}
@ -889,8 +816,12 @@ pub mod tests {
fn test_process_testing_n_times_underused() {
let _args = FakeParentArgs::with(&["git blame once", "git blame twice"]);
assert_eq!(
calling_process_cmdline(ProcInfo::new(), guess_git_blame_filename),
Some("once".into())
calling_process_cmdline(ProcInfo::new(), describe_calling_process),
Some(CallingProcess::GitBlame(CommandLine {
long_options: [].into(),
short_options: [].into(),
last_arg: Some("once".into())
}))
);
}
@ -900,15 +831,25 @@ pub mod tests {
fn test_process_testing_n_times_overused() {
let _args = FakeParentArgs::with(&["git blame once"]);
assert_eq!(
calling_process_cmdline(ProcInfo::new(), guess_git_blame_filename),
Some("once".into())
calling_process_cmdline(ProcInfo::new(), describe_calling_process),
Some(CallingProcess::GitBlame(CommandLine {
long_options: [].into(),
short_options: [].into(),
last_arg: Some("once".into())
}))
);
// ignored: dropping causes a panic while panicking, so can't test
calling_process_cmdline(ProcInfo::new(), guess_git_blame_filename);
calling_process_cmdline(ProcInfo::new(), describe_calling_process);
}
#[test]
fn test_process_blame_no_parent_found() {
fn test_describe_calling_process_blame() {
let no_processes = MockProcInfo::with(&[]);
assert_eq!(
calling_process_cmdline(no_processes, describe_calling_process),
None
);
let two_trees = MockProcInfo::with(&[
(2, 100, "-shell", None),
(3, 100, "git blame src/main.rs", Some(2)),
@ -916,43 +857,77 @@ pub mod tests {
(5, 100, "delta", Some(4)),
]);
assert_eq!(
calling_process_cmdline(two_trees, guess_git_blame_filename),
None
);
}
#[test]
fn test_process_blame_info_with_parent() {
let no_processes = MockProcInfo::with(&[]);
assert_eq!(
calling_process_cmdline(no_processes, guess_git_blame_filename),
calling_process_cmdline(two_trees, describe_calling_process),
None
);
let empty_command_line = CommandLine {
long_options: [].into(),
short_options: [].into(),
last_arg: Some("hello.txt".to_string()),
};
let parent = MockProcInfo::with(&[
(2, 100, "-shell", None),
(3, 100, "git blame hello.txt", Some(2)),
(4, 100, "delta", Some(3)),
]);
assert_eq!(
calling_process_cmdline(parent, guess_git_blame_filename),
Some("hello.txt".into())
calling_process_cmdline(parent, describe_calling_process),
Some(CallingProcess::GitBlame(empty_command_line.clone()))
);
let parent = MockProcInfo::with(&[
(2, 100, "-shell", None),
(3, 100, "git blame -- hello.txt", Some(2)),
(4, 100, "delta", Some(3)),
]);
assert_eq!(
calling_process_cmdline(parent, describe_calling_process),
Some(CallingProcess::GitBlame(empty_command_line.clone()))
);
let parent = MockProcInfo::with(&[
(2, 100, "-shell", None),
(3, 100, "Git.exe blame hello.txt", Some(2)),
(4, 100, "delta", Some(3)),
]);
assert_eq!(
calling_process_cmdline(parent, describe_calling_process),
Some(CallingProcess::GitBlame(empty_command_line))
);
let git_blame_command =
"git -c a=b blame -fnb --incremental -t --color-by-age -M --since=3.weeks --contents annotation.txt -C -C2 hello.txt";
// here -C2 is parsed as -C and -2. It doesn't really matters because we only use last_arg from options
// to determine the file type.
let expected_result = Some(CallingProcess::GitBlame(CommandLine {
long_options: set(&["--incremental", "--color-by-age", "--since", "--contents"]),
short_options: set(&["-f", "-n", "-b", "-t", "-M", "-C", "-2"]),
last_arg: Some("hello.txt".to_string()),
}));
let parent = MockProcInfo::with(&[
(2, 100, "-shell", None),
(3, 100, git_blame_command, Some(2)),
(4, 100, "delta", Some(3)),
]);
assert_eq!(
calling_process_cmdline(parent, describe_calling_process),
expected_result
);
let grandparent = MockProcInfo::with(&[
(2, 100, "-shell", None),
(3, 100, "git blame src/main.rs", Some(2)),
(3, 100, git_blame_command, Some(2)),
(4, 100, "call_delta.sh", Some(3)),
(5, 100, "delta", Some(4)),
]);
assert_eq!(
calling_process_cmdline(grandparent, guess_git_blame_filename),
Some("main.rs".into())
calling_process_cmdline(grandparent, describe_calling_process),
expected_result
);
}
#[test]
fn test_process_blame_info_with_sibling() {
let sibling = MockProcInfo::with(&[
(2, 100, "-xterm", None),
(3, 100, "-shell", Some(2)),
@ -960,8 +935,12 @@ pub mod tests {
(5, 100, "delta", Some(3)),
]);
assert_eq!(
calling_process_cmdline(sibling, guess_git_blame_filename),
Some("main.rs".into())
calling_process_cmdline(sibling, describe_calling_process),
Some(CallingProcess::GitBlame(CommandLine {
long_options: [].into(),
short_options: [].into(),
last_arg: Some("src/main.rs".into())
}))
);
let indirect_sibling = MockProcInfo::with(&[
@ -978,8 +957,12 @@ pub mod tests {
(20, 100, "delta", Some(5)),
]);
assert_eq!(
calling_process_cmdline(indirect_sibling, guess_git_blame_filename),
Some("main.abc".into())
calling_process_cmdline(indirect_sibling, describe_calling_process),
Some(CallingProcess::GitBlame(CommandLine {
long_options: set(&["--correct"]),
short_options: [].into(),
last_arg: Some("src/main.abc".into())
}))
);
let indirect_sibling2 = MockProcInfo::with(&[
@ -991,8 +974,12 @@ pub mod tests {
(20, 100, "delta", Some(5)),
]);
assert_eq!(
calling_process_cmdline(indirect_sibling2, guess_git_blame_filename),
Some("main.def".into())
calling_process_cmdline(indirect_sibling2, describe_calling_process),
Some(CallingProcess::GitBlame(CommandLine {
long_options: [].into(),
short_options: [].into(),
last_arg: Some("src/main.def".into())
}))
);
// 3 blame processes, 2 with matching start times, pick the one with lower
@ -1010,8 +997,12 @@ pub mod tests {
(20, 100, "delta", Some(5)),
]);
assert_eq!(
calling_process_cmdline(indirect_sibling_start_times, guess_git_blame_filename),
Some("main.this".into())
calling_process_cmdline(indirect_sibling_start_times, describe_calling_process),
Some(CallingProcess::GitBlame(CommandLine {
long_options: [].into(),
short_options: [].into(),
last_arg: Some("src/main.this".into())
}))
);
}