Repository: dandavison/delta
Pull request #700 (2021-08-23 05:52:51)

More minimalistic display of submodule (short) diffs
More minimalistic display of submodule diffs under `diff.submodule = short`, as suggested by @mrjoel in #662.

<table><tr><td><img width=600px src="https://user-images.githubusercontent.com/52205/130397143-61f311ba-d3bb-45f2-8d9d-8571bc6275aa.png" alt="image" /></td></tr></table>


Patch:
diff --git a/src/config.rs b/src/config.rs
index acf7d0c..9b1c23c 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -99,8 +99,8 @@ impl Config {
             State::HunkPlus(_) => &self.plus_style,
             State::CommitMeta => &self.commit_style,
             State::FileMeta => &self.file_style,
-            State::HunkHeader => &self.hunk_header_style,
-            State::Submodule => &self.file_style,
+            State::HunkHeader(_, _) => &self.hunk_header_style,
+            State::SubmoduleLog => &self.file_style,
             _ => delta_unreachable("Unreachable code reached in get_style."),
         }
     }
diff --git a/src/delta.rs b/src/delta.rs
index c42ba9f..fca76ff 100644
--- a/src/delta.rs
+++ b/src/delta.rs
@@ -18,13 +18,14 @@ use crate::style::{self, DecorationStyle};
 
 #[derive(Clone, Debug, PartialEq)]
 pub enum State {
-    CommitMeta,                // In commit metadata section
+    CommitMeta,                 // In commit metadata section
     FileMeta, // In diff metadata section, between (possible) commit metadata and first hunk
-    HunkHeader, // In hunk metadata line
+    HunkHeader(String, String), // In hunk metadata line (line, raw_line)
     HunkZero, // In hunk; unchanged line
     HunkMinus(Option<String>), // In hunk; removed line (raw_line)
     HunkPlus(Option<String>), // In hunk; added line (raw_line)
-    Submodule,
+    SubmoduleLog, // In a submodule section, with gitconfig diff.submodule = log
+    SubmoduleShort(String), // In a submodule section, with gitconfig diff.submodule = short
     Unknown,
 }
 
@@ -39,7 +40,7 @@ impl State {
     fn is_in_hunk(&self) -> bool {
         matches!(
             *self,
-            State::HunkHeader | State::HunkZero | State::HunkMinus(_) | State::HunkPlus(_)
+            State::HunkHeader(_, _) | State::HunkZero | State::HunkMinus(_) | State::HunkPlus(_)
         )
     }
 }
