Repository: dandavison/delta
Pull request #806 (2021-11-28 00:05:52)

Initial version of ARCHITECTURE.md
@th1000s @WayneD @ulwlu @clnoll @unphased @dhduvall @Kr1ss-XD @norisio @maximbaz 

This is an initial draft of a guide to the codebase to help people who are working on delta or considering it.  Any thoughts, and additions, welcome!

https://github.com/dandavison/delta/blob/architecture-doc/ARCHITECTURE.md

ulwlu: Hi dandavison.
This is a bit of a long greeting, but it's been a while.
I'm sorry I can't contribute much this year,
but I have introduced delta to many engineers in Japan and they are using it.
Thanks for making a really great tool, including vscode and other viewers, but I still like delta the best.

As for architecture.md, it looks like it is a very good walk through to me.
The code has been refactored and changed quite a bit since I used to participate in the development,
but I was able to understand it quickly.

It might be a good idea to focus only on data flow, data structures, and algorithms,
as writing a code-map as well like the [rust-analyzer](https://github.com/rust-analyzer/rust-analyzer/blob/master/docs/dev/architecture.md) might be difficult to maintain.

However, I think one of the purposes of architectures.md is to be a good help for new contributors.
Because if you don't know anything, it takes a lot of time to figure out "what code to change" and so on.
I often spend hours using git blame and ripgrep to go through the code and history (It's just my experience).

Come to think of it, I think it's worth writing only one description for each file in overview tree-file, probably?
Of course, you can see what it is doing by looking at the comments in the file.
However, I think it is easier for a beginner to understand the architecture by looking at the whole tree and seeing only one line of explanation.
And this isn't maybe that difficult to maintain.
When the file structure changes, you only need to change the tree for that part of the file, so you only need to change one line per file.

It's just a opinion, I would like to see other's one too!

```bash
src/
├── ansi/
│  ├── console_tests.rs # ......
│  ├── iterator.rs # .....
│  └── mod.rs
├── features/
│  ├── color_only.rs
│  ├── diff_highlight.rs
│  ├── diff_so_fancy.rs
│  ├── hyperlinks.rs
│  ├── line_numbers.rs
│  ├── mod.rs
│  ├── navigate.rs
│  ├── raw.rs
│  └── side_by_side.rs
├── git_config/
│  ├── git_config_entry.rs
│  └── mod.rs
├── handlers/
│  ├── blame.rs
│  ├── commit_meta.rs
│  ├── diff_stat.rs
│  ├── draw.rs
│  ├── file_meta.rs
│  ├── file_meta_diff.rs
│  ├── file_meta_misc.rs
│  ├── git_show_file.rs
│  ├── grep.rs
│  ├── hunk.rs
│  ├── hunk_header.rs
│  ├── mod.rs
│  ├── ripgrep_json.rs
│  └── submodule.rs
├── options/
│  ├── get.rs
│  ├── mod.rs
│  ├── option_value.rs
│  ├── rewrite.rs
│  ├── set.rs
│  └── theme.rs
├── subcommands/
│  ├── diff.rs
│  ├── list_syntax_themes.rs
│  ├── mod.rs
│  ├── parse_ansi.rs
│  ├── sample_diff.rs
│  ├── show_colors.rs
│  ├── show_config.rs
│  ├── show_syntax_themes.rs
│  └── show_themes.rs
├── tests/
│  ├── ansi_test_utils.rs
│  ├── integration_test_utils.rs
│  ├── mod.rs
│  ├── test_example_diffs.rs
│  └── test_utils.rs
├── utils/
│  ├── bat/
│  │  ├── assets.rs
│  │  ├── dirs.rs
│  │  ├── less.rs
│  │  ├── LICENSE
│  │  ├── mod.rs
│  │  ├── output.rs
│  │  └── terminal.rs
│  ├── mod.rs
│  ├── process.rs
│  └── syntect.rs
├── align.rs
├── cli.rs
├── color.rs
├── colors.rs
├── config.rs
├── delta.rs
├── edits.rs
├── env.rs
├── format.rs
├── main.rs
├── minusplus.rs
├── paint.rs
├── parse_style.rs
├── parse_styles.rs
├── style.rs
└── wrapping.rs
```


Patch:
diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
new file mode 100644
index 0000000..072894c
--- /dev/null
+++ b/ARCHITECTURE.md
@@ -0,0 +1,111 @@
+The purpose of delta is to transform input received from git, diff, git blame, grep, etc to produce visually appealing output, including by syntax highlighting code.
+
+### Initialization
+
+Delta [reads](https://github.com/dandavison/delta/blob/1e1bd6b6b96a3515fd7c70d6b252a25eb9807dc7/src/main.rs#L83) user options from `[delta]` sections in [`.gitconfig`](https://git-scm.com/docs/git-config), and from the command line.
+
+### Input
+
+Delta [reads from stdin](https://github.com/dandavison/delta/blob/1e1bd6b6b96a3515fd7c70d6b252a25eb9807dc7/src/main.rs#L138), hence one can do e.g. `git diff | delta`.
+Note that when git's stdout is sent to a pipe, (a) git does not emit [ANSI color escape sequences](https://en.wikipedia.org/wiki/ANSI_escape_code) unless `--color=always`, and (b) git does not start its own pager process.
+
+Users typically configure git to use delta as its pager.
+In that case, git sends its stdout to delta behind the scenes (_with_ ANSI color escape sequences), without the user needing to pipe it explicitly.
+
+### Parsing the input
+
+Delta [parses input](https://github.com/dandavison/delta/blob/1e1bd6b6b96a3515fd7c70d6b252a25eb9807dc7/src/delta.rs#L81) using a state machine in which
+the states correspond to semantically distinct sections of the input (e.g. `HunkMinus` means that we are in a removed line in a diff hunk).
+The core dispatching loop is [here](https://github.com/dandavison/delta/blob/1e1bd6b6b96a3515fd7c70d6b252a25eb9807dc7/src/delta.rs#L115-L129).
+
+### Output
+
+Delta [creates](https://github.com/dandavison/delta/blob/114ae670223520657208501a3245a3b4261c1093/src/main.rs#L125) a child pager process (`less`) and writes its output to the stdin of the pager process.
+Delta's `navigate` feature is implemented by constructing an appropriate regex and passing it as an argument to `less`.
+
+## Core utility data structures
+
+- [`config::Config`](https://github.com/dandavison/delta/blob/5dc0d6ef7e37a565b06d794b50fcc763079f9ed7/src/config.rs#L59-L143)
+  This is a struct with many fields corresponding to all user options and miscellaneous other useful things.
+  It might be possible to store it globally, but currently the code passes references to it around the call stack.
+
+- [`paint::Painter`](https://github.com/dandavison/delta/blob/1e1bd6b6b96a3515fd7c70d6b252a25eb9807dc7/src/paint.rs#L24-L36)
+  This struct holds the syntax highlighter, and a writable output stream (connected to the stdin of the child `less` process).
+  It also holds two line buffers: one to store all the removed ("minus") lines encountered in a single diff hunk, and one to hold the added ("plus") lines.
+
+## Handling diff hunk lines
+
+Here we will follow one code path in detail: [handling diff hunk lines](https://github.com/dandavison/delta/blob/1e1bd6b6b96a3515fd7c70d6b252a25eb9807dc7/src/handlers/hunk.rs#L27) (removed/unchanged/added).
+This is the most important, and most complex, code path.
+
+Recall that git diff output contains multiple diff "hunks".
+A hunk is a sequence of diff lines describing the changes among some lines of code that are close together in the same file.
+A git diff may have many hunks, from multiple files (and therefore multiple languages).
+Within a hunk, there are sequences of consecutive removed and/or added lines ("subhunks"), separated by unchanged lines.
+(The term "hunk" is standard; the term "subhunk" is specific to delta.)
+
+The handler function that is called when delta process a hunk line is [`handle_hunk_line`](https://github.com/dandavison/delta/blob/1e1bd6b6b96a3515fd7c70d6b252a25eb9807dc7/src/handlers/hunk.rs#L27).
+This function stores the line in a buffer (one buffer for minus lines and one for plus lines): the processing work is not done until we get to the end of the subhunk.
+
+Now, we are at the end of a subhunk, and we have a sequence of minus lines, and a sequence of plus lines.
+
+<table><tr><td><img width=1000px src="https://user-images.githubusercontent.com/52205/143171872-64f41fe1-9968-48c7-86e8-dba9303a54e2.png" alt="image" /></td></tr></table>
+
+Delta [processes a subhunk](https://github.com/dandavison/delta/blob/1e1bd6b6b96a3515fd7c70d6b252a25eb9807dc7/src/paint.rs#L163) as follows:
+
+1. **Compute syntax styles for the subhunk**
+
+   We [call](https://github.com/dandavison/delta/blob/1e1bd6b6b96a3515fd7c70d6b252a25eb9807dc7/src/paint.rs#L164-L173) the [syntect](https://github.com/trishume/syntect) library to compute syntax highlighting styles for each of the minus lines, and each of the plus lines, if the minus/plus styles specify syntax highlighting.
+   The language used for syntax-highlighting is determined by the filename in the diff.
+   For a single line, the result is an array of `(style, substring)` pairs. Each pair specifies the foreground (text) color to be applied to a substring of the line (for example, a language keyword, or a string literal).
+
+2. **Compute diff styles for the subhunk**
+
+   Again, the [call](https://github.com/dandavison/delta/blob/1e1bd6b6b96a3515fd7c70d6b252a25eb9807dc7/src/paint.rs#L174-L175) yields, for each line, an array of `(style, substring)` pairs.
+   Each pair represents foreground and background colors to be applied to a substring of the line, as specified by delta's `*-style` options.
+
+   In order to compute the array of style sections, the call has to (1) infer the correct alignment of minus and plus lines, and (2) for each such "homologous pair", infer the edit operations that transformed the minus line into the plus line (see [within-line-diff-algorithm](#within-line-diff-algorithm)).
+
+   For example, for a minus line, we may have inferred that the line has a homologous plus line, and that a word has been deleted.
+   By default, delta applies a bright red background color to such a word and lets the foreground color be determined by the terminal emulator default foreground color (`minus-emph-style = normal "#901011"`).
+   On the other hand, for an added word, delta by default applies a bright green background color, and specifies that the foreground color should come from the syntax highlighting styles (`plus-emph-style = syntax "#006000"`).
+
+3. **Process subhunk lines for side-by-side or unified output**
+
+   At this point we have a collection of lines corresponding to a subhunk and, for each line, a specification of how syntax styles and diff styles are applied to substrings of the line. These data structures are [processed differently](https://github.com/dandavison/delta/blob/3e21f00765794f7a4e955826a1612b49f1723bfd/src/paint.rs#L177-L230) according to whether unified or side-by-side diff display has been requested.
+
+4. **Superimpose syntax and diff styles for a line**
+
+   Before we can output a line of code we need to take the two arrays of `(style, substring)` pairs and compute a single output array of `(style, substring)` pairs, such that the output array represents the diff styles, but with foreground colors taken from the syntax highlighting, where appropriate.
+   The call is [here](https://github.com/dandavison/delta/blob/1e1bd6b6b96a3515fd7c70d6b252a25eb9807dc7/src/paint.rs#L490-L495).
+
+5. **Output a line with styles converted to ANSI color escape sequences**
+
+   The `style` structs that delta uses are implemented by the [`ansi_term`](https://github.com/ogham/rust-ansi-term) library.
+   Individual substrings are [painted](https://github.com/dandavison/delta/blob/3e21f00765794f7a4e955826a1612b49f1723bfd/src/paint.rs#L507) with their assigned style, and [concatenated](https://github.com/dandavison/delta/blob/3e21f00765794f7a4e955826a1612b49f1723bfd/src/paint.rs#L514) to form a utf-8 string containing ANSI color escape sequences.
+
+## Within-line diff algorithm
+
+There is currently only one within-line diff algorithm implemented.
+This [considers](https://github.com/dandavison/delta/blob/3e21f00765794f7a4e955826a1612b49f1723bfd/src/edits.rs#L41-L43) all possible pairings for a given line and for each one, [computes](https://github.com/dandavison/delta/blob/3e21f00765794f7a4e955826a1612b49f1723bfd/src/edits.rs#L48-L56) the minimum number of edit operations between the candidate pair.
+The inferred pairing is the one with the smallest edit distance.
+(The number of comparisons is constrained by the possible interleavings, and furthermore a greedy heuristic is used, so that the number of comparisons is not quadratic).
+
+## Features
+
+Delta features such as `line-numbers`, `side-by-side`, `diff-so-fancy`, etc can be considered to consist of (a) some feature-specific implementation code, and (b) a collection of key-value pairs specifying the values that certain delta options should take if that feature is enabled.
+Accordingly, each such "feature" is implemented by a separate module under [`src/features/`](https://github.com/dandavison/delta/tree/master/src/features).
+Each of these modules must export a function named `make_feature` whose job is to return key-value pairs for updating the user options.
+
+## Common terms used in the code
+
+|                  |                                                                                                                              |
+| ---------------- | ---------------------------------------------------------------------------------------------------------------------------- |
+| `minus`          | a removed line in a diff hunk (i.e. the lines starting with `-`)                                                             |
+| `zero`           | an unchanged line in a diff hunk                                                                                             |
+| `plus`           | an added line in a diff hunk (i.e. the lines starting with `+`)                                                              |
+| `style`          | a struct specifying foreground colors, background colors, and other attributes such as boldness, derived from ANSI sequences |
+| `style_sections` | an array of `(style, section)` tuples                                                                                        |
+| `paint`          | to take a string without ANSI color sequences and return a new one with ANSI color sequences                                 |
+| `hunk`           | a [diff hunk](https://www.gnu.org/software/diffutils/manual/html_node/Detailed-Unified.html)                                 |
+| `subhunk`        | a consecutive sequence of minus and/or plus lines, without any zero line                                                     |
diff --git a/Cargo.lock b/Cargo.lock
index beb6e4d..951d5aa 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -354,7 +354,7 @@ dependencies = [
 
 [[package]]
 name = "git-delta"
-version = "0.10.2"
+version = "0.10.3"
 dependencies = [
  "ansi_colours",
  "ansi_term 0.12.1",
diff --git a/Cargo.toml b/Cargo.toml
index c699db2..a97a6b2 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -8,7 +8,7 @@ edition = "2018"
 homepage = "https://github.com/dandavison/delta"
 license = "MIT"
 repository = "https://github.com/dandavison/delta"
-version = "0.10.2"
+version = "0.10.3"
 
 [[bin]]
 name = "delta"
diff --git a/etc/examples/styles.py b/etc/examples/styles.py
new file mode 100644
index 0000000..1da13cc
--- /dev/null
+++ b/etc/examples/styles.py
@@ -0,0 +1,4 @@
+# avocet avocet avocet avocet
+# bee-eater bee-eater bee-eater bee-eater
+# chaffinch chaffinch chaffinch
+# dodo dodo dodo dodo dodo
diff --git a/src/delta.rs b/src/delta.rs
index e405ddb..e6c05fb 100644
--- a/src/delta.rs
+++ b/src/delta.rs
@@ -112,6 +112,10 @@ impl<'a> StateMachine<'a> {
                 self.source = detect_source(&self.line);
             }
 
+            // Every method named handle_* must return std::io::Result<bool>.
+            // The bool indicates whether the line has been handled by that
+            // method (in which case no subsequent handlers are permitted to
+            // handle it).
             let _ = self.handle_commit_meta_header_line()?
                 || self.handle_diff_stat_line()?
                 || self.handle_file_meta_diff_line()?
diff --git a/src/edits.rs b/src/edits.rs
index 11979aa..9ee4d19 100644
--- a/src/edits.rs
+++ b/src/edits.rs
@@ -4,14 +4,21 @@ use unicode_segmentation::UnicodeSegmentation;
 use unicode_width::UnicodeWidthStr;
 
 use crate::align;
+use crate::minusplus::MinusPlus;
 
 /// Infer the edit operations responsible for the differences between a collection of old and new
 /// lines. A "line" is a string. An annotated line is a Vec of (op, &str) pairs, where the &str
 /// slices are slices of the line, and their concatenation equals the line. Return the input minus
-/// and plus lines, in annotated form. Also return a specification of the inferred alignment of
-/// minus and plus lines. `noop_deletions[i]` is the appropriate deletion operation tag to be used
-/// for `minus_lines[i]`; `noop_deletions` is guaranteed to be the same length as `minus_lines`.
-/// The equivalent statements hold for `plus_insertions` and `plus_lines`.
+/// and plus lines, in annotated form.
+///
+/// Also return a specification of the inferred alignment of minus and plus lines: a paired minus
+/// and plus line is represented in this alignment specification as
+/// (Some(minus_line_index),Some(plus_line_index)), whereas an unpaired minus line is
+/// (Some(minus_line_index), None).
+///
+/// `noop_deletions[i]` is the appropriate deletion operation tag to be used for `minus_lines[i]`;
+/// `noop_deletions` is guaranteed to be the same length as `minus_lines`. The equivalent statements
+/// hold for `plus_insertions` and `plus_lines`.
 #[allow(clippy::too_many_arguments)]
 #[allow(clippy::type_complexity)]
 pub fn infer_edits<'a, EditOperation>(
@@ -91,6 +98,24 @@ where
     (annotated_minus_lines, annotated_plus_lines, line_alignment)
 }
 
+// Return boolean arrays indicating whether each line has a homolog (is "paired").
+pub fn make_lines_have_homolog(
+    line_alignment: &[(Option<usize>, Option<usize>)],
+) -> MinusPlus<Vec<bool>> {
+    MinusPlus::new(
+        line_alignment
+            .iter()
+            .filter(|(m, _)| m.is_some())
+            .map(|(_, p)| p.is_some())
+            .collect(),
+        line_alignment
+            .iter()
+            .filter(|(_, p)| p.is_some())
+            .map(|(m, _)| m.is_some())
+            .collect(),
+    )
+}
+
 /// Split line into tokens for alignment. The alignment algorithm aligns sequences of substrings;
 /// not individual characters.
 fn tokenize<'a>(line: &'a str, regex: &Regex) -> Vec<&'a str> {
diff --git a/src/features/side_by_side.rs b/src/features/side_by_side.rs
index 77749ea..4f02c47 100644
--- a/src/features/side_by_side.rs
+++ b/src/features/side_by_side.rs
@@ -6,6 +6,7 @@ use crate::ansi;
 use crate::cli;
 use crate::config::{self, delta_unreachable, Config};
 use crate::delta::State;
+use crate::edits;
 use crate::features::{line_numbers, OptionValueFunction};
 use crate::minusplus::*;
 use crate::paint::{BgFillMethod, BgShouldFill, LineSections, Painter};
@@ -85,7 +86,7 @@ pub fn line_is_too_long(line: &str, line_width: usize) -> bool {
 /// structure indicating which of the input lines are too long. This avoids
 /// recalculating the length later.
 pub fn has_long_lines(
-    lines: &LeftRight<&[(String, State)]>,
+    lines: &LeftRight<&Vec<(String, State)>>,
     line_width: &line_numbers::SideBySideLineWidth,
 ) -> (bool, LeftRight<Vec<bool>>) {
     let mut wrap_any = LeftRight::default();
@@ -108,9 +109,10 @@ pub fn has_long_lines(
 
 #[allow(clippy::too_many_arguments)]
 pub fn paint_minus_and_plus_lines_side_by_side(
-    lines: LeftRight<&[(String, State)]>,
+    lines: LeftRight<&Vec<(String, State)>>,
     syntax_sections: LeftRight<Vec<LineSections<SyntectStyle>>>,
     diff_sections: LeftRight<Vec<LineSections<Style>>>,
+    lines_have_homolog: LeftRight<Vec<bool>>,
     line_alignment: Vec<(Option<usize>, Option<usize>)>,
     line_numbers_data: &mut Option<LineNumbersData>,
     output_buffer: &mut String,
@@ -164,12 +166,18 @@ pub fn paint_minus_and_plus_lines_side_by_side(
     } else {
         (line_alignment, line_states, syntax_sections, diff_sections)
     };
+    let lines_have_homolog = if should_wrap {
+        edits::make_lines_have_homolog(&line_alignment)
+    } else {
+        lines_have_homolog
+    };
 
     for (minus_line_index, plus_line_index) in line_alignment {
         output_buffer.push_str(&paint_left_panel_minus_line(
             minus_line_index,
             &syntax_sections[Left],
             &diff_sections[Left],
+            &lines_have_homolog[Left],
             match minus_line_index {
                 Some(i) => &line_states[Left][i],
                 None => &State::HunkMinus(None),
@@ -182,6 +190,7 @@ pub fn paint_minus_and_plus_lines_side_by_side(
             plus_line_index,
             &syntax_sections[Right],
             &diff_sections[Right],
+            &lines_have_homolog[Right],
             match plus_line_index {
                 Some(i) => &line_states[Right][i],
                 None => &State::HunkPlus(None),
@@ -237,6 +246,7 @@ pub fn paint_zero_lines_side_by_side<'a>(
                 panel_line_is_empty,
                 Some(line_index),
                 &diff_style_sections,
+                None,
                 &state,
                 *panel_side,
                 background_color_extends_to_terminal_width,
@@ -253,6 +263,7 @@ fn paint_left_panel_minus_line<'a>(
     line_index: Option<usize>,
     syntax_style_sections: &[LineSections<'a, SyntectStyle>],
     diff_style_sections: &[LineSections<'a, Style>],
+    lines_have_homolog: &[bool],
     state: &'a State,
     line_numbers_data: &mut Option<&mut line_numbers::LineNumbersData>,
     background_color_extends_to_terminal_width: BgShouldFill,
@@ -272,6 +283,7 @@ fn paint_left_panel_minus_line<'a>(
         panel_line_is_empty,
         line_index,
         diff_style_sections,
+        Some(lines_have_homolog),
         state,
         Left,
         background_color_extends_to_terminal_width,
@@ -286,6 +298,7 @@ fn paint_right_panel_plus_line<'a>(
     line_index: Option<usize>,
     syntax_style_sections: &[LineSections<'a, SyntectStyle>],
     diff_style_sections: &[LineSections<'a, Style>],
+    lines_have_homolog: &[bool],
     state: &'a State,
     line_numbers_data: &mut Option<&mut line_numbers::LineNumbersData>,
     background_color_extends_to_terminal_width: BgShouldFill,
@@ -306,6 +319,7 @@ fn paint_right_panel_plus_line<'a>(
         panel_line_is_empty,
         line_index,
         diff_style_sections,
+        Some(lines_have_homolog),
         state,
         Right,
         background_color_extends_to_terminal_width,
@@ -315,10 +329,12 @@ fn paint_right_panel_plus_line<'a>(
     panel_line
 }
 
+#[allow(clippy::too_many_arguments)]
 fn get_right_fill_style_for_panel<'a>(
     line_is_empty: bool,
     line_index: Option<usize>,
     diff_style_sections: &[LineSections<'a, Style>],
+    lines_have_homolog: Option<&[bool]>,
     state: &State,
     panel_side: PanelSide,
     background_color_extends_to_terminal_width: BgShouldFill,
@@ -338,6 +354,7 @@ fn get_right_fill_style_for_panel<'a>(
             let (bg_fill_mode, fill_style) =
                 Painter::get_should_right_fill_background_color_and_fill_style(
                     &diff_style_sections[index],
+                    lines_have_homolog.map(|h| h[index]),
                     state,
                     background_color_extends_to_terminal_width,
                     config,
@@ -434,6 +451,7 @@ fn pad_panel_line_to_width<'a>(
     panel_line_is_empty: bool,
     line_index: Option<usize>,
     diff_style_sections: &[LineSections<'a, Style>],
+    lines_have_homolog: Option<&[bool]>,
     state: &State,
     panel_side: PanelSide,
     background_color_extends_to_terminal_width: BgShouldFill,
@@ -471,6 +489,7 @@ fn pad_panel_line_to_width<'a>(
         panel_line_is_empty,
         line_index,
         diff_style_sections,
+        lines_have_homolog,
         state,
         panel_side,
         background_color_extends_to_terminal_width,
diff --git a/src/handlers/blame.rs b/src/handlers/blame.rs
index 629dc70..2cf6547 100644
--- a/src/handlers/blame.rs
+++ b/src/handlers/blame.rs
@@ -9,8 +9,7 @@ use crate::config;
 use crate::config::delta_unreachable;
 use crate::delta::{self, State, StateMachine};
 use crate::format::{self, Placeholder};
-use crate::paint::BgShouldFill;
-use crate::paint::StyleSectionSpecifier;
+use crate::paint::{self, BgShouldFill, StyleSectionSpecifier};
 use crate::style::Style;
 use crate::utils;
 
@@ -35,17 +34,30 @@ impl<'a> StateMachine<'a> {
                 parse_git_blame_line(&self.line, &self.config.blame_timestamp_format)
             {
                 let is_repeat = previous_commit == Some(blame.commit);
-                let color = self.get_color(blame.commit, previous_commit, is_repeat);
-                let mut style = Style::from_colors(
-                    None,
-                    color::parse_color(&color, true, self.config.git_config.as_ref()),
-                );
-                // TODO: This will often be pointlessly updating a key with the
-                // value it already has. It might be nicer to do this (and
-                // compute the style) in get_color(), but as things stand the
-                // borrow checker won't permit that.
-                self.blame_commit_colors
-                    .insert(blame.commit.to_owned(), color);
+
+                let mut style =
+                    match paint::parse_style_sections(&self.raw_line, self.config).first() {
+                        Some((style, _)) => {
+                            // Something like `blame.coloring = highlightRecent` is in effect; honor
+                            // the color from git, subject to map-styles.
+                            *style
+                        }
+                        None => {
+                            // Compute the color ourselves.
+                            let color = self.get_color(blame.commit, previous_commit, is_repeat);
+                            // TODO: This will often be pointlessly updating a key with the
+                            // value it already has. It might be nicer to do this (and
+                            // compute the style) in get_color(), but as things stand the
+                            // borrow checker won't permit that.
+                            let style = Style::from_colors(
+                                None,
+                                color::parse_color(&color, true, self.config.git_config.as_ref()),
+                            );
+                            self.blame_commit_colors
+                                .insert(blame.commit.to_owned(), color);
+                            style
+                        }
+                    };
 
                 style.is_syntax_highlighted = true;
 
diff --git a/src/handlers/git_show_file.rs b/src/handlers/git_show_file.rs
index 2a4998c..d5eeb51 100644
--- a/src/handlers/git_show_file.rs
+++ b/src/handlers/git_show_file.rs
@@ -9,11 +9,11 @@ impl<'a> StateMachine<'a> {
         self.painter.emit()?;
         let mut handled_line = false;
         if matches!(self.state, State::Unknown) {
-            if let Some(process::CallingProcess::GitShow(extension)) =
+            if let Some(process::CallingProcess::GitShow(_, extension)) =
                 process::calling_process().as_deref()
             {
                 self.state = State::GitShowFile;
-                self.painter.set_syntax(Some(extension));
+                self.painter.set_syntax(extension.as_deref());
                 self.painter.set_highlighter();
             } else {
                 return Ok(handled_line);
diff --git a/src/handlers/grep.rs b/src/handlers/grep.rs
index cffb98e..e5c004f 100644
--- a/src/handlers/grep.rs
+++ b/src/handlers/grep.rs
@@ -244,8 +244,9 @@ fn get_code_style_sections<'b>(
 
 fn make_output_config() -> GrepOutputConfig {
     match process::calling_process().as_deref() {
-        Some(process::CallingProcess::GitGrep((longs, shorts)))
-            if shorts.contains("-W") || longs.contains("--function-context") =>
+        Some(process::CallingProcess::GitGrep(command_line))
+            if command_line.short_options.contains("-W")
+                || command_line.long_options.contains("--function-context") =>
         {
             // --function-context is in effect: i.e. the entire function is
             // being displayed. In that case we don't render the first line as a
@@ -261,8 +262,9 @@ fn make_output_config() -> GrepOutputConfig {
                 pad_line_number: true,
             }
         }
-        Some(process::CallingProcess::GitGrep((longs, shorts)))
-            if shorts.contains("-p") || longs.contains("--show-function") =>
+        Some(process::CallingProcess::GitGrep(command_line))
+            if command_line.short_options.contains("-p")
+                || command_line.long_options.contains("--show-function") =>
         {
             // --show-function is in effect, i.e. the function header is being
             // displayed, along with matches within the function. Therefore we
@@ -406,7 +408,7 @@ pub fn _parse_grep_line<'b>(regex: &Regex, line: &'b str) -> Option<GrepLine<'b>
             None
         }
     })
-    .unwrap(); // The regex matches so one of the three alternatrives must have matched
+    .unwrap(); // The regex matches so one of the three alternatives must have matched
     let code = caps.get(8).unwrap().as_str().into();
 
     Some(GrepLine {
@@ -421,83 +423,73 @@ pub fn _parse_grep_line<'b>(regex: &Regex, line: &'b str) -> Option<GrepLine<'b>
 #[cfg(test)]
 mod tests {
     use crate::handlers::grep::{parse_grep_line, GrepLine, LineType};
-    use crate::utils::process::tests::cfg;
+    use crate::utils::process::tests::FakeParentArgs;
 
     #[test]
     fn test_parse_grep_match() {
         let fake_parent_grep_command = "git --doesnt-matter grep --nor-this nor_this -- nor_this";
-        {
-            let _args = cfg::WithArgs::new(&fake_parent_grep_command);
-            assert_eq!(
-                parse_grep_line("src/co-7-fig.rs:xxx"),
-                Some(GrepLine {
-                    path: "src/co-7-fig.rs".into(),
-                    line_number: None,
-                    line_type: LineType::Match,
-                    code: "xxx".into(),
-                    submatches: None,
-                })
-            );
-        }
-        {
-            let _args = cfg::WithArgs::new(&fake_parent_grep_command);
-            assert_eq!(
-                parse_grep_line("src/config.rs:use crate::minusplus::MinusPlus;"),
-                Some(GrepLine {
-                    path: "src/config.rs".into(),
-                    line_number: None,
-                    line_type: LineType::Match,
-                    code: "use crate::minusplus::MinusPlus;".into(),
-                    submatches: None,
-                })
-            );
-        }
-        {
-            let _args = cfg::WithArgs::new(&fake_parent_grep_command);
-            assert_eq!(
-                parse_grep_line(
-                    "src/config.rs:    pub line_numbers_style_minusplus: MinusPlus<Style>,"
-                ),
-                Some(GrepLine {
-                    path: "src/config.rs".into(),
-                    line_number: None,
-                    line_type: LineType::Match,
-                    code: "    pub line_numbers_style_minusplus: MinusPlus<Style>,".into(),
-                    submatches: None,
-                })
-            );
-        }
-        {
-            let _args = cfg::WithArgs::new(&fake_parent_grep_command);
-            assert_eq!(
-                parse_grep_line("src/con-fig.rs:use crate::minusplus::MinusPlus;"),
-                Some(GrepLine {
-                    path: "src/con-fig.rs".into(),
-                    line_number: None,
-                    line_type: LineType::Match,
-                    code: "use crate::minusplus::MinusPlus;".into(),
-                    submatches: None,
-                })
-            );
-        }
-        {
-            let _args = cfg::WithArgs::new(&fake_parent_grep_command);
-            assert_eq!(
-                parse_grep_line(
-                    "src/con-fig.rs:    pub line_numbers_style_minusplus: MinusPlus<Style>,"
-                ),
-                Some(GrepLine {
-                    path: "src/con-fig.rs".into(),
-                    line_number: None,
-                    line_type: LineType::Match,
-                    code: "    pub line_numbers_style_minusplus: MinusPlus<Style>,".into(),
-                    submatches: None,
-                })
-            );
-        }
-        {
-            let _args = cfg::WithArgs::new(&fake_parent_grep_command);
-            assert_eq!(
+        let _args = FakeParentArgs::for_scope(&fake_parent_grep_command);
+
+        assert_eq!(
+            parse_grep_line("src/co-7-fig.rs:xxx"),
+            Some(GrepLine {
+                path: "src/co-7-fig.rs".into(),
+                line_number: None,
+                line_type: LineType::Match,
+                code: "xxx".into(),
+                submatches: None,
+            })
+        );
+
+        assert_eq!(
+            parse_grep_line("src/config.rs:use crate::minusplus::MinusPlus;"),
+            Some(GrepLine {
+                path: "src/config.rs".into(),
+                line_number: None,
+                line_type: LineType::Match,
+                code: "use crate::minusplus::MinusPlus;".into(),
+                submatches: None,
+            })
+        );
+
+        assert_eq!(
+            parse_grep_line(
+                "src/config.rs:    pub line_numbers_style_minusplus: MinusPlus<Style>,"
+            ),
+            Some(GrepLine {
+                path: "src/config.rs".into(),
+                line_number: None,
+                line_type: LineType::Match,
+                code: "    pub line_numbers_style_minusplus: MinusPlus<Style>,".into(),
+                submatches: None,
+            })
+        );
+
+        assert_eq!(
+            parse_grep_line("src/con-fig.rs:use crate::minusplus::MinusPlus;"),
+            Some(GrepLine {
+                path: "src/con-fig.rs".into(),
+                line_number: None,
+                line_type: LineType::Match,
+                code: "use crate::minusplus::MinusPlus;".into(),
+                submatches: None,
+            })
+        );
+
+        assert_eq!(
+            parse_grep_line(
+                "src/con-fig.rs:    pub line_numbers_style_minusplus: MinusPlus<Style>,"
+            ),
+            Some(GrepLine {
+                path: "src/con-fig.rs".into(),
+                line_number: None,
+                line_type: LineType::Match,
+                code: "    pub line_numbers_style_minusplus: MinusPlus<Style>,".into(),
+                submatches: None,
+            })
+        );
+
+        assert_eq!(
             parse_grep_line(
                 "src/de lta.rs:pub fn delta<I>(lines: ByteLines<I>, writer: &mut dyn Write, config: &Config) -> std::io::Result<()>"
             ),
@@ -509,10 +501,8 @@ mod tests {
                 submatches: None,
             })
         );
-        }
-        {
-            let _args = cfg::WithArgs::new(&fake_parent_grep_command);
-            assert_eq!(
+
+        assert_eq!(
             parse_grep_line(
                 "src/de lta.rs:    pub fn new(writer: &'a mut dyn Write, config: &'a Config) -> Self {"
             ),
@@ -524,82 +514,72 @@ mod tests {
                 submatches: None,
             })
         );
-        }
     }
 
     #[test]
     fn test_parse_grep_n_match() {
         let fake_parent_grep_command =
             "/usr/local/bin/git --doesnt-matter grep --nor-this nor_this -- nor_this";
-        {
-            let _args = cfg::WithArgs::new(&fake_parent_grep_command);
-            assert_eq!(
-                parse_grep_line("src/co-7-fig.rs:7:xxx"),
-                Some(GrepLine {
-                    path: "src/co-7-fig.rs".into(),
-                    line_number: Some(7),
-                    line_type: LineType::Match,
-                    code: "xxx".into(),
-                    submatches: None,
-                })
-            );
-        }
-        {
-            let _args = cfg::WithArgs::new(&fake_parent_grep_command);
-            assert_eq!(
-                parse_grep_line("src/config.rs:21:use crate::minusplus::MinusPlus;"),
-                Some(GrepLine {
-                    path: "src/config.rs".into(),
-                    line_number: Some(21),
-                    line_type: LineType::Match,
-                    code: "use crate::minusplus::MinusPlus;".into(),
-                    submatches: None,
-                })
-            );
-        }
-        {
-            let _args = cfg::WithArgs::new(&fake_parent_grep_command);
-            assert_eq!(
-                parse_grep_line(
-                    "src/config.rs:95:    pub line_numbers_style_minusplus: MinusPlus<Style>,"
-                ),
-                Some(GrepLine {
-                    path: "src/config.rs".into(),
-                    line_number: Some(95),
-                    line_type: LineType::Match,
-                    code: "    pub line_numbers_style_minusplus: MinusPlus<Style>,".into(),
-                    submatches: None,
-                })
-            );
-        }
-        {
-            let _args = cfg::WithArgs::new(&fake_parent_grep_command);
-            assert_eq!(
-                parse_grep_line("Makefile:10:test: unit-test end-to-end-test"),
-                Some(GrepLine {
-                    path: "Makefile".into(),
-                    line_number: Some(10),
-                    line_type: LineType::Match,
-                    code: "test: unit-test end-to-end-test".into(),
-                    submatches: None,
-                })
-            );
-        }
-        {
-            let _args = cfg::WithArgs::new(&fake_parent_grep_command);
-            assert_eq!(
-                parse_grep_line(
-                    "Makefile:16:    ./tests/test_raw_output_matches_git_on_full_repo_history"
-                ),
-                Some(GrepLine {
-                    path: "Makefile".into(),
-                    line_number: Some(16),
-                    line_type: LineType::Match,
-                    code: "    ./tests/test_raw_output_matches_git_on_full_repo_history".into(),
-                    submatches: None,
-                })
-            );
-        }
+        let _args = FakeParentArgs::for_scope(&fake_parent_grep_command);
+
+        assert_eq!(
+            parse_grep_line("src/co-7-fig.rs:7:xxx"),
+            Some(GrepLine {
+                path: "src/co-7-fig.rs".into(),
+                line_number: Some(7),
+                line_type: LineType::Match,
+                code: "xxx".into(),
+                submatches: None,
+            })
+        );
+
+        assert_eq!(
+            parse_grep_line("src/config.rs:21:use crate::minusplus::MinusPlus;"),
+            Some(GrepLine {
+                path: "src/config.rs".into(),
+                line_number: Some(21),
+                line_type: LineType::Match,
+                code: "use crate::minusplus::MinusPlus;".into(),
+                submatches: None,
+            })
+        );
+
+        assert_eq!(
+            parse_grep_line(
+                "src/config.rs:95:    pub line_numbers_style_minusplus: MinusPlus<Style>,"
+            ),
+            Some(GrepLine {
+                path: "src/config.rs".into(),
+                line_number: Some(95),
+                line_type: LineType::Match,
+                code: "    pub line_numbers_style_minusplus: MinusPlus<Style>,".into(),
+                submatches: None,
+            })
+        );
+
+        assert_eq!(
+            parse_grep_line("Makefile:10:test: unit-test end-to-end-test"),
+            Some(GrepLine {
+                path: "Makefile".into(),
+                line_number: Some(10),
+                line_type: LineType::Match,
+                code: "test: unit-test end-to-end-test".into(),
+                submatches: None,
+            })
+        );
+
+        assert_eq!(
+            parse_grep_line(
+                "Makefile:16:    ./tests/test_raw_output_matches_git_on_full_repo_history"
+            ),
+            Some(GrepLine {
+                path: "Makefile".into(),
+                line_number: Some(16),
+                line_type: LineType::Match,
+                code: "    ./tests/test_raw_output_matches_git_on_full_repo_history".into(),
+                submatches: None,
+            })
+        );
     }
 
     #[test]
@@ -609,77 +589,69 @@ mod tests {
         // This fails: we can't parse it currently.
         let fake_parent_grep_command =
             "/usr/local/bin/git --doesnt-matter grep --nor-this nor_this -- nor_this";
-        {
-            let _args = cfg::WithArgs::new(&fake_parent_grep_command);
-            assert_eq!(
-                parse_grep_line("etc/examples/119-within-line-edits:4:repo=$(mktemp -d)"),
-                Some(GrepLine {
-                    path: "etc/examples/119-within-line-edits".into(),
-                    line_number: Some(4),
-                    line_type: LineType::Match,
-                    code: "repo=$(mktemp -d)".into(),
-                    submatches: None,
-                })
-            );
-        }
+        let _args = FakeParentArgs::once(&fake_parent_grep_command);
+
+        assert_eq!(
+            parse_grep_line("etc/examples/119-within-line-edits:4:repo=$(mktemp -d)"),
+            Some(GrepLine {
+                path: "etc/examples/119-within-line-edits".into(),
+                line_number: Some(4),
+                line_type: LineType::Match,
+                code: "repo=$(mktemp -d)".into(),
+                submatches: None,
+            })
+        );
     }
 
     #[test]
     fn test_parse_grep_no_match() {
         let fake_parent_grep_command =
             "/usr/local/bin/git --doesnt-matter grep --nor-this nor_this -- nor_this";
-        {
-            let _args = cfg::WithArgs::new(&fake_parent_grep_command);
-            assert_eq!(
-                parse_grep_line("src/co-7-fig.rs-xxx"),
-                Some(GrepLine {
-                    path: "src/co-7-fig.rs".into(),
-                    line_number: None,
-                    line_type: LineType::Context,
-                    code: "xxx".into(),
-                    submatches: None,
-                })
-            );
-        }
-        {
-            let _args = cfg::WithArgs::new(&fake_parent_grep_command);
-            assert_eq!(
-                parse_grep_line("src/config.rs-    pub available_terminal_width: usize,"),
-                Some(GrepLine {
-                    path: "src/config.rs".into(),
-                    line_number: None,
-                    line_type: LineType::Context,
-                    code: "    pub available_terminal_width: usize,".into(),
-                    submatches: None,
-                })
-            );
-        }
-        {
-            let _args = cfg::WithArgs::new(&fake_parent_grep_command);
-            assert_eq!(
-                parse_grep_line("src/con-fig.rs-use crate::minusplus::MinusPlus;"),
-                Some(GrepLine {
-                    path: "src/con-fig.rs".into(),
-                    line_number: None,
-                    line_type: LineType::Context,
-                    code: "use crate::minusplus::MinusPlus;".into(),
-                    submatches: None,
-                })
-            );
-        }
-        {
-            let _args = cfg::WithArgs::new(&fake_parent_grep_command);
-            assert_eq!(
-                parse_grep_line("de-lta.rs-            if self.source == Source::Unknown {"),
-                Some(GrepLine {
-                    path: "de-lta.rs".into(),
-                    line_number: None,
-                    line_type: LineType::Context,
-                    code: "            if self.source == Source::Unknown {".into(),
-                    submatches: None,
-                })
-            );
-        }
+        let _args = FakeParentArgs::for_scope(&fake_parent_grep_command);
+
+        assert_eq!(
+            parse_grep_line("src/co-7-fig.rs-xxx"),
+            Some(GrepLine {
+                path: "src/co-7-fig.rs".into(),
+                line_number: None,
+                line_type: LineType::Context,
+                code: "xxx".into(),
+                submatches: None,
+            })
+        );
+
+        assert_eq!(
+            parse_grep_line("src/config.rs-    pub available_terminal_width: usize,"),
+            Some(GrepLine {
+                path: "src/config.rs".into(),
+                line_number: None,
+                line_type: LineType::Context,
+                code: "    pub available_terminal_width: usize,".into(),
+                submatches: None,
+            })
+        );
+
+        assert_eq!(
+            parse_grep_line("src/con-fig.rs-use crate::minusplus::MinusPlus;"),
+            Some(GrepLine {
+                path: "src/con-fig.rs".into(),
+                line_number: None,
+                line_type: LineType::Context,
+                code: "use crate::minusplus::MinusPlus;".into(),
+                submatches: None,
+            })
+        );
+
+        assert_eq!(
+            parse_grep_line("de-lta.rs-            if self.source == Source::Unknown {"),
+            Some(GrepLine {
+                path: "de-lta.rs".into(),
+                line_number: None,
+                line_type: LineType::Context,
+                code: "            if self.source == Source::Unknown {".into(),
+                submatches: None,
+            })
+        );
     }
 
     #[test]
@@ -687,51 +659,47 @@ mod tests {
         // git grep -n
         let fake_parent_grep_command =
             "/usr/local/bin/git --doesnt-matter grep --nor-this nor_this -- nor_this";
-        {
-            let _args = cfg::WithArgs::new(&fake_parent_grep_command);
-            assert_eq!(
-                parse_grep_line("src/co-7-fig.rs-7-xxx"),
-                Some(GrepLine {
-                    path: "src/co-7-fig.rs".into(),
-                    line_number: Some(7),
-                    line_type: LineType::Context,
-                    code: "xxx".into(),
-                    submatches: None,
-                })
-            );
-        }
-        {
-            let _args = cfg::WithArgs::new(&fake_parent_grep_command);
-            assert_eq!(
-                parse_grep_line("src/config.rs-58-    pub available_terminal_width: usize,"),
-                Some(GrepLine {
-                    path: "src/config.rs".into(),
-                    line_number: Some(58),
-                    line_type: LineType::Context,
-                    code: "    pub available_terminal_width: usize,".into(),
-                    submatches: None,
-                })
-            );
-        }
+
+        let _args = FakeParentArgs::for_scope(&fake_parent_grep_command);
+        assert_eq!(
+            parse_grep_line("src/co-7-fig.rs-7-xxx"),
+            Some(GrepLine {
+                path: "src/co-7-fig.rs".into(),
+                line_number: Some(7),
+                line_type: LineType::Context,
+                code: "xxx".into(),
+                submatches: None,
+            })
+        );
+
+        assert_eq!(
+            parse_grep_line("src/config.rs-58-    pub available_terminal_width: usize,"),
+            Some(GrepLine {
+                path: "src/config.rs".into(),
+                line_number: Some(58),
+                line_type: LineType::Context,
+                code: "    pub available_terminal_width: usize,".into(),
+                submatches: None,
+            })
+        );
     }
 
     #[test]
     fn test_parse_grep_match_no_extension() {
         let fake_parent_grep_command =
             "/usr/local/bin/git --doesnt-matter grep --nor-this nor_this -- nor_this";
-        {
-            let _args = cfg::WithArgs::new(&fake_parent_grep_command);
-            assert_eq!(
-                parse_grep_line("Makefile:xxx"),
-                Some(GrepLine {
-                    path: "Makefile".into(),
-                    line_number: None,
-                    line_type: LineType::Match,
-                    code: "xxx".into(),
-                    submatches: None,
-                })
-            );
-        }
+        let _args = FakeParentArgs::once(&fake_parent_grep_command);
+
+        assert_eq!(
+            parse_grep_line("Makefile:xxx"),
+            Some(GrepLine {
+                path: "Makefile".into(),
+                line_number: None,
+                line_type: LineType::Match,
+                code: "xxx".into(),
+                submatches: None,
+            })
+        );
     }
 
     #[test]
@@ -739,19 +707,17 @@ mod tests {
         // git grep -n
         let fake_parent_grep_command =
             "/usr/local/bin/git --doesnt-matter grep --nor-this nor_this -- nor_this";
-        {
-            let _args = cfg::WithArgs::new(&fake_parent_grep_command);
-            assert_eq!(
-                parse_grep_line("Makefile:7:xxx"),
-                Some(GrepLine {
-                    path: "Makefile".into(),
-                    line_number: Some(7),
-                    line_type: LineType::Match,
-                    code: "xxx".into(),
-                    submatches: None,
-                })
-            );
-        }
+        let _args = FakeParentArgs::once(&fake_parent_grep_command);
+        assert_eq!(
+            parse_grep_line("Makefile:7:xxx"),
+            Some(GrepLine {
+                path: "Makefile".into(),
+                line_number: Some(7),
+                line_type: LineType::Match,
+                code: "xxx".into(),
+                submatches: None,
+            })
+        );
     }
 
     #[test]
@@ -760,19 +726,18 @@ mod tests {
         // git grep -W
         let fake_parent_grep_command =
             "/usr/local/bin/git --doesnt-matter grep --nor-this nor_this -- nor_this";
-        {
-            let _args = cfg::WithArgs::new(&fake_parent_grep_command);
-            assert_eq!(
-                parse_grep_line("src/config.rs=pub struct Config {"), // match
-                Some(GrepLine {
-                    path: "src/config.rs".into(),
-                    line_number: None,
-                    line_type: LineType::ContextHeader,
-                    code: "pub struct Config {".into(),
-                    submatches: None,
-                })
-            );
-        }
+
+        let _args = FakeParentArgs::once(&fake_parent_grep_command);
+        assert_eq!(
+            parse_grep_line("src/config.rs=pub struct Config {"), // match
+            Some(GrepLine {
+                path: "src/config.rs".into(),
+                line_number: None,
+                line_type: LineType::ContextHeader,
+                code: "pub struct Config {".into(),
+                submatches: None,
+            })
+        );
     }
 
     #[test]
@@ -781,49 +746,44 @@ mod tests {
         // git grep -n -W
         let fake_parent_grep_command =
             "/usr/local/bin/git --doesnt-matter grep --nor-this nor_this -- nor_this";
-        {
-            let _args = cfg::WithArgs::new(&fake_parent_grep_command);
-            assert_eq!(
-                parse_grep_line("src/config.rs=57=pub struct Config {"),
-                Some(GrepLine {
-                    path: "src/config.rs".into(),
-                    line_number: Some(57),
-                    line_type: LineType::ContextHeader,
-                    code: "pub struct Config {".into(),
-                    submatches: None,
-                })
-            );
-        }
+        let _args = FakeParentArgs::once(&fake_parent_grep_command);
+
+        assert_eq!(
+            parse_grep_line("src/config.rs=57=pub struct Config {"),
+            Some(GrepLine {
+                path: "src/config.rs".into(),
+                line_number: Some(57),
+                line_type: LineType::ContextHeader,
+                code: "pub struct Config {".into(),
+                submatches: None,
+            })
+        );
     }
 
     #[test]
     fn test_parse_grep_not_grep_output() {
         let fake_parent_grep_command =
             "/usr/local/bin/git --doesnt-matter grep --nor-this nor_this -- nor_this";
-        {
-            let _args = cfg::WithArgs::new(&fake_parent_grep_command);
-            let not_grep_output = "|       expose it in delta's color output styled with grep:";
-            assert_eq!(parse_grep_line(not_grep_output), None);
-        }
+        let _args = FakeParentArgs::once(&fake_parent_grep_command);
+
+        let not_grep_output = "|       expose it in delta's color output styled with grep:";
+        assert_eq!(parse_grep_line(not_grep_output), None);
     }
 
     #[test]
     fn test_parse_grep_parent_command_is_not_grep_1() {
         let fake_parent_grep_command =
             "/usr/local/bin/notgrep --doesnt-matter --nor-this nor_this -- nor_this";
-        {
-            let _args = cfg::WithArgs::new(&fake_parent_grep_command);
-            let apparently_grep_output = "src/co-7-fig.rs:xxx";
-            assert_eq!(parse_grep_line(apparently_grep_output), None);
-        }
+        let _args = FakeParentArgs::once(&fake_parent_grep_command);
+
+        let apparently_grep_output = "src/co-7-fig.rs:xxx";
+        assert_eq!(parse_grep_line(apparently_grep_output), None);
     }
 
     #[test]
     fn test_parse_grep_parent_command_is_not_grep_2() {
-        {
-            // No fake parent grep command
-            let apparently_grep_output = "src/co-7-fig.rs:xxx";
-            assert_eq!(parse_grep_line(apparently_grep_output), None);
-        }
+        // No fake parent grep command
+        let apparently_grep_output = "src/co-7-fig.rs:xxx";
+        assert_eq!(parse_grep_line(apparently_grep_output), None);
     }
 }
diff --git a/src/handlers/hunk.rs b/src/handlers/hunk.rs
index 201068e..75e0bb6 100644
--- a/src/handlers/hunk.rs
+++ b/src/handlers/hunk.rs
@@ -1,21 +1,33 @@
+use lazy_static::lazy_static;
+
 use crate::cli;
 use crate::delta::{State, StateMachine};
 use crate::style;
+use crate::utils::process::{self, CallingProcess};
 use unicode_segmentation::UnicodeSegmentation;
 
-impl State {
-    fn is_in_hunk(&self) -> bool {
-        matches!(
-            *self,
-            State::HunkHeader(_, _) | State::HunkZero | State::HunkMinus(_) | State::HunkPlus(_)
-        )
-    }
+lazy_static! {
+    static ref IS_WORD_DIFF: bool = match process::calling_process().as_deref() {
+        Some(
+            CallingProcess::GitDiff(cmd_line)
+            | CallingProcess::GitShow(cmd_line, _)
+            | CallingProcess::GitLog(cmd_line)
+            | CallingProcess::GitReflog(cmd_line),
+        ) =>
+            cmd_line.long_options.contains("--word-diff")
+                || cmd_line.long_options.contains("--word-diff-regex")
+                || cmd_line.long_options.contains("--color-words"),
+        _ => false,
+    };
 }
 
 impl<'a> StateMachine<'a> {
     #[inline]
     fn test_hunk_line(&self) -> bool {
-        self.state.is_in_hunk()
+        matches!(
+            self.state,
+            State::HunkHeader(_, _) | State::HunkZero | State::HunkMinus(_) | State::HunkPlus(_)
+        ) && !&*IS_WORD_DIFF
     }
 
     /// Handle a hunk line, i.e. a minus line, a plus line, or an unchanged line.
@@ -44,6 +56,8 @@ impl<'a> StateMachine<'a> {
         self.state = match self.line.chars().next() {
             Some('-') => {
                 if let State::HunkPlus(_) = self.state {
+                    // We have just entered a new subhunk; process the previous one
+                    // and flush the line buffers.
                     self.painter.paint_buffered_minus_and_plus_lines();
                 }
                 let state = match self.config.inspect_raw_lines {
@@ -88,6 +102,10 @@ impl<'a> StateMachine<'a> {
                 // The first character here could be e.g. '\' from '\ No newline at end of file'. This
                 // is not a hunk line, but the parser does not have a more accurate state corresponding
                 // to this.
+
+                // We are in a zero (unchanged) line, therefore we have just exited a subhunk (a
+                // sequence of consecutive minus (removed) and/or plus (added) lines). Process that
+                // subhunk and flush the line buffers.
                 self.painter.paint_buffered_minus_and_plus_lines();
                 self.painter
                     .output_buffer
diff --git a/src/paint.rs b/src/paint.rs
index a3f8f41..89593bd 100644
--- a/src/paint.rs
+++ b/src/paint.rs
@@ -15,8 +15,8 @@ use crate::features::line_numbers;
 use crate::features::side_by_side::ansifill;
 use crate::features::side_by_side::{self, PanelSide};
 use crate::minusplus::*;
+use crate::paint::superimpose_style_sections::superimpose_style_sections;
 use crate::style::Style;
-use crate::utils::bat::terminal::to_ansi_color;
 use crate::{ansi, style};
 
 pub type LineSections<'a, S> = Vec<(S, &'a str)>;
@@ -161,43 +161,49 @@ impl<'p> Painter<'p> {
     }
 
     pub fn paint_buffered_minus_and_plus_lines(&mut self) {
-        let minus_line_syntax_style_sections = Self::get_syntax_style_sections_for_lines(
-            &self.minus_lines,
-            self.highlighter.as_mut(),
-            self.config,
+        let lines = MinusPlus::new(&self.minus_lines, &self.plus_lines);
+        let syntax_style_sections = MinusPlus::new(
+            Self::get_syntax_style_sections_for_lines(
+                lines[Minus],
+                self.highlighter.as_mut(),
+                self.config,
+            ),
+            Self::get_syntax_style_sections_for_lines(
+                lines[Plus],
+                self.highlighter.as_mut(),
+                self.config,
+            ),
         );
-        let plus_line_syntax_style_sections = Self::get_syntax_style_sections_for_lines(
-            &self.plus_lines,
-            self.highlighter.as_mut(),
+        let (mut diff_style_sections, line_alignment) =
+            Self::get_diff_style_sections(&lines, self.config);
+        let lines_have_homolog = edits::make_lines_have_homolog(&line_alignment);
+
+        Self::update_diff_style_sections(
+            &lines,
+            &mut diff_style_sections,
+            &lines_have_homolog,
             self.config,
         );
-        let (minus_line_diff_style_sections, plus_line_diff_style_sections, line_alignment) =
-            Self::get_diff_style_sections(&self.minus_lines, &self.plus_lines, self.config);
 
         if self.config.side_by_side {
             side_by_side::paint_minus_and_plus_lines_side_by_side(
-                MinusPlus::new(&self.minus_lines, &self.plus_lines),
-                MinusPlus::new(
-                    minus_line_syntax_style_sections,
-                    plus_line_syntax_style_sections,
-                ),
-                MinusPlus::new(
-                    minus_line_diff_style_sections,
-                    plus_line_diff_style_sections,
-                ),
+                lines,
+                syntax_style_sections,
+                diff_style_sections,
+                lines_have_homolog,
                 line_alignment,
                 &mut self.line_numbers_data,
                 &mut self.output_buffer,
                 self.config,
             )
         } else {
-            // Unified mode:
-
+            // Unified diff mode:
             if !self.minus_lines.is_empty() {
                 Painter::paint_lines(
-                    minus_line_syntax_style_sections,
-                    minus_line_diff_style_sections,
-                    self.minus_lines.iter().map(|(_, state)| state),
+                    lines[Minus],
+                    &syntax_style_sections[Minus],
+                    &diff_style_sections[Minus],
+                    &lines_have_homolog[Minus],
                     &mut self.output_buffer,
                     self.config,
                     &mut self.line_numbers_data.as_mut(),
@@ -212,9 +218,10 @@ impl<'p> Painter<'p> {
             }
             if !self.plus_lines.is_empty() {
                 Painter::paint_lines(
-                    plus_line_syntax_style_sections,
-                    plus_line_diff_style_sections,
-                    self.plus_lines.iter().map(|(_, state)| state),
+                    lines[Plus],
+                    &syntax_style_sections[Plus],
+                    &diff_style_sections[Plus],
+                    &lines_have_homolog[Plus],
                     &mut self.output_buffer,
                     self.config,
                     &mut self.line_numbers_data.as_mut(),
@@ -241,7 +248,7 @@ impl<'p> Painter<'p> {
             None
         };
 
-        let lines = vec![(self.prepare(line), state.clone())];
+        let lines = vec![(self.prepare(line), state)];
         let syntax_style_sections = Painter::get_syntax_style_sections_for_lines(
             &lines,
             self.highlighter.as_mut(),
@@ -263,9 +270,10 @@ impl<'p> Painter<'p> {
             );
         } else {
             Painter::paint_lines(
-                syntax_style_sections,
-                vec![diff_style_sections],
-                [state].iter(),
+                &lines,
+                &syntax_style_sections,
+                &[diff_style_sections],
+                &[false],
                 &mut self.output_buffer,
                 self.config,
                 &mut self.line_numbers_data.as_mut(),
@@ -280,9 +288,10 @@ impl<'p> Painter<'p> {
     /// highlighting styles, and write colored lines to output buffer.
     #[allow(clippy::too_many_arguments)]
     pub fn paint_lines<'a>(
-        syntax_style_sections: Vec<LineSections<'a, SyntectStyle>>,
-        diff_style_sections: Vec<LineSections<'a, Style>>,
-        states: impl Iterator<Item = &'a State>,
+        lines: &'a [(String, State)],
+        syntax_style_sections: &[LineSections<'a, SyntectStyle>],
+        diff_style_sections: &[LineSections<'a, Style>],
+        lines_have_homolog: &[bool],
         output_buffer: &mut String,
         config: &config::Config,
         line_numbers_data: &mut Option<&mut line_numbers::LineNumbersData>,
@@ -297,11 +306,12 @@ impl<'p> Painter<'p> {
         // 2. We must ensure that we fill rightwards with the appropriate
         //    non-emph background color. In that case we don't use the last
         //    style of the line, because this might be emph.
-        for (state, (syntax_sections, diff_sections)) in states.zip_eq(
-            syntax_style_sections
-                .iter()
-                .zip_eq(diff_style_sections.iter()),
-        ) {
+        for ((((_, state), syntax_sections), diff_sections), &line_has_homolog) in lines
+            .iter()
+            .zip_eq(syntax_style_sections)
+            .zip_eq(diff_style_sections)
+            .zip_eq(lines_have_homolog)
+        {
             let (mut line, line_is_empty) = Painter::paint_line(
                 syntax_sections,
                 diff_sections,
@@ -314,6 +324,7 @@ impl<'p> Painter<'p> {
             let (bg_fill_mode, fill_style) =
                 Painter::get_should_right_fill_background_color_and_fill_style(
                     diff_sections,
+                    Some(line_has_homolog),
                     state,
                     background_color_extends_to_terminal_width,
                     config,
@@ -354,7 +365,7 @@ impl<'p> Painter<'p> {
         state: State,
         background_color_extends_to_terminal_width: BgShouldFill,
     ) {
-        let lines = vec![(self.expand_tabs(line.graphemes(true)), state.clone())];
+        let lines = vec![(self.expand_tabs(line.graphemes(true)), state)];
         let syntax_style_sections = Painter::get_syntax_style_sections_for_lines(
             &lines,
             self.highlighter.as_mut(),
@@ -365,9 +376,10 @@ impl<'p> Painter<'p> {
             StyleSectionSpecifier::StyleSections(style_sections) => vec![style_sections],
         };
         Painter::paint_lines(
-            syntax_style_sections,
-            diff_style_sections,
-            [state].iter(),
+            &lines,
+            &syntax_style_sections,
+            &diff_style_sections,
+            &[false],
             &mut self.output_buffer,
             self.config,
             &mut None,
@@ -381,35 +393,42 @@ impl<'p> Painter<'p> {
     /// the style for doing so.
     pub fn get_should_right_fill_background_color_and_fill_style(
         diff_sections: &[(Style, &str)],
+        line_has_homolog: Option<bool>,
         state: &State,
         background_color_extends_to_terminal_width: BgShouldFill,
         config: &config::Config,
     ) -> (Option<BgFillMethod>, Style) {
-        // style:          for right fill if line contains no emph sections
-        // non_emph_style: for right fill if line contains emph sections
-        let (style, non_emph_style) = match state {
+        let fill_style = match state {
             State::HunkMinus(None) | State::HunkMinusWrapped => {
-                (config.minus_style, config.minus_non_emph_style)
+                if let Some(true) = line_has_homolog {
+                    config.minus_non_emph_style
+                } else {
+                    config.minus_style
+                }
             }
-            State::HunkZero | State::HunkZeroWrapped => (config.zero_style, config.zero_style),
+            State::HunkZero | State::HunkZeroWrapped => config.zero_style,
             State::HunkPlus(None) | State::HunkPlusWrapped => {
-                (config.plus_style, config.plus_non_emph_style)
+                if let Some(true) = line_has_homolog {
+                    config.plus_non_emph_style
+                } else {
+                    config.plus_style
+                }
             }
             State::HunkMinus(Some(_)) | State::HunkPlus(Some(_)) => {
-                let style = if !diff_sections.is_empty() {
-                    diff_sections[diff_sections.len() - 1].0
-                } else {
-                    config.null_style
-                };
-                (style, style)
+                // Consider the following raw line, from git colorMoved:
+                // ␛[1;36m+␛[m␛[1;36mclass·X:·pass␛[m␊ The last style section returned by
+                // parse_style_sections will be a default style associated with the terminal newline
+                // character; we want the last "real" style.
+                diff_sections
+                    .iter()
+                    .rev()
+                    .filter(|(_, s)| s != &"\n")
+                    .map(|(style, _)| *style)
+                    .next()
+                    .unwrap_or(config.null_style)
             }
-            State::Blame(_, _) => (diff_sections[0].0, diff_sections[0].0),
-            _ => (config.null_style, config.null_style),
-        };
-        let fill_style = if style_sections_contain_more_than_one_style(diff_sections) {
-            non_emph_style // line contains an emph section
-        } else {
-            style
+            State::Blame(_, _) => diff_sections[0].0,
+            _ => config.null_style,
         };
 
         match (
@@ -589,62 +608,40 @@ impl<'p> Painter<'p> {
         line_sections
     }
 
-    /// Set background styles to represent diff for minus and plus lines in buffer.
+    /// Get background styles to represent diff for minus and plus lines in buffer.
     #[allow(clippy::type_complexity)]
     fn get_diff_style_sections<'a>(
-        minus_lines_and_states: &'a [(String, State)],
-        plus_lines_and_states: &'a [(String, State)],
+        lines: &MinusPlus<&'a Vec<(String, State)>>,
         config: &config::Config,
     ) -> (
-        Vec<LineSections<'a, Style>>,
-        Vec<LineSections<'a, Style>>,
+        MinusPlus<Vec<LineSections<'a, Style>>>,
         Vec<(Option<usize>, Option<usize>)>,
     ) {
-        let (minus_lines, minus_styles): (Vec<&str>, Vec<Style>) = minus_lines_and_states
+        let (minus_lines, minus_styles): (Vec<&str>, Vec<Style>) = lines[Minus]
             .iter()
-            .map(|(s, t)| (s.as_str(), *config.get_style(t)))
+            .map(|(s, state)| (s.as_str(), *config.get_style(state)))
             .unzip();
-        let (plus_lines, plus_styles): (Vec<&str>, Vec<Style>) = plus_lines_and_states
+        let (plus_lines, plus_styles): (Vec<&str>, Vec<Style>) = lines[Plus]
             .iter()
-            .map(|(s, t)| (s.as_str(), *config.get_style(t)))
+            .map(|(s, state)| (s.as_str(), *config.get_style(state)))
             .unzip();
-        let mut diff_sections = edits::infer_edits(
-            minus_lines,
-            plus_lines,
-            minus_styles,
-            config.minus_emph_style, // FIXME
-            plus_styles,
-            config.plus_emph_style, // FIXME
-            &config.tokenization_regex,
-            config.max_line_distance,
-            config.max_line_distance_for_naively_paired_lines,
-        );
-        let minus_non_emph_style = if config.minus_non_emph_style != config.minus_emph_style {
-            Some(config.minus_non_emph_style)
-        } else {
-            None
-        };
-        let mut lines_style_sections = MinusPlus::new(&mut diff_sections.0, &mut diff_sections.1);
-        Self::update_styles(
-            minus_lines_and_states,
-            lines_style_sections[Minus],
-            None,
-            minus_non_emph_style,
-            config,
-        );
-        let plus_non_emph_style = if config.plus_non_emph_style != config.plus_emph_style {
-            Some(config.plus_non_emph_style)
-        } else {
-            None
-        };
-        Self::update_styles(
-            plus_lines_and_states,
-            lines_style_sections[Plus],
-            Some(config.whitespace_error_style),
-            plus_non_emph_style,
-            config,
+        let (minus_line_diff_style_sections, plus_line_diff_style_sections, line_alignment) =
+            edits::infer_edits(
+                minus_lines,
+                plus_lines,
+                minus_styles,
+                config.minus_emph_style, // FIXME
+                plus_styles,
+                config.plus_emph_style, // FIXME
+                &config.tokenization_regex,
+                config.max_line_distance,
+                config.max_line_distance_for_naively_paired_lines,
+            );
+        let diff_sections = MinusPlus::new(
+            minus_line_diff_style_sections,
+            plus_line_diff_style_sections,
         );
-        diff_sections
+        (diff_sections, line_alignment)
     }
 
     /// There are some rules according to which we update line section styles that were computed
@@ -659,14 +656,51 @@ impl<'p> Painter<'p> {
     ///    are going to be preserved in the output, then replace delta's
     ///    computed diff styles with these styles from the raw line. (This is
     ///    how support for git's --color-moved is implemented.)
+    fn update_diff_style_sections<'a>(
+        lines: &MinusPlus<&'a Vec<(String, State)>>,
+        lines_style_sections: &mut MinusPlus<Vec<LineSections<'a, Style>>>,
+        lines_have_homolog: &MinusPlus<Vec<bool>>,
+        config: &config::Config,
+    ) {
+        Self::update_styles(
+            lines[Minus],
+            &mut lines_style_sections[Minus],
+            None,
+            if config.minus_non_emph_style != config.minus_emph_style {
+                Some(config.minus_non_emph_style)
+            } else {
+                None
+            },
+            &lines_have_homolog[Minus],
+            config,
+        );
+        Self::update_styles(
+            lines[Plus],
+            &mut lines_style_sections[Plus],
+            Some(config.whitespace_error_style),
+            if config.plus_non_emph_style != config.plus_emph_style {
+                Some(config.plus_non_emph_style)
+            } else {
+                None
+            },
+            &lines_have_homolog[Plus],
+            config,
+        );
+    }
+
     fn update_styles<'a>(
-        lines_and_states: &'a [(String, State)],
+        lines: &'a [(String, State)],
         lines_style_sections: &mut Vec<LineSections<'a, Style>>,
         whitespace_error_style: Option<Style>,
         non_emph_style: Option<Style>,
+        lines_have_homolog: &[bool],
         config: &config::Config,
     ) {
-        for ((_, state), style_sections) in lines_and_states.iter().zip(lines_style_sections) {
+        for (((_, state), style_sections), line_has_homolog) in lines
+            .iter()
+            .zip_eq(lines_style_sections)
+            .zip_eq(lines_have_homolog)
+        {
             match state {
                 State::HunkMinus(Some(raw_line)) | State::HunkPlus(Some(raw_line)) => {
                     *style_sections = parse_style_sections(raw_line, config);
@@ -676,14 +710,16 @@ impl<'p> Painter<'p> {
             };
             let line_has_emph_and_non_emph_sections =
                 style_sections_contain_more_than_one_style(style_sections);
-            let should_update_non_emph_styles =
-                non_emph_style.is_some() && line_has_emph_and_non_emph_sections;
+            let should_update_non_emph_styles = non_emph_style.is_some() && *line_has_homolog;
             let is_whitespace_error =
                 whitespace_error_style.is_some() && is_whitespace_error(style_sections);
             for (style, _) in style_sections.iter_mut() {
                 // If the line as a whole constitutes a whitespace error then highlight this
                 // section if either (a) it is an emph section, or (b) the line lacks any
                 // emph/non-emph distinction.
+
+                // TODO: is this logic correct now, after introducing
+                // line_has_homolog for non_emph style?
                 if is_whitespace_error && (style.is_emph || !line_has_emph_and_non_emph_sections) {
                     *style = whitespace_error_style.unwrap();
                 }
@@ -807,201 +843,231 @@ fn is_whitespace_error(sections: &[(Style, &str)]) -> bool {
     false
 }
 
-// We have two different annotations of the same line:
-// `syntax_style_sections` contains foreground styles computed by syntect,
-// and `diff_style_sections` contains styles computed by delta reflecting
-// within-line edits. The delta styles may assign a foreground color, or
-// they may indicate that the foreground color comes from syntax
-// highlighting (the is_syntax_highlighting attribute on style::Style). This
-// function takes in the two input streams and outputs one stream with a
-// single style assigned to each character.
-pub fn superimpose_style_sections(
-    syntax_style_sections: &[(SyntectStyle, &str)],
-    diff_style_sections: &[(Style, &str)],
-    true_color: bool,
-    null_syntect_style: SyntectStyle,
-) -> Vec<(Style, String)> {
-    let superimpose_styles = |syntect_style: SyntectStyle, style: Style| {
-        if style.is_syntax_highlighted && syntect_style != null_syntect_style {
-            Style {
-                ansi_term_style: ansi_term::Style {
-                    foreground: to_ansi_color(syntect_style.foreground, true_color),
-                    ..style.ansi_term_style
-                },
-                ..style
+mod superimpose_style_sections {
+    use syntect::highlighting::Style as SyntectStyle;
+
+    use crate::style::Style;
+    use crate::utils::bat::terminal::to_ansi_color;
+
+    // We have two different annotations of the same line:
+    // `syntax_style_sections` contains foreground styles computed by syntect,
+    // and `diff_style_sections` contains styles computed by delta reflecting
+    // within-line edits. The delta styles may assign a foreground color, or
+    // they may indicate that the foreground color comes from syntax
+    // highlighting (the is_syntax_highlighting attribute on style::Style). This
+    // function takes in the two input streams and outputs one stream with a
+    // single style assigned to each character.
+    pub fn superimpose_style_sections(
+        syntax_style_sections: &[(SyntectStyle, &str)],
+        diff_style_sections: &[(Style, &str)],
+        true_color: bool,
+        null_syntect_style: SyntectStyle,
+    ) -> Vec<(Style, String)> {
+        coalesce(
+            superimpose(
+                explode(syntax_style_sections)
+                    .iter()
+                    .zip(explode(diff_style_sections))
+                    .collect::<Vec<(&(SyntectStyle, char), (Style, char))>>(),
+            ),
+            true_color,
+            null_syntect_style,
+        )
+    }
+
+    fn explode<T>(style_sections: &[(T, &str)]) -> Vec<(T, char)>
+    where
+        T: Copy,
+    {
+        let mut exploded: Vec<(T, char)> = Vec::new();
+        for (style, s) in style_sections {
+            for c in s.chars() {
+                exploded.push((*style, c));
             }
-        } else {
-            style
         }
-    };
+        exploded
+    }
 
-    #[cfg(test)]
-    {
-        let syntax_concat: String = syntax_style_sections
-            .iter()
-            .flat_map(|(_, s)| s.chars())
-            .collect();
-        let diff_concat: String = diff_style_sections
-            .iter()
-            .flat_map(|(_, s)| s.chars())
-            .collect();
-        assert_eq!(syntax_concat, diff_concat)
-    }
-
-    let mut superimposed = Vec::new();
-    if syntax_style_sections.is_empty() {
-        return superimposed;
-    }
-    let mut chars = syntax_style_sections.iter().flat_map(|(_, s)| s.chars()); // arbitrary choice
-    let mut sections_1 = syntax_style_sections.iter();
-    let mut sections_2 = diff_style_sections.iter();
-    // sections_1 and sections_2 are both arrays of (style, substring) pairs. The concatenation
-    // of the substrings from sections_1 equals the concatenation of the substrings from
-    // sections_2 (i.e. the entire line). We align the two sequences of sections and move
-    // through from left to right, stopping at each section end to release a superimposed style
-    // applied to the unreleased chunk to our left.
-    let (mut style_1, s_1) = sections_1.next().unwrap();
-    let (mut style_2, s_2) = sections_2.next().unwrap();
-    let (mut end_1, mut end_2, mut next_change) = (s_1.len(), s_2.len(), 0);
-    let (mut pos, mut last_style, mut done) = (0, None, false);
-    loop {
-        let superimposed_style = superimpose_styles(style_1, style_2);
-        if end_1 <= end_2 {
-            next_change = end_1;
-            if let Some((style, s)) = sections_1.next() {
-                style_1 = *style;
-                end_1 = next_change + s.len();
-            } else {
-                done = true;
+    #[allow(clippy::type_complexity)]
+    fn superimpose(
+        style_section_pairs: Vec<(&(SyntectStyle, char), (Style, char))>,
+    ) -> Vec<((SyntectStyle, Style), char)> {
+        let mut superimposed: Vec<((SyntectStyle, Style), char)> = Vec::new();
+        for ((syntax_style, char_1), (style, char_2)) in style_section_pairs {
+            if *char_1 != char_2 {
+                panic!(
+                    "String mismatch encountered while superimposing style sections: '{}' vs '{}'",
+                    *char_1, char_2
+                )
             }
+            superimposed.push(((*syntax_style, style), *char_1));
         }
-        if end_2 <= end_1 {
-            next_change = end_2;
-            if let Some((style, s)) = sections_2.next() {
-                style_2 = *style;
-                end_2 = next_change + s.len();
+        superimposed
+    }
+
+    fn coalesce(
+        style_sections: Vec<((SyntectStyle, Style), char)>,
+        true_color: bool,
+        null_syntect_style: SyntectStyle,
+    ) -> Vec<(Style, String)> {
+        let make_superimposed_style = |(syntect_style, style): (SyntectStyle, Style)| {
+            if style.is_syntax_highlighted && syntect_style != null_syntect_style {
+                Style {
+                    ansi_term_style: ansi_term::Style {
+                        foreground: to_ansi_color(syntect_style.foreground, true_color),
+                        ..style.ansi_term_style
+                    },
+                    ..style
+                }
             } else {
-                done = true;
+                style
             }
+        };
+        let mut coalesced: Vec<(Style, String)> = Vec::new();
+        let mut style_sections = style_sections.iter();
+        if let Some((style_pair, c)) = style_sections.next() {
+            let mut current_string = c.to_string();
+            let mut current_style_pair = style_pair;
+            for (style_pair, c) in style_sections {
+                if style_pair != current_style_pair {
+                    let style = make_superimposed_style(*current_style_pair);
+                    coalesced.push((style, current_string));
+                    current_string = String::new();
+                    current_style_pair = style_pair;
+                }
+                current_string.push(*c);
+            }
+
+            // TODO: This is not the ideal location for the following code.
+            if current_string.ends_with('\n') {
+                // Remove the terminating newline whose presence was necessary for the syntax
+                // highlighter to work correctly.
+                current_string.truncate(current_string.len() - 1);
+            }
+            let style = make_superimposed_style(*current_style_pair);
+            coalesced.push((style, current_string));
         }
-        let mut substring = chars.by_ref().take(next_change - pos).join("");
-        // Don't emit the terminating newline character whose
-        // presence was necessary for the syntax highlighter to work correctly.
-        if substring.ends_with('\n') {
-            substring.truncate(substring.len() - 1)
-        }
-        if last_style == Some(superimposed_style) {
-            let (_, last_substring) = superimposed.pop().unwrap();
-            substring = format!("{}{}", last_substring, substring)
-        }
-        superimposed.push((superimposed_style, substring));
-        if done {
-            break superimposed;
-        } else {
-            last_style = Some(superimposed_style);
-            pos = next_change;
-        }
+        coalesced
     }
-}
 
-#[cfg(test)]
-mod tests {
-    use lazy_static::lazy_static;
+    #[cfg(test)]
+    mod tests {
+        use lazy_static::lazy_static;
 
-    use super::*;
-    use ansi_term::{self, Color};
-    use syntect::highlighting::Color as SyntectColor;
-    use syntect::highlighting::FontStyle as SyntectFontStyle;
-    use syntect::highlighting::Style as SyntectStyle;
+        use super::*;
+        use ansi_term::{self, Color};
+        use syntect::highlighting::Color as SyntectColor;
+        use syntect::highlighting::FontStyle as SyntectFontStyle;
+        use syntect::highlighting::Style as SyntectStyle;
 
-    use crate::style::{DecorationStyle, Style};
+        use crate::style::{DecorationStyle, Style};
 
-    lazy_static! {
-        static ref SYNTAX_STYLE: SyntectStyle = SyntectStyle {
-            foreground: SyntectColor::BLACK,
-            background: SyntectColor::BLACK,
-            font_style: SyntectFontStyle::BOLD,
-        };
-    }
-    lazy_static! {
-        static ref SYNTAX_HIGHLIGHTED_STYLE: Style = Style {
-            ansi_term_style: ansi_term::Style {
-                foreground: Some(Color::White),
-                background: Some(Color::White),
-                is_underline: true,
-                ..ansi_term::Style::new()
-            },
-            is_emph: false,
-            is_omitted: false,
-            is_raw: false,
-            is_syntax_highlighted: true,
-            decoration_style: DecorationStyle::NoDecoration,
-        };
-    }
-    lazy_static! {
-        static ref NON_SYNTAX_HIGHLIGHTED_STYLE: Style = Style {
-            ansi_term_style: ansi_term::Style {
-                foreground: Some(Color::White),
-                background: Some(Color::White),
-                is_underline: true,
-                ..ansi_term::Style::new()
-            },
-            is_emph: false,
-            is_omitted: false,
-            is_raw: false,
-            is_syntax_highlighted: false,
-            decoration_style: DecorationStyle::NoDecoration,
-        };
-    }
-    lazy_static! {
-        static ref SUPERIMPOSED_STYLE: Style = Style {
-            ansi_term_style: ansi_term::Style {
-                foreground: to_ansi_color(SyntectColor::BLACK, true),
-                background: Some(Color::White),
-                is_underline: true,
-                ..ansi_term::Style::new()
-            },
-            is_emph: false,
-            is_omitted: false,
-            is_raw: false,
-            is_syntax_highlighted: true,
-            decoration_style: DecorationStyle::NoDecoration,
-        };
-    }
+        lazy_static! {
+            static ref SYNTAX_STYLE: SyntectStyle = SyntectStyle {
+                foreground: SyntectColor::BLACK,
+                background: SyntectColor::BLACK,
+                font_style: SyntectFontStyle::BOLD,
+            };
+        }
+        lazy_static! {
+            static ref SYNTAX_HIGHLIGHTED_STYLE: Style = Style {
+                ansi_term_style: ansi_term::Style {
+                    foreground: Some(Color::White),
+                    background: Some(Color::White),
+                    is_underline: true,
+                    ..ansi_term::Style::new()
+                },
+                is_emph: false,
+                is_omitted: false,
+                is_raw: false,
+                is_syntax_highlighted: true,
+                decoration_style: DecorationStyle::NoDecoration,
+            };
+        }
+        lazy_static! {
+            static ref NON_SYNTAX_HIGHLIGHTED_STYLE: Style = Style {
+                ansi_term_style: ansi_term::Style {
+                    foreground: Some(Color::White),
+                    background: Some(Color::White),
+                    is_underline: true,
+                    ..ansi_term::Style::new()
+                },
+                is_emph: false,
+                is_omitted: false,
+                is_raw: false,
+                is_syntax_highlighted: false,
+                decoration_style: DecorationStyle::NoDecoration,
+            };
+        }
+        lazy_static! {
+            static ref SUPERIMPOSED_STYLE: Style = Style {
+                ansi_term_style: ansi_term::Style {
+                    foreground: to_ansi_color(SyntectColor::BLACK, true),
+                    background: Some(Color::White),
+                    is_underline: true,
+                    ..ansi_term::Style::new()
+                },
+                is_emph: false,
+                is_omitted: false,
+                is_raw: false,
+                is_syntax_highlighted: true,
+                decoration_style: DecorationStyle::NoDecoration,
+            };
+        }
 
-    #[test]
-    fn test_superimpose_style_sections_1() {
-        let sections_1 = vec![(*SYNTAX_STYLE, "ab")];
-        let sections_2 = vec![(*SYNTAX_HIGHLIGHTED_STYLE, "ab")];
-        let superimposed = vec![(*SUPERIMPOSED_STYLE, "ab".to_string())];
-        assert_eq!(
-            superimpose_style_sections(&sections_1, &sections_2, true, SyntectStyle::default()),
-            superimposed
-        );
-    }
+        #[test]
+        fn test_superimpose_style_sections_1() {
+            let sections_1 = vec![(*SYNTAX_STYLE, "ab")];
+            let sections_2 = vec![(*SYNTAX_HIGHLIGHTED_STYLE, "ab")];
+            let superimposed = vec![(*SUPERIMPOSED_STYLE, "ab".to_string())];
+            assert_eq!(
+                superimpose_style_sections(&sections_1, &sections_2, true, SyntectStyle::default()),
+                superimposed
+            );
+        }
 
-    #[test]
-    fn test_superimpose_style_sections_2() {
-        let sections_1 = vec![(*SYNTAX_STYLE, "ab")];
-        let sections_2 = vec![
-            (*SYNTAX_HIGHLIGHTED_STYLE, "a"),
-            (*SYNTAX_HIGHLIGHTED_STYLE, "b"),
-        ];
-        let superimposed = vec![(*SUPERIMPOSED_STYLE, String::from("ab"))];
-        assert_eq!(
-            superimpose_style_sections(&sections_1, &sections_2, true, SyntectStyle::default()),
-            superimposed
-        );
-    }
+        #[test]
+        fn test_superimpose_style_sections_2() {
+            let sections_1 = vec![(*SYNTAX_STYLE, "ab")];
+            let sections_2 = vec![
+                (*SYNTAX_HIGHLIGHTED_STYLE, "a"),
+                (*SYNTAX_HIGHLIGHTED_STYLE, "b"),
+            ];
+            let superimposed = vec![(*SUPERIMPOSED_STYLE, String::from("ab"))];
+            assert_eq!(
+                superimpose_style_sections(&sections_1, &sections_2, true, SyntectStyle::default()),
+                superimposed
+            );
+        }
 
-    #[test]
-    fn test_superimpose_style_sections_3() {
-        let sections_1 = vec![(*SYNTAX_STYLE, "ab")];
-        let sections_2 = vec![(*NON_SYNTAX_HIGHLIGHTED_STYLE, "ab")];
-        let superimposed = vec![(*NON_SYNTAX_HIGHLIGHTED_STYLE, "ab".to_string())];
-        assert_eq!(
-            superimpose_style_sections(&sections_1, &sections_2, true, SyntectStyle::default()),
-            superimposed
-        );
+        #[test]
+        fn test_superimpose_style_sections_3() {
+            let sections_1 = vec![(*SYNTAX_STYLE, "ab")];
+            let sections_2 = vec![(*NON_SYNTAX_HIGHLIGHTED_STYLE, "ab")];
+            let superimposed = vec![(*NON_SYNTAX_HIGHLIGHTED_STYLE, "ab".to_string())];
+            assert_eq!(
+                superimpose_style_sections(&sections_1, &sections_2, true, SyntectStyle::default()),
+                superimposed
+            );
+        }
+
+        #[test]
+        fn test_explode() {
+            let arbitrary = 0;
+            assert_eq!(
+                explode(&[(arbitrary, "ab")]),
+                vec![(arbitrary, 'a'), (arbitrary, 'b')]
+            )
+        }
+
+        #[test]
+        fn test_superimpose() {
+            let x = (*SYNTAX_STYLE, 'a');
+            let pairs = vec![(&x, (*SYNTAX_HIGHLIGHTED_STYLE, 'a'))];
+            assert_eq!(
+                superimpose(pairs),
+                vec![((*SYNTAX_STYLE, *SYNTAX_HIGHLIGHTED_STYLE), 'a')]
+            );
+        }
     }
 }
diff --git a/src/utils/process.rs b/src/utils/process.rs
index 124defa..63021b0 100644
--- a/src/utils/process.rs
+++ b/src/utils/process.rs
@@ -7,9 +7,20 @@ use lazy_static::lazy_static;
 
 #[derive(Clone, Debug, PartialEq)]
 pub enum CallingProcess {
-    GitShow(String),                             // (extension)
-    GitGrep((HashSet<String>, HashSet<String>)), // ((long_options, short_options))
-    OtherGrep,                                   // rg, grep, ag, ack, etc
+    GitDiff(CommandLine),
+    GitShow(CommandLine, Option<String>), // element 2 is file extension
+    GitLog(CommandLine),
+    GitReflog(CommandLine),
+    GitGrep(CommandLine),
+    OtherGrep, // rg, grep, ag, ack, etc
+}
+// TODO: Git blame is currently handled differently
+
+#[derive(Clone, Debug, PartialEq)]
+pub struct CommandLine {
+    pub long_options: HashSet<String>,
+    pub short_options: HashSet<String>,
+    last_arg: Option<String>,
 }
 
 pub fn calling_process() -> Option<Cow<'static, CallingProcess>> {
@@ -46,53 +57,73 @@ pub enum ProcessArgs<T> {
 }
 
 pub fn git_blame_filename_extension() -> Option<String> {
-    calling_process_cmdline(ProcInfo::new(), blame::guess_git_blame_filename_extension)
+    calling_process_cmdline(ProcInfo::new(), guess_git_blame_filename_extension)
 }
 
-mod blame {
-    use super::*;
-
-    pub fn guess_git_blame_filename_extension(args: &[String]) -> ProcessArgs<String> {
-        let all_args = args.iter().map(|s| s.as_str());
+pub fn guess_git_blame_filename_extension(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";
+    // 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(' '));
+    let selected_args =
+        skip_uninteresting_args(all_args, git_blame_options_with_parameter.split(' '));
 
-        match selected_args.as_slice() {
-            [_git, "blame", .., last_arg] => match last_arg.split('.').last() {
-                Some(arg) => ProcessArgs::Args(arg.to_string()),
-                None => ProcessArgs::ArgError,
-            },
-            [_git, "blame"] => ProcessArgs::ArgError,
-            _ => ProcessArgs::OtherProcess,
-        }
+    match selected_args.as_slice() {
+        [git, "blame", .., last_arg] if is_git_binary(git) => match last_arg.split('.').last() {
+            Some(arg) => ProcessArgs::Args(arg.to_string()),
+            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());
 
+    fn is_any_of<'a, I>(cmd: Option<&str>, others: I) -> bool
+    where
+        I: IntoIterator<Item = &'a str>,
+    {
+        cmd.map(|cmd| others.into_iter().any(|o| o.eq_ignore_ascii_case(cmd)))
+            .unwrap_or(false)
+    }
+
     match args.next() {
         Some(command) => match Path::new(command).file_stem() {
-            Some(s) if s.to_str() == Some("git") => {
-                let mut args = args.skip_while(|s| *s != "grep" && *s != "show");
+            Some(s) if s.to_str().map(|s| is_git_binary(s)).unwrap_or(false) => {
+                let mut args = args.skip_while(|s| {
+                    *s != "diff" && *s != "show" && *s != "log" && *s != "reflog" && *s != "grep"
+                });
                 match args.next() {
-                    Some("grep") => {
-                        ProcessArgs::Args(CallingProcess::GitGrep(parse_command_option_keys(args)))
+                    Some("diff") => {
+                        ProcessArgs::Args(CallingProcess::GitDiff(parse_command_line(args)))
                     }
                     Some("show") => {
-                        if let Some(extension) = get_git_show_file_extension(args) {
-                            ProcessArgs::Args(CallingProcess::GitShow(extension.to_string()))
+                        let command_line = parse_command_line(args);
+                        let extension = if let Some(last_arg) = &command_line.last_arg {
+                            match last_arg.split_once(':') {
+                                Some((_, suffix)) => {
+                                    suffix.split('.').last().map(|s| s.to_string())
+                                }
+                                None => None,
+                            }
                         } else {
-                            // It's git show, but we failed to determine the
-                            // file extension. Don't look at any more processes.
-                            ProcessArgs::ArgError
-                        }
+                            None
+                        };
+                        ProcessArgs::Args(CallingProcess::GitShow(command_line, extension))
+                    }
+                    Some("log") => {
+                        ProcessArgs::Args(CallingProcess::GitLog(parse_command_line(args)))
+                    }
+                    Some("reflog") => {
+                        ProcessArgs::Args(CallingProcess::GitReflog(parse_command_line(args)))
+                    }
+                    Some("grep") => {
+                        ProcessArgs::Args(CallingProcess::GitGrep(parse_command_line(args)))
                     }
                     _ => {
                         // It's git, but not a subcommand that we parse. Don't
@@ -101,18 +132,16 @@ pub fn describe_calling_process(args: &[String]) -> ProcessArgs<CallingProcess>
                     }
                 }
             }
-            Some(s) => match s.to_str() {
-                // TODO: parse_style_sections is failing to parse ANSI escape sequences emitted by
-                // grep (BSD and GNU), ag, pt. See #794
-                Some("rg") | Some("ack") | Some("sift") => {
-                    ProcessArgs::Args(CallingProcess::OtherGrep)
-                }
-                _ => {
-                    // It's not git, and it's not another grep tool. Keep
-                    // looking at other processes.
-                    ProcessArgs::OtherProcess
-                }
-            },
+            // TODO: parse_style_sections is failing to parse ANSI escape sequences emitted by
+            // grep (BSD and GNU), ag, pt. See #794
+            Some(s) if is_any_of(s.to_str(), ["rg", "ack", "sift"]) => {
+                ProcessArgs::Args(CallingProcess::OtherGrep)
+            }
+            Some(_) => {
+                // It's not git, and it's not another grep tool. Keep
+                // looking at other processes.
+                ProcessArgs::OtherProcess
+            }
             _ => {
                 // Could not parse file stem (not expected); keep looking at
                 // other processes.
@@ -126,16 +155,13 @@ pub fn describe_calling_process(args: &[String]) -> ProcessArgs<CallingProcess>
     }
 }
 
-fn get_git_show_file_extension<'a>(args: impl Iterator<Item = &'a str>) -> Option<&'a str> {
-    if let Some(last_arg) = skip_uninteresting_args(args, "".split(' ')).last() {
-        // E.g. "HEAD~1:Makefile" or "775c3b8:./src/delta.rs"
-        match last_arg.split_once(':') {
-            Some((_, suffix)) => suffix.split('.').last(),
-            None => None,
-        }
-    } else {
-        None
-    }
+fn is_git_binary(git: &str) -> bool {
+    // Ignore case, for e.g. NTFS or APFS file systems
+    Path::new(git)
+        .file_stem()
+        .and_then(|os_str| os_str.to_str())
+        .map(|s| s.eq_ignore_ascii_case("git"))
+        .unwrap_or(false)
 }
 
 // Skip all arguments starting with '-' from `args_it`. Also skip all arguments listed in
@@ -173,22 +199,28 @@ where
 
 // Given `--aa val -bc -d val e f -- ...` return
 // ({"--aa"}, {"-b", "-c", "-d"})
-fn parse_command_option_keys<'a>(
-    args: impl Iterator<Item = &'a str>,
-) -> (HashSet<String>, HashSet<String>) {
-    let mut longs = HashSet::new();
-    let mut shorts = HashSet::new();
+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;
 
     for s in args {
         if s == "--" {
             break;
         } else if s.starts_with("--") {
-            longs.insert(s.split('=').next().unwrap().to_owned());
+            long_options.insert(s.split('=').next().unwrap().to_owned());
         } else if let Some(suffix) = s.strip_prefix('-') {
-            shorts.extend(suffix.chars().map(|c| format!("-{}", c)));
+            short_options.extend(suffix.chars().map(|c| format!("-{}", c)));
+        } else {
+            last_arg = Some(s);
         }
     }
-    (longs, shorts)
+
+    CommandLine {
+        long_options,
+        short_options,
+        last_arg: last_arg.map(|s| s.to_string()),
+    }
 }
 
 struct ProcInfo {
@@ -339,13 +371,14 @@ where
 {
     #[cfg(test)]
     {
-        if let Some(args) = tests::cfg::WithArgs::get() {
+        if let Some(args) = tests::FakeParentArgs::get() {
             match extract_args(&args) {
                 ProcessArgs::Args(result) => return Some(result),
                 _ => return None,
             }
         }
     }
+
     let my_pid = info.my_pid();
 
     // 1) Try the parent process. If delta is set as the pager in git, then git is the parent process.
@@ -409,6 +442,10 @@ where
         P: ProcessInterface,
         F: FnMut(Pid, usize),
     {
+        // Probably bad input, not a tree:
+        if distance > 2000 {
+            return;
+        }
         if let Some(proc) = info.process(pid) {
             if let Some(pid) = proc.parent() {
                 f(pid, distance);
@@ -421,58 +458,106 @@ where
 
 #[cfg(test)]
 pub mod tests {
-    use super::blame::*;
+
     use super::*;
 
     use itertools::Itertools;
+    use std::cell::RefCell;
+    use std::rc::Rc;
 
-    pub mod cfg {
-        use std::cell::RefCell;
+    thread_local! {
+        static FAKE_ARGS: RefCell<TlsState<Vec<String>>> = RefCell::new(TlsState::None);
+    }
 
-        #[derive(Debug, PartialEq)]
-        enum TlsState<T> {
-            Some(T),
-            None,
-            Invalid,
-        }
+    #[derive(Debug, PartialEq)]
+    enum TlsState<T> {
+        Once(T),
+        Scope(T),
+        With(usize, Rc<Vec<T>>),
+        None,
+        Invalid,
+    }
 
-        thread_local! {
-            static FAKE_ARGS: RefCell<TlsState<Vec<String>>> = RefCell::new(TlsState::None);
+    // When calling `FakeParentArgs::get()`, it can return `Some(values)` which were set earlier
+    // during in the #[test]. Otherwise returns None.
+    // This value can be valid once: `FakeParentArgs::once(val)`, for the entire scope:
+    // `FakeParentArgs::for_scope(val)`, or can be different values everytime `get()` is called:
+    // `FakeParentArgs::with([val1, val2, val3])`.
+    // It is an error if `once` or `with` values remain unused, or are overused.
+    // Note: The values are stored per-thread, so the expectation is that no thread boundaries are
+    // crossed.
+    pub struct FakeParentArgs {}
+    impl FakeParentArgs {
+        pub fn once(args: &str) -> Self {
+            Self::new(args, |v| TlsState::Once(v), "once")
         }
-
-        pub struct WithArgs {}
-        impl WithArgs {
-            pub fn new(args: &str) -> Self {
-                let string_vec = args.split(' ').map(str::to_owned).collect();
-                assert!(
-                    FAKE_ARGS.with(|a| a.replace(TlsState::Some(string_vec))) != TlsState::Invalid,
-                    "test logic error (in new): wrong WithArgs scope?"
-                );
-                WithArgs {}
-            }
-            pub fn get() -> Option<Vec<String>> {
-                FAKE_ARGS.with(|a| {
-                    let old_value = a.replace_with(|old_value| match old_value {
-                        TlsState::Some(_) => TlsState::Invalid,
-                        TlsState::None => TlsState::None,
-                        TlsState::Invalid => TlsState::Invalid,
-                    });
-
-                    match old_value {
-                        TlsState::Some(args) => Some(args),
-                        TlsState::None => None,
-                        TlsState::Invalid => {
-                            panic!("test logic error (in get): wrong WithArgs scope?")
-                        }
-                    }
-                })
+        pub fn for_scope(args: &str) -> Self {
+            Self::new(args, |v| TlsState::Scope(v), "for_scope")
+        }
+        fn new<F>(args: &str, initial: F, from_: &str) -> Self
+        where
+            F: Fn(Vec<String>) -> TlsState<Vec<String>>,
+        {
+            let string_vec = args.split(' ').map(str::to_owned).collect();
+            if FAKE_ARGS.with(|a| a.replace(initial(string_vec))) != TlsState::None {
+                Self::error(from_);
             }
+            FakeParentArgs {}
         }
-        impl Drop for WithArgs {
-            fn drop(&mut self) {
-                // clears an invalid state
-                FAKE_ARGS.with(|a| a.replace(TlsState::None));
+        pub fn with(args: &[&str]) -> Self {
+            let with = TlsState::With(
+                0,
+                Rc::new(
+                    args.iter()
+                        .map(|a| a.split(' ').map(str::to_owned).collect())
+                        .collect(),
+                ),
+            );
+            if FAKE_ARGS.with(|a| a.replace(with)) != TlsState::None || args.is_empty() {
+                Self::error("with creation");
             }
+            FakeParentArgs {}
+        }
+        pub fn get() -> Option<Vec<String>> {
+            FAKE_ARGS.with(|a| {
+                let old_value = a.replace_with(|old_value| match old_value {
+                    TlsState::Once(_) => TlsState::Invalid,
+                    TlsState::Scope(args) => TlsState::Scope(args.clone()),
+                    TlsState::With(n, args) => TlsState::With(*n + 1, Rc::clone(args)),
+                    TlsState::None => TlsState::None,
+                    TlsState::Invalid => TlsState::Invalid,
+                });
+
+                match old_value {
+                    TlsState::Once(args) | TlsState::Scope(args) => Some(args),
+                    TlsState::With(n, args) if n < args.len() => Some(args[n].clone()),
+                    TlsState::None => None,
+                    TlsState::Invalid | TlsState::With(_, _) => Self::error("get"),
+                }
+            })
+        }
+        fn error(where_: &str) -> ! {
+            panic!(
+                "test logic error (in {}): wrong FakeParentArgs scope?",
+                where_
+            );
+        }
+    }
+    impl Drop for FakeParentArgs {
+        fn drop(&mut self) {
+            // Clears an Invalid state and tests if a Once or With value has been used.
+            FAKE_ARGS.with(|a| {
+                let old_value = a.replace(TlsState::None);
+                match old_value {
+                    TlsState::With(n, args) => {
+                        if n != args.len() {
+                            Self::error("drop with")
+                        }
+                    }
+                    TlsState::Once(_) | TlsState::None => Self::error("drop"),
+                    TlsState::Scope(_) | TlsState::Invalid => {}
+                }
+            });
         }
     }
 
@@ -609,19 +694,35 @@ pub mod tests {
         }
     }
 
+    fn set(arg1: &[&str]) -> HashSet<String> {
+        arg1.iter().map(|&s| s.to_owned()).collect()
+    }
+
     #[test]
     fn test_process_testing() {
         {
-            let _args = cfg::WithArgs::new(&"git blame hello");
+            let _args = FakeParentArgs::once(&"git blame hello");
             assert_eq!(
-                calling_process_cmdline(ProcInfo::new(), blame::guess_git_blame_filename_extension),
+                calling_process_cmdline(ProcInfo::new(), guess_git_blame_filename_extension),
                 Some("hello".into())
             );
         }
         {
-            let _args = cfg::WithArgs::new(&"git blame world.txt");
+            let _args = FakeParentArgs::once(&"git blame world.txt");
             assert_eq!(
-                calling_process_cmdline(ProcInfo::new(), blame::guess_git_blame_filename_extension),
+                calling_process_cmdline(ProcInfo::new(), guess_git_blame_filename_extension),
+                Some("txt".into())
+            );
+        }
+        {
+            let _args = FakeParentArgs::for_scope(&"git blame hello world.txt");
+            assert_eq!(
+                calling_process_cmdline(ProcInfo::new(), guess_git_blame_filename_extension),
+                Some("txt".into())
+            );
+
+            assert_eq!(
+                calling_process_cmdline(ProcInfo::new(), guess_git_blame_filename_extension),
                 Some("txt".into())
             );
         }
@@ -630,22 +731,78 @@ pub mod tests {
     #[test]
     #[should_panic]
     fn test_process_testing_assert() {
-        {
-            let _args = cfg::WithArgs::new(&"git blame do.not.panic");
-            assert_eq!(
-                calling_process_cmdline(ProcInfo::new(), blame::guess_git_blame_filename_extension),
-                Some("panic".into())
-            );
+        let _args = FakeParentArgs::once(&"git blame do.not.panic");
+        assert_eq!(
+            calling_process_cmdline(ProcInfo::new(), guess_git_blame_filename_extension),
+            Some("panic".into())
+        );
 
-            calling_process_cmdline(ProcInfo::new(), blame::guess_git_blame_filename_extension);
-        }
+        calling_process_cmdline(ProcInfo::new(), guess_git_blame_filename_extension);
+    }
+
+    #[test]
+    #[should_panic]
+    fn test_process_testing_assert_never_used() {
+        let _args = FakeParentArgs::once(&"never used");
+
+        // causes a panic while panicing, so can't test:
+        // let _args = FakeParentArgs::for_scope(&"never used");
+        // let _args = FakeParentArgs::once(&"never used");
+    }
+
+    #[test]
+    fn test_process_testing_scope_can_remain_unused() {
+        let _args = FakeParentArgs::for_scope(&"never used");
+    }
+
+    #[test]
+    fn test_process_testing_n_times_panic() {
+        let _args = FakeParentArgs::with(&["git blame once", "git blame twice"]);
+        assert_eq!(
+            calling_process_cmdline(ProcInfo::new(), guess_git_blame_filename_extension),
+            Some("once".into())
+        );
+
+        assert_eq!(
+            calling_process_cmdline(ProcInfo::new(), guess_git_blame_filename_extension),
+            Some("twice".into())
+        );
+    }
+
+    #[test]
+    #[should_panic]
+    fn test_process_testing_n_times_unused() {
+        let _args = FakeParentArgs::with(&["git blame once", "git blame twice"]);
+    }
+
+    #[test]
+    #[should_panic]
+    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_extension),
+            Some("once".into())
+        );
+    }
+
+    #[test]
+    #[should_panic]
+    #[ignore]
+    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_extension),
+            Some("once".into())
+        );
+        // ignored: dropping causes a panic while panicing, so can't test
+        calling_process_cmdline(ProcInfo::new(), guess_git_blame_filename_extension);
     }
 
     #[test]
     fn test_process_blame_info_with_parent() {
         let no_processes = MockProcInfo::with(&[]);
         assert_eq!(
-            calling_process_cmdline(no_processes, blame::guess_git_blame_filename_extension),
+            calling_process_cmdline(no_processes, guess_git_blame_filename_extension),
             None
         );
 
@@ -655,7 +812,7 @@ pub mod tests {
             (4, 100, "delta", Some(3)),
         ]);
         assert_eq!(
-            calling_process_cmdline(parent, blame::guess_git_blame_filename_extension),
+            calling_process_cmdline(parent, guess_git_blame_filename_extension),
             Some("txt".into())
         );
 
@@ -666,11 +823,78 @@ pub mod tests {
             (5, 100, "delta", Some(4)),
         ]);
         assert_eq!(
-            calling_process_cmdline(grandparent, blame::guess_git_blame_filename_extension),
+            calling_process_cmdline(grandparent, guess_git_blame_filename_extension),
             Some("rs".into())
         );
     }
 
+    #[test]
+    fn test_process_blame_info_with_sibling() {
+        let sibling = MockProcInfo::with(&[
+            (2, 100, "-xterm", None),
+            (3, 100, "-shell", Some(2)),
+            (4, 100, "git blame src/main.rs", Some(3)),
+            (5, 100, "delta", Some(3)),
+        ]);
+        assert_eq!(
+            calling_process_cmdline(sibling, guess_git_blame_filename_extension),
+            Some("rs".into())
+        );
+
+        let indirect_sibling = MockProcInfo::with(&[
+            (2, 100, "-xterm", None),
+            (3, 100, "-shell", Some(2)),
+            (4, 100, "Git.exe blame --correct src/main.abc", Some(3)),
+            (
+                10,
+                100,
+                "Git.exe blame --ignored-child src/main.def",
+                Some(4),
+            ),
+            (5, 100, "delta.sh", Some(3)),
+            (20, 100, "delta", Some(5)),
+        ]);
+        assert_eq!(
+            calling_process_cmdline(indirect_sibling, guess_git_blame_filename_extension),
+            Some("abc".into())
+        );
+
+        let indirect_sibling2 = MockProcInfo::with(&[
+            (2, 100, "-xterm", None),
+            (3, 100, "-shell", Some(2)),
+            (4, 100, "git wrap src/main.abc", Some(3)),
+            (10, 100, "git blame src/main.def", Some(4)),
+            (5, 100, "delta.sh", Some(3)),
+            (20, 100, "delta", Some(5)),
+        ]);
+        assert_eq!(
+            calling_process_cmdline(indirect_sibling2, guess_git_blame_filename_extension),
+            Some("def".into())
+        );
+
+        // 3 blame processes, 2 with matching start times, pick the one with lower
+        // distance but larger start time difference.
+        let indirect_sibling_start_times = MockProcInfo::with(&[
+            (2, 100, "-xterm", None),
+            (3, 100, "-shell", Some(2)),
+            (4, 109, "git wrap src/main.abc", Some(3)),
+            (10, 109, "git blame src/main.def", Some(4)),
+            (20, 100, "git wrap1 src/main.abc", Some(3)),
+            (21, 100, "git wrap2 src/main.def", Some(20)),
+            (22, 101, "git blame src/main.not", Some(21)),
+            (23, 102, "git blame src/main.this", Some(20)),
+            (5, 100, "delta.sh", Some(3)),
+            (20, 100, "delta", Some(5)),
+        ]);
+        assert_eq!(
+            calling_process_cmdline(
+                indirect_sibling_start_times,
+                guess_git_blame_filename_extension
+            ),
+            Some("this".into())
+        );
+    }
+
     #[test]
     fn test_describe_calling_process_grep() {
         let no_processes = MockProcInfo::with(&[]);
@@ -679,6 +903,11 @@ pub mod tests {
             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 grep pattern hello.txt", Some(2)),
@@ -686,12 +915,22 @@ pub mod tests {
         ]);
         assert_eq!(
             calling_process_cmdline(parent, describe_calling_process),
-            Some(CallingProcess::GitGrep(([].into(), [].into())))
+            Some(CallingProcess::GitGrep(empty_command_line.clone()))
+        );
+
+        let parent = MockProcInfo::with(&[
+            (2, 100, "-shell", None),
+            (3, 100, "Git.exe grep pattern hello.txt", Some(2)),
+            (4, 100, "delta", Some(3)),
+        ]);
+        assert_eq!(
+            calling_process_cmdline(parent, describe_calling_process),
+            Some(CallingProcess::GitGrep(empty_command_line.clone()))
         );
 
         for grep_command in &[
             "/usr/local/bin/rg pattern hello.txt",
-            "rg pattern hello.txt",
+            "RG.exe pattern hello.txt",
             "/usr/local/bin/ack pattern hello.txt",
             "ack.exe pattern hello.txt",
         ] {
@@ -706,17 +945,14 @@ pub mod tests {
             );
         }
 
-        fn set(arg1: &[&str]) -> HashSet<String> {
-            arg1.iter().map(|&s| s.to_owned()).collect()
-        }
-
         let git_grep_command =
             "git grep -ab --function-context -n --show-function -W --foo=val pattern hello.txt";
 
-        let expected_result = Some(CallingProcess::GitGrep((
-            set(&["--function-context", "--show-function", "--foo"]),
-            set(&["-a", "-b", "-n", "-W"]),
-        )));
+        let expected_result = Some(CallingProcess::GitGrep(CommandLine {
+            long_options: set(&["--function-context", "--show-function", "--foo"]),
+            short_options: set(&["-a", "-b", "-n", "-W"]),
+            last_arg: Some("hello.txt".to_string()),
+        }));
 
         let parent = MockProcInfo::with(&[
             (2, 100, "-shell", None),
@@ -743,10 +979,16 @@ pub mod tests {
     #[test]
     fn test_describe_calling_process_git_show() {
         for (command, expected_extension) in [
-            ("/usr/local/bin/git show 775c3b84:./src/hello.rs", "rs"),
-            ("/usr/local/bin/git show HEAD~1:Makefile", "Makefile"),
             (
-                "git -c x.y=z show --abbrev-commit 775c3b84:./src/hello.bye.R",
+                "/usr/local/bin/git show --abbrev-commit -w 775c3b84:./src/hello.rs",
+                "rs",
+            ),
+            (
+                "/usr/local/bin/git show --abbrev-commit -w HEAD~1:Makefile",
+                "Makefile",
+            ),
+            (
+                "git -c x.y=z show --abbrev-commit -w 775c3b84:./src/hello.bye.R",
                 "R",
             ),
         ] {
@@ -755,10 +997,15 @@ pub mod tests {
                 (3, 100, command, Some(2)),
                 (4, 100, "delta", Some(3)),
             ]);
-            assert_eq!(
-                calling_process_cmdline(parent, describe_calling_process),
-                Some(CallingProcess::GitShow(expected_extension.to_string())),
-            );
+            if let Some(CallingProcess::GitShow(cmd_line, ext)) =
+                calling_process_cmdline(parent, describe_calling_process)
+            {
+                assert_eq!(cmd_line.long_options, set(&["--abbrev-commit"]));
+                assert_eq!(cmd_line.short_options, set(&["-w"]));
+                assert_eq!(ext, Some(expected_extension.to_string()));
+            } else {
+                assert!(false);
+            }
         }
     }
 


Test patch:
diff --git a/src/tests/ansi_test_utils.rs b/src/tests/ansi_test_utils.rs
index c4968ad..04555b5 100644
--- a/src/tests/ansi_test_utils.rs
+++ b/src/tests/ansi_test_utils.rs
@@ -143,9 +143,10 @@ pub mod ansi_test_utils {
             _ => "",
         };
         paint::Painter::paint_lines(
-            syntax_style_sections,
-            diff_style_sections,
-            [state].iter(),
+            &lines,
+            &syntax_style_sections,
+            &diff_style_sections,
+            &[false],
             &mut output_buffer,
             config,
             &mut None,