Skip to content

Commit

Permalink
Handle combined diff format
Browse files Browse the repository at this point in the history
With this commit combined diff
format (https://git-scm.com/docs/git-diff#_combined_diff_format) is
handled appropriately. However, there is no special handling of merge
conflict markers.

Fixes #189, #736
  • Loading branch information
dandavison committed Dec 4, 2021
1 parent cbdfccc commit 4a6c1fe
Show file tree
Hide file tree
Showing 15 changed files with 196 additions and 125 deletions.
8 changes: 4 additions & 4 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,11 +145,11 @@ pub struct Config {
impl Config {
pub fn get_style(&self, state: &State) -> &Style {
match state {
State::HunkMinus(_) => &self.minus_style,
State::HunkPlus(_) => &self.plus_style,
State::HunkMinus(_, _) => &self.minus_style,
State::HunkPlus(_, _) => &self.plus_style,
State::CommitMeta => &self.commit_style,
State::DiffHeader => &self.file_style,
State::HunkHeader(_, _) => &self.hunk_header_style,
State::DiffHeader(_) => &self.file_style,
State::HunkHeader(_, _, _) => &self.hunk_header_style,
State::SubmoduleLog => &self.file_style,
_ => delta_unreachable("Unreachable code reached in get_style."),
}
Expand Down
34 changes: 23 additions & 11 deletions src/delta.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,30 @@ use crate::style::DecorationStyle;

#[derive(Clone, Debug, PartialEq)]
pub enum State {
CommitMeta, // In commit metadata section
DiffHeader, // In diff header section, between (possible) commit metadata and first hunk
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)
SubmoduleLog, // In a submodule section, with gitconfig diff.submodule = log
CommitMeta, // In commit metadata section
DiffHeader(DiffType), // In diff metadata section, between (possible) commit metadata and first hunk
HunkHeader(DiffType, String, String), // In hunk metadata line (line, raw_line)
HunkZero(Option<String>), // In hunk; unchanged line (prefix)
HunkMinus(Option<String>, Option<String>), // In hunk; removed line (prefix, raw_line)
HunkPlus(Option<String>, Option<String>), // In hunk; added line (prefix, raw_line)
SubmoduleLog, // In a submodule section, with gitconfig diff.submodule = log
SubmoduleShort(String), // In a submodule section, with gitconfig diff.submodule = short
Blame(String, Option<String>), // In a line of `git blame` output (commit, repeat_blame_line).
GitShowFile, // In a line of `git show $revision:./path/to/file.ext` output
Grep, // In a line of `git grep` output
GitShowFile, // In a line of `git show $revision:./path/to/file.ext` output
Grep, // In a line of `git grep` output
Unknown,
// The following elements are created when a line is wrapped to display it:
HunkZeroWrapped, // Wrapped unchanged line
HunkMinusWrapped, // Wrapped removed line
HunkPlusWrapped, // Wrapped added line
}

#[derive(Clone, Debug, PartialEq)]
pub enum DiffType {
Unified,
Combined(usize), // number of parent commits: https://git-scm.com/docs/git-diff#_combined_diff_format
}

#[derive(Debug, PartialEq)]
pub enum Source {
GitDiff, // Coming from a `git diff` command
Expand Down Expand Up @@ -171,7 +177,9 @@ impl<'a> StateMachine<'a> {

/// Skip file metadata lines unless a raw diff style has been requested.
pub fn should_skip_line(&self) -> bool {
self.state == State::DiffHeader && self.should_handle() && !self.config.color_only
matches!(self.state, State::DiffHeader(_))
&& self.should_handle()
&& !self.config.color_only
}

/// Emit unchanged any line that delta does not handle.
Expand Down Expand Up @@ -211,7 +219,11 @@ pub fn format_raw_line<'a>(line: &'a str, config: &Config) -> Cow<'a, str> {
/// * git diff
/// * diff -u
fn detect_source(line: &str) -> Source {
if line.starts_with("commit ") || line.starts_with("diff --git ") {
if line.starts_with("commit ")
|| line.starts_with("diff --git ")
|| line.starts_with("diff --cc ")
|| line.starts_with("diff --combined ")
{
Source::GitDiff
} else if line.starts_with("diff -u")
|| line.starts_with("diff -ru")
Expand Down
6 changes: 3 additions & 3 deletions src/features/line_numbers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,18 +76,18 @@ pub fn linenumbers_and_styles<'a>(
config.line_numbers_style_minusplus[Plus],
);
let ((minus_number, plus_number), (minus_style, plus_style)) = match state {
State::HunkMinus(_) => {
State::HunkMinus(_, _) => {
line_numbers_data.line_number[Left] += increment as usize;
((Some(nr_left), None), (minus_style, plus_style))
}
State::HunkMinusWrapped => ((None, None), (minus_style, plus_style)),
State::HunkZero => {
State::HunkZero(_) => {
line_numbers_data.line_number[Left] += increment as usize;
line_numbers_data.line_number[Right] += increment as usize;
((Some(nr_left), Some(nr_right)), (zero_style, zero_style))
}
State::HunkZeroWrapped => ((None, None), (zero_style, zero_style)),
State::HunkPlus(_) => {
State::HunkPlus(_, _) => {
line_numbers_data.line_number[Right] += increment as usize;
((None, Some(nr_right)), (minus_style, plus_style))
}
Expand Down
16 changes: 8 additions & 8 deletions src/features/side_by_side.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ pub fn paint_minus_and_plus_lines_side_by_side(
&lines_have_homolog[Left],
match minus_line_index {
Some(i) => &line_states[Left][i],
None => &State::HunkMinus(None),
None => &State::HunkMinus(None, None),
},
&mut Some(line_numbers_data),
bg_should_fill[Left],
Expand All @@ -201,7 +201,7 @@ pub fn paint_minus_and_plus_lines_side_by_side(
&lines_have_homolog[Right],
match plus_line_index {
Some(i) => &line_states[Right][i],
None => &State::HunkPlus(None),
None => &State::HunkPlus(None, None),
},
&mut Some(line_numbers_data),
bg_should_fill[Right],
Expand All @@ -222,7 +222,7 @@ pub fn paint_zero_lines_side_by_side<'a>(
painted_prefix: Option<ansi_term::ANSIString>,
background_color_extends_to_terminal_width: BgShouldFill,
) {
let states = vec![State::HunkZero];
let states = vec![State::HunkZero(None)];

let (states, syntax_style_sections, diff_style_sections) = wrap_zero_block(
config,
Expand Down Expand Up @@ -418,8 +418,8 @@ fn paint_minus_or_plus_panel_line<'a>(
)
} else {
let opposite_state = match state {
State::HunkMinus(x) => State::HunkPlus(x.clone()),
State::HunkPlus(x) => State::HunkMinus(x.clone()),
State::HunkMinus(_, s) => State::HunkPlus(None, s.clone()),
State::HunkPlus(_, s) => State::HunkMinus(None, s.clone()),
_ => unreachable!(),
};
(
Expand Down Expand Up @@ -470,17 +470,17 @@ fn pad_panel_line_to_width<'a>(
// to form the other half of the line, then don't emit the empty line marker.
if panel_line_is_empty && line_index.is_some() {
match state {
State::HunkMinus(_) => Painter::mark_empty_line(
State::HunkMinus(_, _) => Painter::mark_empty_line(
&config.minus_empty_line_marker_style,
panel_line,
Some(" "),
),
State::HunkPlus(_) => Painter::mark_empty_line(
State::HunkPlus(_, _) => Painter::mark_empty_line(
&config.plus_empty_line_marker_style,
panel_line,
Some(" "),
),
State::HunkZero => {}
State::HunkZero(_) => {}
_ => unreachable!(),
};
};
Expand Down
8 changes: 4 additions & 4 deletions src/handlers/diff_header.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use unicode_segmentation::UnicodeSegmentation;

use super::draw;
use crate::config::Config;
use crate::delta::{Source, State, StateMachine};
use crate::delta::{DiffType, Source, State, StateMachine};
use crate::features;
use crate::paint::Painter;

Expand All @@ -24,7 +24,7 @@ pub enum FileEvent {
impl<'a> StateMachine<'a> {
#[inline]
fn test_diff_header_minus_line(&self) -> bool {
(self.state == State::DiffHeader || self.source == Source::DiffUnified)
(matches!(self.state, State::DiffHeader(_)) || self.source == Source::DiffUnified)
&& (self.line.starts_with("--- ")
|| self.line.starts_with("rename from ")
|| self.line.starts_with("copy from ")
Expand Down Expand Up @@ -57,7 +57,7 @@ impl<'a> StateMachine<'a> {
self.minus_file_event = file_event;

if self.source == Source::DiffUnified {
self.state = State::DiffHeader;
self.state = State::DiffHeader(DiffType::Unified);
self.painter
.set_syntax(get_file_extension_from_marker_line(&self.line));
} else {
Expand Down Expand Up @@ -85,7 +85,7 @@ impl<'a> StateMachine<'a> {

#[inline]
fn test_diff_header_plus_line(&self) -> bool {
(self.state == State::DiffHeader || self.source == Source::DiffUnified)
(matches!(self.state, State::DiffHeader(_)) || self.source == Source::DiffUnified)
&& (self.line.starts_with("+++ ")
|| self.line.starts_with("rename to ")
|| self.line.starts_with("copy to ")
Expand Down
9 changes: 7 additions & 2 deletions src/handlers/diff_header_diff.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::delta::{State, StateMachine};
use crate::delta::{DiffType, State, StateMachine};

impl<'a> StateMachine<'a> {
#[inline]
Expand All @@ -12,7 +12,12 @@ impl<'a> StateMachine<'a> {
return Ok(false);
}
self.painter.paint_buffered_minus_and_plus_lines();
self.state = State::DiffHeader;
self.state =
if self.line.starts_with("diff --cc ") || self.line.starts_with("diff --combined ") {
State::DiffHeader(DiffType::Combined(2)) // We will confirm the number of parents when we see the hunk header
} else {
State::DiffHeader(DiffType::Unified)
};
self.handled_diff_header_header_line_file_pair = None;
self.diff_line = self.line.clone();
if !self.should_skip_line() {
Expand Down
7 changes: 5 additions & 2 deletions src/handlers/diff_header_misc.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::delta::{Source, State, StateMachine};
use crate::delta::{DiffType, Source, State, StateMachine};

impl<'a> StateMachine<'a> {
#[inline]
Expand All @@ -11,6 +11,9 @@ impl<'a> StateMachine<'a> {
if !self.test_diff_header_misc_cases() {
return Ok(false);
}
self.handle_additional_cases(State::DiffHeader)
self.handle_additional_cases(match self.state {
State::DiffHeader(_) => self.state.clone(),
_ => State::DiffHeader(DiffType::Unified),
})
}
}
93 changes: 68 additions & 25 deletions src/handlers/hunk.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
use std::cmp::min;

use lazy_static::lazy_static;

use crate::cli;
use crate::delta::{State, StateMachine};
use crate::config::delta_unreachable;
use crate::delta::{DiffType, State, StateMachine};
use crate::style;
use crate::utils::process::{self, CallingProcess};
use unicode_segmentation::UnicodeSegmentation;
Expand All @@ -26,7 +29,10 @@ impl<'a> StateMachine<'a> {
fn test_hunk_line(&self) -> bool {
matches!(
self.state,
State::HunkHeader(_, _) | State::HunkZero | State::HunkMinus(_) | State::HunkPlus(_)
State::HunkHeader(_, _, _)
| State::HunkZero(_)
| State::HunkMinus(_, _)
| State::HunkPlus(_, _)
) && !&*IS_WORD_DIFF
}

Expand All @@ -37,6 +43,7 @@ impl<'a> StateMachine<'a> {
// highlighting according to inferred edit operations. In the case of
// an unchanged line, we paint it immediately.
pub fn handle_hunk_line(&mut self) -> std::io::Result<bool> {
use State::*;
// 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.
if !self.test_hunk_line() {
Expand All @@ -50,71 +57,107 @@ impl<'a> StateMachine<'a> {
{
self.painter.paint_buffered_minus_and_plus_lines();
}
if let State::HunkHeader(line, raw_line) = &self.state.clone() {
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 {
self.state = match new_line_state(&self.line, &self.state) {
Some(HunkMinus(prefix, _)) => {
if let 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 line = self.painter.prepare(&self.line, prefix.as_deref());
let state = match self.config.inspect_raw_lines {
cli::InspectRawLines::True
if style::line_has_style_other_than(
&self.raw_line,
[*style::GIT_DEFAULT_MINUS_STYLE, self.config.git_minus_style].iter(),
) =>
{
State::HunkMinus(Some(self.painter.prepare_raw_line(&self.raw_line)))
let raw_line = self
.painter
.prepare_raw_line(&self.raw_line, prefix.as_deref());
HunkMinus(prefix, Some(raw_line))
}
_ => State::HunkMinus(None),
_ => HunkMinus(prefix, None),
};
self.painter
.minus_lines
.push((self.painter.prepare(&self.line), state.clone()));
self.painter.minus_lines.push((line, state.clone()));
state
}
Some('+') => {
Some(HunkPlus(prefix, _)) => {
let line = self.painter.prepare(&self.line, prefix.as_deref());
let state = match self.config.inspect_raw_lines {
cli::InspectRawLines::True
if style::line_has_style_other_than(
&self.raw_line,
[*style::GIT_DEFAULT_PLUS_STYLE, self.config.git_plus_style].iter(),
) =>
{
State::HunkPlus(Some(self.painter.prepare_raw_line(&self.raw_line)))
let raw_line = self
.painter
.prepare_raw_line(&self.raw_line, prefix.as_deref());
HunkPlus(prefix, Some(raw_line))
}
_ => State::HunkPlus(None),
_ => HunkPlus(prefix, None),
};
self.painter
.plus_lines
.push((self.painter.prepare(&self.line), state.clone()));
self.painter.plus_lines.push((line, state.clone()));
state
}
Some(' ') => {
Some(HunkZero(prefix)) => {
// 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.paint_zero_line(&self.line);
State::HunkZero
self.painter.paint_zero_line(&self.line, prefix.clone());
HunkZero(prefix)
}
_ => {
// 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
.push_str(&self.painter.expand_tabs(self.raw_line.graphemes(true)));
self.painter.output_buffer.push('\n');
State::HunkZero
State::HunkZero(None)
}
};
self.painter.emit()?;
Ok(true)
}
}

fn new_line_state(new_line: &str, prev_state: &State) -> Option<State> {
use State::*;
let diff_type = match prev_state {
HunkMinus(None, _) | HunkZero(None) | HunkPlus(None, _) => DiffType::Unified,
HunkHeader(diff_type, _, _) => diff_type.clone(),
HunkMinus(Some(prefix), _) | HunkZero(Some(prefix)) | HunkPlus(Some(prefix), _) => {
DiffType::Combined(prefix.len())
}
_ => delta_unreachable(&format!("diff_type: unexpected state: {:?}", prev_state)),
};

let (prefix_char, prefix) = match diff_type {
DiffType::Unified => (new_line.chars().next(), None),
DiffType::Combined(n_parents) => {
let prefix = &new_line[..min(n_parents, new_line.len())];
let prefix_char = match prefix.chars().find(|c| c == &'-' || c == &'+') {
Some(c) => Some(c),
None => match prefix.chars().find(|c| c != &' ') {
None => Some(' '),
Some(_) => None,
},
};
(prefix_char, Some(prefix.to_string()))
}
};
match prefix_char {
Some('-') => Some(HunkMinus(prefix, None)),
Some(' ') => Some(HunkZero(prefix)),
Some('+') => Some(HunkPlus(prefix, None)),
_ => None,
}
}
Loading

0 comments on commit 4a6c1fe

Please sign in to comment.