@@ -145,7 +146,13 @@ impl<'a> StateMachine<'a> {
             {
                 self.handle_additional_cases(State::FileMeta)?
             } else if line.starts_with("Submodule ") {
-                self.handle_additional_cases(State::Submodule)?
+                self.handle_additional_cases(State::SubmoduleLog)?
+            } else if (matches!(self.state, State::HunkHeader(_, _))
+                && line.starts_with("-Subproject commit "))
+                || (matches!(self.state, State::SubmoduleShort(_))
+                    && line.starts_with("+Subproject commit "))
+            {
+                self.handle_submodule_short_line()?
             } else if self.state.is_in_hunk() {
                 // A true hunk line should start with one of: '+', '-', ' '. However, handle_hunk_line
                 // handles all lines until the state transitions away from the hunk states.
@@ -415,14 +422,39 @@ impl<'a> StateMachine<'a> {
         Ok(handled_line)
     }
 
-    /// Emit the hunk header, with any requested decoration.
+    fn handle_submodule_short_line(&mut self) -> std::io::Result<bool> {
+        if let Some(commit) = parse::get_submodule_short_commit(&self.line) {
+            if let State::HunkHeader(_, _) = self.state {
+                self.state = State::SubmoduleShort(commit.to_owned());
+            } else if let State::SubmoduleShort(minus_commit) = &self.state {
+                self.painter.emit()?;
+                writeln!(
+                    self.painter.writer,
+                    "{} ⟶   {}",
+                    self.config
+                        .minus_style
+                        .paint(minus_commit.chars().take(7).collect::<String>()),
+                    self.config
+                        .plus_style
+                        .paint(commit.chars().take(7).collect::<String>()),
+                )?;
+            }
+        }
+        Ok(true)
+    }
+
     fn handle_hunk_header_line(&mut self) -> std::io::Result<bool> {
+        self.state = State::HunkHeader(self.line.clone(), self.raw_line.clone());
+        Ok(true)
+    }
+
+    /// Emit the hunk header, with any requested decoration.
+    fn emit_hunk_header_line(&mut self, line: &str, raw_line: &str) -> std::io::Result<bool> {
         self.painter.paint_buffered_minus_and_plus_lines();
-        self.state = State::HunkHeader;
         self.painter.set_highlighter();
         self.painter.emit()?;
 
-        let (code_fragment, line_numbers) = parse::parse_hunk_header(&self.line);
+        let (code_fragment, line_numbers) = parse::parse_hunk_header(line);
         if self.config.line_numbers {
             self.painter
                 .line_numbers_data
@@ -430,12 +462,7 @@ impl<'a> StateMachine<'a> {
         }
 
         if self.config.hunk_header_style.is_raw {
-            hunk_header::write_hunk_header_raw(
-                &mut self.painter,
-                &self.line,
-                &self.raw_line,
-                self.config,
-            )?;
+            hunk_header::write_hunk_header_raw(&mut self.painter, line, raw_line, self.config)?;
         } else if self.config.hunk_header_style.is_omitted {
             writeln!(self.painter.writer)?;
         } else {
@@ -449,7 +476,7 @@ impl<'a> StateMachine<'a> {
                 &code_fragment,
                 &line_numbers,
                 &mut self.painter,
-                &self.line,
+                line,
                 &self.plus_file,
                 self.config,
             )?;
@@ -473,6 +500,9 @@ impl<'a> StateMachine<'a> {
         {
             self.painter.paint_buffered_minus_and_plus_lines();
         }
+        if let State::HunkHeader(line, raw_line) = &self.state.clone() {
+            self.emit_hunk_header_line(line, raw_line)?;
+        }
         self.state = match self.line.chars().next() {
             Some('-') => {
                 if let State::HunkPlus(_) = self.state {
diff --git a/src/hunk_header.rs b/src/hunk_header.rs
index c076041..b3c596c 100644
--- a/src/hunk_header.rs
+++ b/src/hunk_header.rs
@@ -142,18 +142,18 @@ fn write_to_output_buffer(
     if !line.is_empty() {
         let lines = vec![(
             painter.expand_tabs(line.graphemes(true)),
-            delta::State::HunkHeader,
+            delta::State::HunkHeader("".to_owned(), "".to_owned()),
         )];
         let syntax_style_sections = Painter::get_syntax_style_sections_for_lines(
             &lines,
-            &delta::State::HunkHeader,
+            &delta::State::HunkHeader("".to_owned(), "".to_owned()),
             painter.highlighter.as_mut(),
             painter.config,
         );
         Painter::paint_lines(
             syntax_style_sections,
             vec![vec![(config.hunk_header_style, &lines[0].0)]], // TODO: compute style from state
-            [delta::State::HunkHeader].iter(),
+            [delta::State::HunkHeader("".to_owned(), "".to_owned())].iter(),
             &mut painter.output_buffer,
             config,
             &mut None,
diff --git a/src/paint.rs b/src/paint.rs
index 88f26bf..2236ec2 100644
--- a/src/paint.rs
+++ b/src/paint.rs
@@ -463,7 +463,7 @@ impl<'a> Painter<'a> {
                 config.plus_style.is_syntax_highlighted
                     || config.plus_emph_style.is_syntax_highlighted
             }
-            State::HunkHeader => true,
+            State::HunkHeader(_, _) => true,
             State::HunkMinus(Some(_)) | State::HunkPlus(Some(_)) => false,
             _ => panic!(
                 "should_compute_syntax_highlighting is undefined for state {:?}",
diff --git a/src/parse.rs b/src/parse.rs
index a62d486..c6a0860 100644
--- a/src/parse.rs
+++ b/src/parse.rs
@@ -268,6 +268,18 @@ fn get_extension(s: &str) -> Option<&str> {
         .or_else(|| path.file_name().and_then(|s| s.to_str()))
 }
 
+lazy_static! {
+    static ref SUBMODULE_SHORT_LINE_REGEX: Regex =
+        Regex::new("^[-+]Subproject commit ([0-9a-f]{40})$").unwrap();
+}
+
+pub fn get_submodule_short_commit(line: &str) -> Option<&str> {
+    match SUBMODULE_SHORT_LINE_REGEX.captures(line) {
+        Some(caps) => Some(caps.get(1).unwrap().as_str()),
+        None => None,
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;


Test patch:
diff --git a/src/tests/test_example_diffs.rs b/src/tests/test_example_diffs.rs
index 3b249ef..d60067b 100644
--- a/src/tests/test_example_diffs.rs
+++ b/src/tests/test_example_diffs.rs
@@ -1386,7 +1386,7 @@ src/align.rs:71: impl<'a> Alignment<'a> { │
             4,
             "impl<'a> Alignment<'a> { ",
             "rs",
-            State::HunkHeader,
+            State::HunkHeader("".to_owned(), "".to_owned()),
             &config,
         );
         ansi_test_utils::assert_line_has_no_color(&output, 12, "─────────────────────────────┘");