diff options
| author | Dan Davison <dandavison7@gmail.com> | 2021-11-29 20:54:48 -0500 |
|---|---|---|
| committer | Dan Davison <dandavison7@gmail.com> | 2021-12-05 11:25:05 -0500 |
| commit | 6745f42ddadeccfa30628c70d39b8f9abbff35f0 (patch) | |
| tree | 19cdd504eba5632d6bb0d6a72559b1ee1774c057 | |
| parent | e7294060ef3b8af0d2307eea4359123232f85646 (diff) | |
Display merge conflicts
| -rw-r--r-- | README.md | 3 | ||||
| -rw-r--r-- | etc/examples/822-hunk-header-within-merge-conflict.diff | 545 | ||||
| -rw-r--r-- | src/cli.rs | 44 | ||||
| -rw-r--r-- | src/config.rs | 11 | ||||
| -rw-r--r-- | src/delta.rs | 43 | ||||
| -rw-r--r-- | src/features/side_by_side.rs | 15 | ||||
| -rw-r--r-- | src/handlers/diff_header_diff.rs | 5 | ||||
| -rw-r--r-- | src/handlers/draw.rs | 2 | ||||
| -rw-r--r-- | src/handlers/hunk.rs | 81 | ||||
| -rw-r--r-- | src/handlers/hunk_header.rs | 27 | ||||
| -rw-r--r-- | src/handlers/merge_conflict.rs | 1340 | ||||
| -rw-r--r-- | src/handlers/mod.rs | 1 | ||||
| -rw-r--r-- | src/options/set.rs | 6 | ||||
| -rw-r--r-- | src/paint.rs | 63 | ||||
| -rw-r--r-- | src/parse_styles.rs | 24 | ||||
| -rw-r--r-- | src/subcommands/show_colors.rs | 6 | ||||
| -rw-r--r-- | src/tests/test_example_diffs.rs | 6 | ||||
| -rw-r--r-- | src/wrapping.rs | 5 |
18 files changed, 2134 insertions, 93 deletions
@@ -27,6 +27,9 @@ [delta] navigate = true +[merge] + conflictstyle = diff3 + [diff] colorMoved = default diff --git a/etc/examples/822-hunk-header-within-merge-conflict.diff b/etc/examples/822-hunk-header-within-merge-conflict.diff new file mode 100644 index 0000000..4709bc5 --- /dev/null +++ b/etc/examples/822-hunk-header-within-merge-conflict.diff @@ -0,0 +1,545 @@ +diff --cc src/delta.rs +index 9a65aaa,ab08d84..0000000 +--- a/src/delta.rs ++++ b/src/delta.rs +@@@ -21,7 -21,7 +21,13 @@@ pub enum State + HunkZero(DiffType), // In hunk; unchanged line (prefix) + HunkMinus(DiffType, Option<String>), // In hunk; removed line (diff_type, raw_line) + HunkPlus(DiffType, Option<String>), // In hunk; added line (diff_type, raw_line) +++<<<<<<< HEAD + + MergeConflict(MergeParents, merge_conflict::MergeConflictCommit), +++||||||| parent of b2b28c8... Display merge conflict branches +++ MergeConflict(merge_conflict::Source), +++======= ++ MergeConflict(merge_conflict::MergeConflictCommit), +++>>>>>>> b2b28c8... Display merge conflict branches + 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). +diff --cc src/handlers/hunk.rs +index 26cb288,7df74ae..0000000 +--- a/src/handlers/hunk.rs ++++ b/src/handlers/hunk.rs +@@@ -141,28 -141,19 +141,45 @@@ fn new_line_state(new_line: &str, prev_ + | HunkZero(Unified) + | HunkPlus(Unified, _) + | HunkHeader(Unified, _, _) => Unified, +++<<<<<<< HEAD + + HunkHeader(Combined(Number(n), InMergeConflict::No), _, _) => { + + Combined(Number(*n), InMergeConflict::No) + + } + + // The prefixes are specific to the previous line, but the number of merge parents remains + + // equal to the prefix length. + + HunkHeader(Combined(Prefix(prefix), InMergeConflict::No), _, _) => { + + Combined(Number(prefix.len()), InMergeConflict::No) + + } + + HunkMinus(Combined(Prefix(prefix), in_merge_conflict), _) + + | HunkZero(Combined(Prefix(prefix), in_merge_conflict)) + + | HunkPlus(Combined(Prefix(prefix), in_merge_conflict), _) => { + + Combined(Number(prefix.len()), in_merge_conflict.clone()) + + } + + _ => delta_unreachable(&format!( + + "Unexpected state in new_line_state: {:?}", + + prev_state + + )), +++||||||| parent of b2b28c8... Display merge conflict branches +++ HunkHeader(Combined(Number(n)), _, _) => Combined(Number(*n)), +++ HunkMinus(Combined(Prefix(prefix)), _) +++ | HunkZero(Combined(Prefix(prefix))) +++ | HunkPlus(Combined(Prefix(prefix)), _) => Combined(Number(prefix.len())), +++ _ => delta_unreachable(&format!("diff_type: unexpected state: {:?}", prev_state)), +++======= ++ HunkHeader(Combined(Number(n)), _, _) => Combined(Number(*n)), ++ // The prefixes are specific to the previous line, but the number of merge parents remains ++ // equal to the prefix length. ++ HunkHeader(Combined(Prefix(prefix)), _, _) ++ | HunkMinus(Combined(Prefix(prefix)), _) ++ | HunkZero(Combined(Prefix(prefix))) ++ | HunkPlus(Combined(Prefix(prefix)), _) => Combined(Number(prefix.len())), ++ _ => delta_unreachable(&format!("diff_type: unexpected state: {:?}", prev_state)), +++>>>>>>> b2b28c8... Display merge conflict branches + }; + + - let (prefix_char, prefix) = match diff_type { + - Unified => (new_line.chars().next(), None), + - Combined(Number(n_parents)) => { + + let (prefix_char, prefix, in_merge_conflict) = match diff_type { + + Unified => (new_line.chars().next(), None, None), + + Combined(Number(n_parents), in_merge_conflict) => { + let prefix = &new_line[..min(n_parents, new_line.len())]; + let prefix_char = match prefix.chars().find(|c| c == &'-' || c == &'+') { + Some(c) => Some(c), +diff --cc src/handlers/merge_conflict.rs +index a956f2e,3a7e7b9..0000000 +--- a/src/handlers/merge_conflict.rs ++++ b/src/handlers/merge_conflict.rs +@@@ -1,9 -1,10 +1,18 @@@ + -use std::cmp::min; + use std::ops::{Index, IndexMut}; + +++<<<<<<< HEAD + use super::draw; + use crate::cli; + use crate::config::{self, delta_unreachable}; + +use crate::delta::{DiffType, InMergeConflict, MergeParents, State, StateMachine}; +++||||||| parent of b2b28c8... Display merge conflict branches ++ use crate::delta::{DiffType, MergeParents, State, StateMachine}; +++======= +++use super::draw; +++use crate::cli; +++use crate::config::{self, delta_unreachable}; +++use crate::delta::{DiffType, MergeParents, State, StateMachine}; +++>>>>>>> b2b28c8... Display merge conflict branches + use crate::minusplus::MinusPlus; + use crate::paint; + use crate::style::DecorationStyle; +@@@ -28,7 -29,8 +37,15 @@@ pub type MergeConflictCommitNames = Mer + impl<'a> StateMachine<'a> { + pub fn handle_merge_conflict_line(&mut self) -> std::io::Result<bool> { + use DiffType::*; +++<<<<<<< HEAD + + use MergeConflictCommit::*; +++||||||| parent of b2b28c8... Display merge conflict branches +++ use MergeParents::*; +++ use Source::*; +++======= ++ use MergeConflictCommit::*; ++ use MergeParents::*; +++>>>>>>> b2b28c8... Display merge conflict branches + use State::*; + + let mut handled_line = false; +@@@ -36,36 -38,28 +53,113 @@@ + return Ok(handled_line); + } + +++<<<<<<< HEAD + + match self.state.clone() { + + HunkHeader(Combined(merge_parents, InMergeConflict::No), _, _) + + | HunkMinus(Combined(merge_parents, InMergeConflict::No), _) + + | HunkZero(Combined(merge_parents, InMergeConflict::No)) + + | HunkPlus(Combined(merge_parents, InMergeConflict::No), _) => { + + handled_line = self.enter_merge_conflict(&merge_parents) + + } + + MergeConflict(merge_parents, Ours) => { + + handled_line = self.enter_ancestral(&merge_parents) + + || self.enter_theirs(&merge_parents) + + || self.exit_merge_conflict(&merge_parents)? + + || self.store_line( + + Ours, + + HunkPlus(Combined(merge_parents, InMergeConflict::Yes), None), + + ); +++||||||| parent of b2b28c8... Display merge conflict branches +++ // TODO: don't allocate on heap at this point +++ let prefix = self.line[..min(self.line.len(), 2)].to_string(); +++ let diff_type = Combined(Prefix(prefix)); +++ +++ match self.state { +++ // The only transition into a merge conflict is HunkZero => MergeConflict(Ours) +++ // TODO: shouldn't this be HunkZero(Some(_))? +++ HunkZero(_) => { +++ if self.line.starts_with("++<<<<<<<") { +++ self.state = MergeConflict(Ours); +++ handled_line = true +++ } + + } +++ MergeConflict(Ours) => { +++ if self.line.starts_with("++|||||||") { +++ self.state = MergeConflict(Ancestral); +++ } else if self.line.starts_with("++=======") { +++ self.state = MergeConflict(Theirs); +++ } else if self.line.starts_with("++>>>>>>>") { +++ self.paint_buffered_merge_conflict_lines(diff_type)?; +++ } else { +++ let line = self.painter.prepare(&self.line, diff_type.n_parents()); +++ self.painter.merge_conflict_lines[Ours].push((line, HunkPlus(diff_type, None))); +++ } +++ handled_line = true +++======= ++ // TODO: don't allocate on heap at this point ++ let prefix = self.line[..min(self.line.len(), 2)].to_string(); ++ let diff_type = Combined(Prefix(prefix)); ++ ++ match self.state { ++ // The only transition into a merge conflict is HunkZero => MergeConflict(Ours) ++ // TODO: shouldn't this be HunkZero(Some(_))? ++ HunkZero(_) => handled_line = self.enter_merge_conflict(), ++ MergeConflict(Ours) => { ++ handled_line = self.enter_ancestral() ++ || self.enter_theirs() ++ || self.exit_merge_conflict(diff_type.clone())? ++ || self.store_line(Ours, HunkPlus(diff_type, None)); +++>>>>>>> b2b28c8... Display merge conflict branches ++ } +++<<<<<<< HEAD + + MergeConflict(merge_parents, Ancestral) => { + + handled_line = self.enter_theirs(&merge_parents) + + || self.exit_merge_conflict(&merge_parents)? + + || self.store_line( + + Ancestral, + + HunkMinus(Combined(merge_parents, InMergeConflict::Yes), None), + + ); +++||||||| parent of b2b28c8... Display merge conflict branches +++ MergeConflict(Ancestral) => { +++ if self.line.starts_with("++=======") { +++ self.state = MergeConflict(Theirs); +++ } else if self.line.starts_with("++>>>>>>>") { +++ self.paint_buffered_merge_conflict_lines(diff_type)?; +++ } else { +++ let line = self.painter.prepare(&self.line, diff_type.n_parents()); +++ self.painter.merge_conflict_lines[Ancestral] +++ .push((line, HunkMinus(diff_type, None))); +++ } +++ handled_line = true +++======= ++ MergeConflict(Ancestral) => { ++ handled_line = self.enter_theirs() ++ || self.exit_merge_conflict(diff_type.clone())? ++ || self.store_line(Ancestral, HunkMinus(diff_type, None)); +++>>>>>>> b2b28c8... Display merge conflict branches + } +++<<<<<<< HEAD + + MergeConflict(merge_parents, Theirs) => { + + handled_line = self.exit_merge_conflict(&merge_parents)? + + || self.store_line( + + Theirs, + + HunkPlus(Combined(merge_parents, InMergeConflict::Yes), None), + + ); +++||||||| parent of b2b28c8... Display merge conflict branches +++ MergeConflict(Theirs) => { +++ if self.line.starts_with("++>>>>>>>") { +++ self.paint_buffered_merge_conflict_lines(diff_type)?; +++ } else { +++ let line = self.painter.prepare(&self.line, diff_type.n_parents()); +++ self.painter.merge_conflict_lines[Theirs] +++ .push((line, HunkPlus(diff_type, None))); +++ } +++ handled_line = true +++======= ++ MergeConflict(Theirs) => { ++ handled_line = self.exit_merge_conflict(diff_type.clone())? ++ || self.store_line(Theirs, HunkPlus(diff_type, None)); +++>>>>>>> b2b28c8... Display merge conflict branches + } + _ => {} + } +@@@ -73,65 -67,60 +167,124 @@@ + Ok(handled_line) + } + +++<<<<<<< HEAD + + fn enter_merge_conflict(&mut self, merge_parents: &MergeParents) -> bool { + + use State::*; + + if let Some(commit) = parse_merge_marker(&self.line, "++<<<<<<<") { + + self.state = MergeConflict(merge_parents.clone(), Ours); + + self.painter.merge_conflict_commit_names[Ours] = Some(commit.to_string()); + + true + + } else { + + false + + } + + } + + + + fn enter_ancestral(&mut self, merge_parents: &MergeParents) -> bool { + + use State::*; + + if let Some(commit) = parse_merge_marker(&self.line, "++|||||||") { + + self.state = MergeConflict(merge_parents.clone(), Ancestral); + + self.painter.merge_conflict_commit_names[Ancestral] = Some(commit.to_string()); + + true + + } else { + + false + + } + + } + + + + fn enter_theirs(&mut self, merge_parents: &MergeParents) -> bool { + + use State::*; + + if self.line.starts_with("++=======") { + + self.state = MergeConflict(merge_parents.clone(), Theirs); + + true + + } else { + + false + + } + + } + + + + fn exit_merge_conflict(&mut self, merge_parents: &MergeParents) -> std::io::Result<bool> { + + if let Some(commit) = parse_merge_marker(&self.line, "++>>>>>>>") { + + self.painter.merge_conflict_commit_names[Theirs] = Some(commit.to_string()); + + self.paint_buffered_merge_conflict_lines(merge_parents)?; + + Ok(true) + + } else { + + Ok(false) + + } + + } + + + + fn store_line(&mut self, commit: MergeConflictCommit, state: State) -> bool { + + use State::*; + + if let HunkMinus(diff_type, _) | HunkZero(diff_type) | HunkPlus(diff_type, _) = &state { + + let line = self.painter.prepare(&self.line, diff_type.n_parents()); + + self.painter.merge_conflict_lines[commit].push((line, state)); + + true + + } else { + + delta_unreachable(&format!("Invalid state: {:?}", state)) + + } + + } + + + + fn paint_buffered_merge_conflict_lines( + + &mut self, + + merge_parents: &MergeParents, + + ) -> std::io::Result<()> { + + use DiffType::*; + + use State::*; +++||||||| parent of b2b28c8... Display merge conflict branches +++ fn paint_buffered_merge_conflict_lines(&mut self, diff_type: DiffType) -> std::io::Result<()> { +++======= ++ fn enter_merge_conflict(&mut self) -> bool { ++ use State::*; ++ if let Some(commit) = parse_merge_marker(&self.line, "++<<<<<<<") { ++ self.state = MergeConflict(Ours); ++ self.painter.merge_conflict_commit_names[Ours] = Some(commit.to_string()); ++ true ++ } else { ++ false ++ } ++ } ++ ++ fn enter_ancestral(&mut self) -> bool { ++ use State::*; ++ if let Some(commit) = parse_merge_marker(&self.line, "++|||||||") { ++ self.state = MergeConflict(Ancestral); ++ self.painter.merge_conflict_commit_names[Ancestral] = Some(commit.to_string()); ++ true ++ } else { ++ false ++ } ++ } ++ ++ fn enter_theirs(&mut self) -> bool { ++ use State::*; ++ if self.line.starts_with("++=======") { ++ self.state = MergeConflict(Theirs); ++ true ++ } else { ++ false ++ } ++ } ++ ++ fn exit_merge_conflict(&mut self, diff_type: DiffType) -> std::io::Result<bool> { ++ if let Some(commit) = parse_merge_marker(&self.line, "++>>>>>>>") { ++ self.painter.merge_conflict_commit_names[Theirs] = Some(commit.to_string()); ++ self.paint_buffered_merge_conflict_lines(diff_type)?; ++ Ok(true) ++ } else { ++ Ok(false) ++ } ++ } ++ ++ fn store_line(&mut self, commit: MergeConflictCommit, state: State) -> bool { ++ use State::*; ++ if let HunkMinus(diff_type, _) | HunkZero(diff_type) | HunkPlus(diff_type, _) = &state { ++ let line = self.painter.prepare(&self.line, diff_type.n_parents()); ++ self.painter.merge_conflict_lines[commit].push((line, state)); ++ true ++ } else { ++ delta_unreachable(&format!("Invalid state: {:?}", state)) ++ } ++ } ++ ++ fn paint_buffered_merge_conflict_lines(&mut self, diff_type: DiffType) -> std::io::Result<()> { +++>>>>>>> b2b28c8... Display merge conflict branches + self.painter.emit()?; + + write_merge_conflict_bar("▼", &mut self.painter, self.config)?; +@@@ -163,6 -152,6 +316,7 @@@ + } + } + +++<<<<<<< HEAD + fn write_subhunk_header( + derived_commit_type: &MergeConflictCommit, + decoration_style: &str, +@@@ -197,63 -186,63 +351,170 @@@ + Ok(()) + } + ++ #[allow(unused)] ++ fn write_merge_conflict_line( ++ painter: &mut paint::Painter, ++ config: &config::Config, ++ ) -> std::io::Result<()> { ++ let (mut draw_fn, _pad, decoration_ansi_term_style) = draw::get_draw_function( ++ DecorationStyle::from_str("bold ol", config.true_color, config.git_config.as_ref()), ++ ); ++ draw_fn( ++ painter.writer, ++ "", ++ "", ++ &config.decorations_width, ++ config.hunk_header_style, ++ decoration_ansi_term_style, ++ )?; ++ Ok(()) ++ } +++||||||| parent of b2b28c8... Display merge conflict branches +++pub use Source::*; +++======= +++fn write_subhunk_header( +++ derived_commit_type: &MergeConflictCommit, +++ decoration_style: &str, +++ painter: &mut paint::Painter, +++ config: &config::Config, +++) -> std::io::Result<()> { +++ let (mut draw_fn, pad, decoration_ansi_term_style) = +++ draw::get_draw_function(DecorationStyle::from_str( +++ decoration_style, +++ config.true_color, +++ config.git_config.as_ref(), +++ )); +++ let derived_commit_name = &painter.merge_conflict_commit_names[derived_commit_type]; +++ let text = if let Some(_ancestral_commit) = &painter.merge_conflict_commit_names[Ancestral] { +++ format!( +++ "ancestor {} {}{}", +++ config.right_arrow, +++ derived_commit_name.as_deref().unwrap_or("?"), +++ if pad { " " } else { "" } +++ ) +++ } else { +++ derived_commit_name.as_deref().unwrap_or("?").to_string() +++ }; +++ draw_fn( +++ painter.writer, +++ &text, +++ &text, +++ &config.decorations_width, +++ config.hunk_header_style, +++ decoration_ansi_term_style, +++ )?; +++ Ok(()) +++} +++>>>>>>> b2b28c8... Display merge conflict branches ++ +++<<<<<<< HEAD ++ fn write_merge_conflict_bar( ++ s: &str, ++ painter: &mut paint::Painter, ++ config: &config::Config, ++ ) -> std::io::Result<()> { ++ if let cli::Width::Fixed(width) = config.decorations_width { ++ writeln!(painter.writer, "{}", s.repeat(width))?; ++ } ++ Ok(()) ++ } ++ ++ fn parse_merge_marker<'a>(line: &'a str, marker: &str) -> Option<&'a str> { ++ match line.strip_prefix(marker) { ++ Some(suffix) => { ++ let suffix = suffix.trim(); ++ if !suffix.is_empty() { ++ Some(suffix) ++ } else { ++ None ++ } ++ } ++ None => None, ++ } ++ } ++ ++ pub use MergeConflictCommit::*; ++ ++ impl<T> Index<MergeConflictCommit> for MergeConflictCommits<T> { ++ type Output = T; ++ fn index(&self, commit: MergeConflictCommit) -> &Self::Output { ++ match commit { +++||||||| parent of b2b28c8... Display merge conflict branches +++impl Index<Source> for MergeConflictLines { +++ type Output = Vec<(String, State)>; +++ fn index(&self, source: Source) -> &Self::Output { +++ match source { +++======= + +#[allow(unused)] + +fn write_merge_conflict_line( + + painter: &mut paint::Painter, + + config: &config::Config, + +) -> std::io::Result<()> { + + let (mut draw_fn, _pad, decoration_ansi_term_style) = draw::get_draw_function( + + DecorationStyle::from_str("bold ol", config.true_color, config.git_config.as_ref()), + + ); + + draw_fn( + + painter.writer, + + "", + + "", + + &config.decorations_width, + + config.hunk_header_style, + + decoration_ansi_term_style, + + )?; + + Ok(()) + +} + + + +fn write_merge_conflict_bar( + + s: &str, + + painter: &mut paint::Painter, + + config: &config::Config, + +) -> std::io::Result<()> { + + if let cli::Width::Fixed(width) = config.decorations_width { + + writeln!(painter.writer, "{}", s.repeat(width))?; + + } + + Ok(()) + +} + + + +fn parse_merge_marker<'a>(line: &'a str, marker: &str) -> Option<&'a str> { + + match line.strip_prefix(marker) { + + Some(suffix) => { + + let suffix = suffix.trim(); + + if !suffix.is_empty() { + + Some(suffix) + + } else { + + None + + } + + } + + None => None, + + } + +} + + + +pub use MergeConflictCommit::*; + + + +impl<T> Index<MergeConflictCommit> for MergeConflictCommits<T> { + + type Output = T; + + fn index(&self, commit: MergeConflictCommit) -> &Self::Output { + + match commit { + + Ours => &self.ours, + + Ancestral => &self.ancestral, + + Theirs => &self.theirs, + + } + + } + +} + + +++impl<T> Index<&MergeConflictCommit> for MergeConflictCommits<T> { +++ type Output = T; +++ fn index(&self, commit: &MergeConflictCommit) -> &Self::Output { +++ match commit { +++>>>>>>> b2b28c8... Display merge conflict branches ++ Ours => &self.ours, ++ Ancestral => &self.ancestral, ++ Theirs => &self.theirs, ++ } ++ } ++ } ++ +++<<<<<<< HEAD + impl<T> Index<&MergeConflictCommit> for MergeConflictCommits<T> { + type Output = T; + fn index(&self, commit: &MergeConflictCommit) -> &Self::Output { +@@@ -268,6 -257,6 +529,15 @@@ + impl<T> IndexMut<MergeConflictCommit> for MergeConflictCommits<T> { + fn index_mut(&mut self, commit: MergeConflictCommit) -> &mut Self::Output { + match commit { +++||||||| parent of b2b28c8... Display merge conflict branches +++impl IndexMut<Source> for MergeConflictLines { +++ fn index_mut(&mut self, source: Source) -> &mut Self::Output { +++ match source { +++======= +++impl<T> IndexMut<MergeConflictCommit> for MergeConflictCommits<T> { +++ fn index_mut(&mut self, commit: MergeConflictCommit) -> &mut Self::Output { +++ match commit { +++>>>>>>> b2b28c8... Display merge conflict branches + Ours => &mut self.ours, + Ancestral => &mut self.ancestral, + Theirs => &mut self.theirs, @@ -435,6 +435,50 @@ pub struct Opt { /// (underline), 'ol' (overline), or the combination 'ul ol'. pub hunk_header_decoration_style: String, + #[structopt(long = "merge-conflict-begin-symbol", default_value = "▼")] + /// A string that is repeated to form the line marking the beginning of a merge conflict region. + pub merge_conflict_begin_symbol: String, + + #[structopt(long = "merge-conflict-end-symbol", default_value = "▲")] + /// A string that is repeated to form the line marking the end of a merge conflict region. + pub merge_conflict_end_symbol: String, + + #[structopt( + long = "merge-conflict-ours-diff-header-style", + default_value = "normal" + )] + /// Style (foreground, background, attributes) for the header above the diff between the + /// ancestral commit and 'our' branch. See STYLES section. + pub merge_conflict_ours_diff_header_style: String, + + #[structopt( + long = "merge-conflict-ours-diff-header-decoration-style", + default_value = "box" + )] + /// Style (foreground, background, attributes) for the decoration of the header above the diff + /// between the ancestral commit and 'our' branch. See STYLES section. The style string should + /// contain one of the special attributes 'box', 'ul' (underline), 'ol' (overline), or the + /// combination 'ul ol'. + pub merge_conflict_ours_diff_header_decoration_style: String, + + #[structopt( + long = "merge-conflict-theirs-diff-header-style", + default_value = "normal" + )] + /// Style (foreground, background, attributes) for the header above the diff between the + /// ancestral commit and 'their' branch. See STYLES section. + pub merge_conflict_theirs_diff_header_style: String, + + #[structopt( + long = "merge-conflict-theirs-diff-header-decoration-style", + default_value = "box" + )] + /// Style (foreground, background, attributes) for the decoration of the header above the diff + /// between the ancestral commit and 'their' branch. See STYLES section. The style string should + /// contain one of the special attributes 'box', 'ul' (underline), 'ol' (overline), or the + /// combination 'ul ol'. + pub merge_conflict_theirs_diff_header_decoration_style: String, + #[structopt(long = "map-styles")] /// A string specifying a mapping styles encountered in raw input to desired /// output styles. An example is diff --git a/src/config.rs b/src/config.rs index 5858afe..b9f1464 100644 --- a/src/config.rs +++ b/src/config.rs @@ -87,6 +87,7 @@ pub struct Config { pub grep_match_line_style: Style, pub grep_match_word_style: Style, pub grep_separator_symbol: String, + pub handle_merge_conflicts: bool, pub hunk_header_file_style: Style, pub hunk_header_line_number_style: Style, pub hunk_header_style_include_file_path: bool, @@ -110,6 +111,10 @@ pub struct Config { pub max_line_distance_for_naively_paired_lines: f64, pub max_line_distance: f64, pub max_line_length: usize, + pub merge_conflict_begin_symbol: String, + pub merge_conflict_ours_diff_header_style: Style, + pub merge_conflict_theirs_diff_header_style: Style, + pub merge_conflict_end_symbol: String, pub minus_emph_style: Style, pub minus_empty_line_marker_style: Style, pub minus_file: Option<PathBuf>, @@ -260,6 +265,7 @@ impl From<cli::Opt> for Config { grep_match_line_style: styles["grep-match-line-style"], grep_match_word_style: styles["grep-match-word-style"], grep_separator_symbol: opt.grep_separator_symbol, + handle_merge_conflicts: !opt.raw, hunk_header_file_style: styles["hunk-header-file-style"], hunk_header_line_number_style: styles["hunk-header-line-number-style"], hunk_header_style: styles["hunk-header-style"], @@ -320,6 +326,11 @@ impl From<cli::Opt> for Config { ) } }, + merge_conflict_begin_symbol: opt.merge_conflict_begin_symbol, + merge_conflict_ours_diff_header_style: styles["merge-conflict-ours-diff-header-style"], + merge_conflict_theirs_diff_header_style: styles + ["merge-conflict-theirs-diff-header-style"], + merge_conflict_end_symbol: opt.merge_conflict_end_symbol, minus_emph_style: styles["minus-emph-style"], minus_empty_line_marker_style: styles["minus-empty-line-marker-style"], minus_file: opt.minus_file, diff --git a/src/delta.rs b/src/delta.rs index 3f1a79d..24877df 100644 --- a/src/delta.rs +++ b/src/delta.rs @@ -6,25 +6,27 @@ use std::io::Write; use bytelines::ByteLines; use crate::ansi; +use crate::config::delta_unreachable; use crate::config::Config; use crate::features; -use crate::handlers; +use crate::handlers::{self, merge_conflict}; use crate::paint::Painter; use crate::style::DecorationStyle; #[derive(Clone, Debug, PartialEq)] pub enum State { - CommitMeta, // In commit metadata section + 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 + HunkHeader(DiffType, String, String), // In hunk metadata line (diff_type, line, raw_line) + HunkZero(DiffType), // In hunk; unchanged line (prefix) + HunkMinus(DiffType, Option<String>), // In hunk; removed line (diff_type, raw_line) + HunkPlus(DiffType, Option<String>), // In hunk; added line (diff_type, raw_line) + MergeConflict(MergeParents, merge_conflict::MergeConflictCommit), + 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 @@ -35,7 +37,27 @@ pub enum State { #[derive(Clone, Debug, PartialEq)] pub enum DiffType { Unified, - Combined(usize), // number of parent commits: https://git-scm.com/docs/git-diff#_combined_diff_format + Combined(MergeParents), // https://git-scm.com/docs/git-diff#_combined_diff_format +} + +#[derive(Clone, Debug, PartialEq)] +pub enum MergeParents { + Number(usize), // Number of parent commits == (number of @s in hunk header) - 1 + Prefix(String), // Hunk line prefix, length == number of parent commits + Unknown, +} + +impl DiffType { + pub fn n_parents(&self) -> usize { + use DiffType::*; + use MergeParents::*; + match self { + Combined(Prefix(prefix)) => prefix.len(), + Combined(Number(n_parents)) => *n_parents, + Unified => 1, + Combined(Unknown) => delta_unreachable("Number of merge parents must be known."), + } + } } #[derive(Debug, PartialEq)] @@ -131,6 +153,7 @@ impl<'a> StateMachine<'a> { || self.handle_diff_header_misc_line()? || self.handle_submodule_log_line()? || self.handle_submodule_short_line()? + || self.handle_merge_conflict_line()? || self.handle_hunk_line()? || self.handle_git_show_file_line()? || self.handle_blame_line()? diff --git a/src/features/side_by_side.rs b/src/features/side_by_side.rs index 85b066c..568142d 100644 --- a/src/features/side_by_side.rs +++ b/src/features/side_by_side.rs @@ -5,6 +5,7 @@ use unicode_segmentation::UnicodeSegmentation; use crate::ansi; use crate::cli; use crate::config::{self, delta_unreachable, Config}; +use crate::delta::DiffType; use crate::delta::State; use crate::edits; use crate::features::{line_numbers, OptionValueFunction}; @@ -180,7 +181,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), + None => &State::HunkMinus(DiffType::Unified, None), }, &mut Some(line_numbers_data), bg_should_fill[Left], @@ -201,7 +202,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), + None => &State::HunkPlus(DiffType::Unified, None), }, &mut Some(line_numbers_data), bg_should_fill[Right], @@ -222,7 +223,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(None)]; + let states = vec![State::HunkZero(DiffType::Unified)]; let (states, syntax_style_sections, diff_style_sections) = wrap_zero_block( config, @@ -418,8 +419,12 @@ fn paint_minus_or_plus_panel_line<'a>( ) } else { let opposite_state = match state { - State::HunkMinus(_, s) => State::HunkPlus(None, s.clone()), - State::HunkPlus(_, s) => State::HunkMinus(None, s.clone()), + State::HunkMinus(DiffType::Unified, s) => { + State::HunkPlus(DiffType::Unified, s.clone()) + } + State::HunkPlus(DiffType::Unified, s) => { + State::HunkMinus(DiffType::Unified, s.clone()) + } _ => unreachable!(), }; ( diff --git a/src/handlers/diff_header_diff.rs b/src/handlers/diff_header_diff.rs index 869e40e..56195e7 100644 --- a/src/handlers/diff_header_diff.rs +++ b/src/handlers/diff_header_diff.rs @@ -1,4 +1,4 @@ -use crate::delta::{DiffType, State, StateMachine}; +use crate::delta::{DiffType, MergeParents, State, StateMachine}; impl<'a> StateMachine<'a> { #[inline] @@ -14,7 +14,8 @@ impl<'a> StateMachine<'a> { self.painter.paint_buffered_minus_and_plus_lines(); 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 + // We will determine the number of parents when we see the hunk header. + State::DiffHeader(DiffType::Combined(MergeParents::Unknown)) } else { State::DiffHeader(DiffType::Unified) }; diff --git a/src/handlers/draw.rs b/src/handlers/draw.rs index d7edd8c..7f700b8 100644 --- a/src/handlers/draw.rs +++ b/src/handlers/draw.rs @@ -53,7 +53,7 @@ fn write_no_decoration( /// Write text to stream, surrounded by a box, leaving the cursor just /// beyond the bottom right corner. -fn write_boxed( +pub fn write_boxed( writer: &mut dyn Write, text: &str, raw_text: &str, diff --git a/src/handlers/hunk.rs b/src/handlers/hunk.rs index 93ac70c..8f1c612 100644 --- a/src/handlers/hunk.rs +++ b/src/handlers/hunk.rs @@ -4,7 +4,7 @@ use lazy_static::lazy_static; use crate::cli; use crate::config::delta_unreachable; -use crate::delta::{DiffType, State, StateMachine}; +use crate::delta::{DiffType, MergeParents, State, StateMachine}; use crate::style; use crate::utils::process::{self, CallingProcess}; use unicode_segmentation::UnicodeSegmentation; @@ -43,7 +43,9 @@ 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 DiffType::*; 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() { @@ -61,13 +63,14 @@ impl<'a> StateMachine<'a> { self.emit_hunk_header_line(line, raw_line)?; } self.state = match new_line_state(&self.line, &self.state) { - Some(HunkMinus(prefix, _)) => { + Some(HunkMinus(diff_type, _)) => { 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 n_parents = diff_type.n_parents(); + let line = self.painter.prepare(&self.line, n_parents); let state = match self.config.inspect_raw_lines { cli::InspectRawLines::True if style::line_has_style_other_than( @@ -75,18 +78,17 @@ impl<'a> StateMachine<'a> { [*style::GIT_DEFAULT_MINUS_STYLE, self.config.git_minus_style].iter(), ) => { - let raw_line = self - .painter - .prepare_raw_line(&self.raw_line, prefix.as_deref()); - HunkMinus(prefix, Some(raw_line)) + let raw_line = self.painter.prepare_raw_line(&self.raw_line, n_parents); + HunkMinus(diff_type, Some(raw_line)) } - _ => HunkMinus(prefix, None), + _ => HunkMinus(diff_type, None), }; self.painter.minus_lines.push((line, state.clone())); state } - Some(HunkPlus(prefix, _)) => { - let line = self.painter.prepare(&self.line, prefix.as_deref()); + Some(HunkPlus(diff_type, _)) => { + let n_parents = diff_type.n_parents(); + let line = self.painter.prepare(&self.line, n_parents); let state = match self.config.inspect_raw_lines { cli::InspectRawLines::True if style::line_has_style_other_than( @@ -94,23 +96,21 @@ impl<'a> StateMachine<'a> { [*style::GIT_DEFAULT_PLUS_STYLE, self.config.git_plus_style].iter(), ) => { - let raw_line = self - .painter - .prepare_raw_line(&self.raw_line, prefix.as_deref()); - HunkPlus(prefix, Some(raw_line)) + let raw_line = self.painter.prepare_raw_line(&self.raw_line, n_parents); + HunkPlus(diff_type, Some(raw_line)) } - _ => HunkPlus(prefix, None), + _ => HunkPlus(diff_type, None), }; self.painter.plus_lines.push((line, state.clone())); state } - Some(HunkZero(prefix)) => { + Some(HunkZero(diff_type)) => { // 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, prefix.clone()); - HunkZero(prefix) + self.painter.paint_zero_line(&self.line, diff_type.clone()); + HunkZero(diff_type) } _ => { // The first character here could be e.g. '\' from '\ No newline at end of file'. This @@ -121,7 +121,7 @@ impl<'a> StateMachine<'a> { .output_buffer .push_str(&self.painter.expand_tabs(self.raw_line.graphemes(true))); self.painter.output_buffer.push('\n'); - State::HunkZero(None) + State::HunkZero(Unified) } }; self.painter.emit()?; @@ -129,20 +129,34 @@ impl<'a> StateMachine<'a> { } } +// Return the new state corresponding to `new_line`, given the previous state. A return value of +// None means that `new_line` is not recognized as a hunk line. fn new_line_state(new_line: &str, prev_state: &State) -> Option<State> { + use DiffType::*; + use MergeParents::*; 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)), + HunkMinus(Unified, _) + | HunkZero(Unified) + | HunkPlus(Unified, _) + | HunkHeader(Unified, _, _) => Unified, + HunkHeader(Combined(Number(n)), _, _) => Combined(Number(*n)), + // The prefixes are specific to the previous line, but the number of merge parents remains + // equal to the prefix length. + HunkHeader(Combined(Prefix(prefix)), _, _) + | HunkMinus(Combined(Prefix(prefix)), _) + | HunkZero(Combined(Prefix(prefix))) + | HunkPlus(Combined(Prefix(prefix)), _) => Combined(Number(prefix.len())), + _ => delta_unreachable(&format!( + "Unexpected state in new_line_state: {:?}", + prev_state + )), }; let (prefix_char, prefix) = match diff_type { - DiffType::Unified => (new_line.chars().next(), None), - DiffType::Combined(n_parents) => { + Unified => (new_line.chars().next(), None), + Combined(Number(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), @@ -153,11 +167,16 @@ fn new_line_state(new_line: &str, prev_state: &State) -> Option<State> { }; (prefix_char, Some(prefix.to_string())) } + _ => delta_unreachable(""), }; - match prefix_char { - Some('-') => Some(HunkMinus(prefix, None)), - Some(' ') => Some(HunkZero(prefix)), - Some('+') => Some(HunkPlus(prefix, None)), + + match (prefix_char, prefix) { + (Some('-'), None) => Some(HunkMinus(Unified, None)), + (Some(' '), None) => Some(HunkZero(Unified)), + (Some('+'), None) => Some(HunkPlus(Unified, None)), + (Some('-'), Some(prefix)) => Some(HunkMinus(Combined(Prefix(prefix)), None)), + (Some(' '), Some(prefix)) => Some(HunkZero(Combined(Prefix(prefix)))), + (Some('+'), Some(prefix)) => Some(HunkPlus(Combined(Prefix(prefix)), None)), _ => None, } } diff --git a/src/handlers/hunk_header.rs b/src/handlers/hunk_header.rs index 39c223d..f96c57b 100644 --- a/src/handlers/hunk_header.rs +++ b/src/handlers/hunk_header.rs @@ -25,30 +25,43 @@ use lazy_static::lazy_static; use regex::Regex; use super::draw; -use crate::config::Config; -use crate::delta::{self, DiffType, State, StateMachine}; +use crate::config::{delta_unreachable, Config}; +use crate::delta::{self, DiffType, MergeParents, State, StateMachine}; use crate::paint::{self, BgShouldFill, Painter, StyleSectionSpecifier}; use crate::style::DecorationStyle; impl<'a> StateMachine<'a> { #[inline] fn test_hunk_header_line(&self) -> bool { - self.line.starts_with("@@") + self.line.starts_with("@@") && + // A hunk header can occur within a merge conflict region, but we don't attempt to handle + // that. See #822. + !matches!(self.state, State::MergeConflict(_, _)) } pub fn handle_hunk_header_line(&mut self) -> std::io::Result<bool> { + use DiffType::*; + use State::*; if !self.test_hunk_header_line() { return Ok(false); } let diff_type = match &self.state { - State::DiffHeader(DiffType::Combined(_)) => { + DiffHeader(Combined(MergeParents::Unknown)) => { // https://git-scm.com/docs/git-diff#_combined_diff_format let n_parents = self.line.chars().take_while(|c| c == &'@').count() - 1; - DiffType::Combined(n_parents) + Combined(MergeParents::Number(n_parents)) } - _ => DiffType::Unified, + DiffHeader(diff_type) + | HunkMinus(diff_type, _) + | HunkZero(diff_type) + | HunkPlus(diff_type, _) => diff_type.clone(), + Unknown => Unified, + _ => delta_unreachable(&format!( + "Unexpected state in handle_hunk_header: {:?}", + self.state + )), }; - self.state = State::HunkHeader(diff_type, self.line.clone(), self.raw_line.clone()); + self.state = HunkHeader(diff_type, self.line.clone(), self.raw_line.clone()); Ok(true) } diff --git a/src/handlers/merge_conflict.rs b/src/handlers/merge_conflict.rs new file mode 100644 index 0000000..5c5c093 --- /dev/null +++ b/src/handlers/merge_conflict.rs @@ -0,0 +1,1340 @@ +use std::ops::{Index, IndexMut}; + +use itertools::Itertools; +use unicode_segmentation::UnicodeSegmentation; + +use super::draw; +use crate::cli; +use crate::config::{self, delta_unreachable}; +use crate::delta::{DiffType, MergeParents, State, StateMachine}; +use crate::minusplus::MinusPlus; +use crate::paint; +use crate::style::Style; + +#[derive(Clone, Debug, PartialEq)] +pub enum MergeConflictCommit { + Ours, + Ancestral, + Theirs, +} + +pub struct MergeConflictCommits<T> { + ours: T, + ancestral: T, + theirs: T, +} + +pub type MergeConflictLines = MergeConflictCommits<Vec<(String, State)>>; + +pub type MergeConflictCommitNames = MergeConflictCommits<Option<String>>; + +impl<'a> StateMachine<'a> { + pub fn handle_merge_conflict_line(&mut self) -> std::io::Result<bool> { + use DiffType::*; + use MergeConflictCommit::*; + use State::*; + + let mut handled_line = false; + if self.config.color_only || !self.config.handle_merge_conflicts { + return Ok(handled_line); + } + + match self.state.clone() { + HunkHeader(Combined(merge_parents), _, _) + | HunkMinus(Combined(merge_parents), _) + | HunkZero(Combined(merge_parents)) + | HunkPlus(Combined(merge_parents), _) => { + handled_line = self.enter_merge_conflict(&merge_parents) + } + MergeConflict(merge_parents, Ours) => { + handled_line = self.enter_ancestral(&merge_parents) + || self.enter_theirs(&merge_parents) + || self.exit_merge_conflict(&merge_parents)? + || self.store_line(Ours, HunkPlus(Combined(merge_parents), None)); + } + MergeConflict(merge_parents, Ancestral) => { + handled_line = self.enter_theirs(&merge_parents) + || self.exit_merge_conflict(&merge_parents)? + || self.store_line(Ancestral, HunkMinus(Combined(merge_parents), None)); + } + MergeConflict(merge_parents, Theirs) => { + handled_line = self.exit_merge_conflict(&merge_parents)? + || self.store_line(Theirs, HunkPlus(Combined(merge_parents), None)); + } + _ => {} + } + + Ok(handled_line) + } + + fn enter_merge_conflict(&mut self, merge_parents: &MergeParents) -> bool { + use State::*; + if let Some(commit) = parse_merge_marker(&self.line, "++<<<<<<<") { + self.state = MergeConflict(merge_parents.clone(), Ours); + self.painter.merge_conflict_commit_names[Ours] = Some(commit.to_string()); + true + } else { + false + } + } + + fn enter_ancestral(&mut self, merge_parents: &MergeParents) -> bool { + use State::*; + if let Some(commit) = parse_merge_marker(&self.line, "++|||||||") { + self.state = MergeConflict(merge_parents.clone(), Ancestral); + self.painter.merge_conflict_commit_names[Ancestral] = Some(commit.to_string()); + true + } else { + false + } + } + + fn enter_theirs(&mut self, merge_parents: &MergeParents) -> bool { + use State::*; + if self.line.starts_with("++=======") { + self.state = MergeConflict(merge_parents.clone(), Theirs); + true + } else { + false + } + } + + fn exit_merge_conflict(&mut self, merge_parents: &MergeParents) -> std::io::Result<bool> { + if let Some(commit) = parse_merge_marker(&self.line, "++>>>>>>>") { + self.painter.merge_conflict_commit_names[Theirs] = Some(commit.to_string()); + self.paint_buffered_merge_conflict_lines(merge_parents)?; + Ok(true) + } else { + Ok(false) + } + } + + fn store_line(&mut self, commit: MergeConflictCommit, state: State) -> bool { + use State::*; + if let HunkMinus(diff_type, _) | HunkZero(diff_type) | HunkPlus(diff_type, _) = &state { + let line = self.painter.prepare(&self.line, diff_type.n_parents()); + self.painter.merge_conflict_lines[commit].push((line, state)); + true + } else { + delta_unreachable(&format!("Invalid state: {:?}", state)) + } + } + + fn paint_buffered_merge_conflict_lines( + &mut self, + merge_parents: &MergeParents, + ) -> std::io::Result<()> { + use DiffType::*; + use State::*; + self.painter.emit()?; + + write_merge_conflict_bar( + &self.config.merge_conflict_begin_symbol, + &mut self.painter, + self.config, + )?; + for (derived_commit_type, header_style) in &[ + (Ours, self.config.merge_conflict_ours_diff_header_style), + (Theirs, self.config.merge_conflict_theirs_diff_header_style), + ] { + write_diff_header( + derived_commit_type, + *header_style, + &mut self.painter, + self.config, + )?; + self.painter.emit()?; + paint::paint_minus_and_plus_lines( + MinusPlus::new( + &self.painter.merge_conflict_lines[Ancestral], + &self.painter.merge_conflict_lines[derived_commit_type], + ), + &mut self.painter.line_numbers_data, + &mut self.painter.highlighter, + &mut self.painter.output_buffer, + self.config, + ); + self.painter.emit()?; + } + // write_merge_conflict_decoration("bold ol", &mut self.painter, self.config)?; + write_merge_conflict_bar( + &self.config.merge_conflict_end_symbol, + &mut self.painter, + self.config, + )?; + self.painter.merge_conflict_lines.clear(); + self.state = HunkZero(Combined(merge_parents.clone())); + Ok(()) + } +} + +fn write_diff_header( + derived_commit_type: &MergeConflictCommit, + style: Style, + painter: &mut paint::Painter, + config: &config::Config, +) -> std::io::Result<()> { + let (mut draw_fn, pad, decoration_ansi_term_style) = + draw::get_draw_function(style.decoration_style); + let derived_commit_name = &painter.merge_conflict_commit_names[derived_commit_type]; + let text = if let Some(_ancestral_commit) = &painter.merge_conflict_commit_names[Ancestral] { + format!( + "ancestor {} {}{}", + config.right_arrow, + derived_commit_name.as_deref().unwrap_or("?"), + if pad { " " } else { "" } + ) + } else { + derived_commit_name.as_deref().unwrap_or("?").to_string() + }; + draw_fn( + painter.writer, + &text, + &text, + &config.decorations_width, + style, + decoration_ansi_term_style, + )?; + Ok(()) +} + +fn write_merge_conflict_bar( + s: &str, + painter: &mut paint::Painter, + config: &config::Config, +) -> std::io::Result<()> { + if let cli::Width::Fixed(width) = config.decorations_width { + writeln!( + painter.writer, + "{}", + &s.graphemes(true).cycle().take(width).join("") + )?; + } + Ok(()) +} + +fn parse_merge_marker<'a>(line: &'a str, marker: &str) -> Option<&'a str> { + match line.strip_prefix(marker) { + Some(suffix) => { + let suffix = suffix.trim(); + if !suffix.is_empty() { + Some(suffix) + } else { + None + } + } + None => None, + } +} + +pub use MergeConflictCommit::*; + +impl<T> Index<MergeConflictCommit> for MergeConflictCommits<T> { + type Output = T; + fn index(&self, commit: MergeConflictCommit) -> &Self::Output { + match commit { + Ours => &self.ours, + Ancestral => &self.ancestral, + Theirs => &self.theirs, + } + } +} + +impl<T> Index<&MergeConflictCommit> for MergeConflictCommits<T> { + type Output = T; + fn index(&self, commit: &MergeConflictCommit) -> &Self::Output { + match commit { + Ours => &self.ours, + Ancestral => &self.ancestral, + Theirs => &self.theirs, + } + } +} + +impl<T> IndexMut<MergeConflictCommit> for MergeConflictCommits<T> { + fn index_mut(&mut self, commit: MergeConflictCommit) -> &mut Self::Output { + match commit { + Ours => &mut self.ours, + Ancestral => &mut self.ancestral, + Theirs => &mut self.theirs, + } + } +} + +impl MergeConflictLines { + pub fn new() -> Self { + Self { + ours: Vec::new(), + ancestral: Vec::new(), + theirs: Vec::new(), + } + } + + fn clear(&mut self) { + self[Ours].clear(); + self[Ancestral].clear(); + self[Theirs].clear(); + } +} + +impl MergeConflictCommitNames { + pub fn new() -> Self { + Self { + ours: None, + ancestral: None, + theirs: None, + } + } +} + +#[cfg(test)] +mod tests { + use crate::ansi::strip_ansi_codes; + use crate::tests::integration_test_utils; + + #[test] + fn test_toy_merge_conflict_no_context() { + let config = integration_test_utils::make_config_from_args(&[]); + let output = integration_test_utils::run_delta(GIT_TOY_MERGE_CONFLICT_NO_CONTEXT, &config); + let output = strip_ansi_codes(&output); + assert!(output.contains("\n▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼")); + assert!(output.contains( + "\ +──────────────────┐ +ancestor ⟶ HEAD │ +──────────────────┘ +" + )); + assert!(output.contains("\n▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲")); + } + + #[test] + fn test_real_merge_conflict() { + let config = integration_test_utils::make_config_from_args(&[]); + let output = integration_test_utils::run_delta(GIT_MERGE_CONFLICT, &config); + let output = strip_ansi_codes(&output); + assert!(output.contains("\n▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼")); + assert!(output.contains( + "\ +──────────────────┐ +ancestor ⟶ HEAD │ +──────────────────┘ +" + )); + assert!(output.contains("\n▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲")); + } + + #[test] + #[allow(non_snake_case)] + fn test_real_merge_conflict_U0() { + let config = integration_test_utils::make_config_from_args(&[]); + let output = integration_test_utils::run_delta(GIT_MERGE_CONFLICT_U0, &config); + let output = strip_ansi_codes(&output); + assert!(output.contains("\n▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼")); + assert!(output.contains( + "\ +──────────────────┐ +ancestor ⟶ HEAD │ +──────────────────┘ +" + )); + assert!(output.contains("\n▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲")); + } + + const GIT_TOY_MERGE_CONFLICT_NO_CONTEXT: &str = "\ +diff --cc file +index 6178079,7898192..0000000 +--- a/file ++++ b/file +@@@ -1,1 -1,1 +1,6 @@@ +++<<<<<<< HEAD + +a +++||||||| parent of 0c20c9d... wip +++======= ++ b +++>>>>>>> 0c20c9d... wip +"; + + const GIT_MERGE_CONFLICT: &str = r#"\ +diff --cc src/handlers/merge_conflict.rs +index 27d47c0,3a7e7b9..0000000 +--- a/src/handlers/merge_conflict.rs ++++ b/src/handlers/merge_conflict.rs +@@@ -1,14 -1,13 +1,24 @@@ + -use std::cmp::min; + use std::ops::{Index, IndexMut}; + +++<<<<<<< HEAD + +use itertools::Itertools; + +use unicode_segmentation::UnicodeSegmentation; + + + +use super::draw; + +use crate::cli; + +use crate::config::{self, delta_unreachable}; + +use crate::delta::{DiffType, InMergeConflict, MergeParents, State, StateMachine}; +++||||||| parent of b2b28c8... Display merge conflict branches +++use crate::delta::{DiffType, MergeParents, State, StateMachine}; +++======= ++ use super::draw; ++ use crate::cli; ++ use crate::config::{self, delta_unreachable}; ++ use crate::delta::{DiffType, MergeParents, State, StateMachine}; +++>>>>>>> b2b28c8... Display merge conflict branches + use crate::minusplus::MinusPlus; + use crate::paint; ++ use crate::style::DecorationStyle; + + #[derive(Clone, Debug, PartialEq)] + pub enum MergeConflictCommit { +@@@ -30,7 -29,8 +40,15 @@@ pub type MergeConflictCommitNames = Mer + impl<'a> StateMachine<'a> { + pub fn handle_merge_conflict_line(&mut self) -> std::io::Result<bool> { + use DiffType::*; +++<<<<<<< HEAD + use MergeConflictCommit::*; +++||||||| parent of b2b28c8... Display merge conflict branches ++ use MergeParents::*; +++ use Source::*; +++======= +++ use MergeConflictCommit::*; +++ use MergeParents::*; +++>>>>>>> b2b28c8... Display merge conflict branches + use State::*; + + let mut handled_line = false; +@@@ -38,36 -38,28 +56,113 @@@ + return Ok(handled_line); + } + +++<<<<<<< HEAD + + match self.state.clone() { + + HunkHeader(Combined(merge_parents, InMergeConflict::No), _, _) + + | HunkMinus(Combined(merge_parents, InMergeConflict::No), _) + + | HunkZero(Combined(merge_parents, InMergeConflict::No)) + + | HunkPlus(Combined(merge_parents, InMergeConflict::No), _) => { + + handled_line = self.enter_merge_conflict(&merge_parents) + + } + + MergeConflict(merge_parents, Ours) => { + + handled_line = self.enter_ancestral(&merge_parents) + + || self.enter_theirs(&merge_parents) + + || self.exit_merge_conflict(&merge_parents)? + + || self.store_line( + + Ours, + + HunkPlus(Combined(merge_parents, InMergeConflict::Yes), None), + + ); +++||||||| parent of b2b28c8... Display merge conflict branches +++ // TODO: don't allocate on heap at this point +++ let prefix = self.line[..min(self.line.len(), 2)].to_string(); +++ let diff_type = Combined(Prefix(prefix)); +++ +++ match self.state { +++ // The only transition into a merge conflict is HunkZero => MergeConflict(Ours) +++ // TODO: shouldn't this be HunkZero(Some(_))? +++ HunkZero(_) => { +++ if self.line.starts_with("++<<<<<<<") { +++ self.state = MergeConflict(Ours); +++ handled_line = true +++ } + + } +++ MergeConflict(Ours) => { +++ if self.line.starts_with("++|||||||") { +++ self.state = MergeConflict(Ancestral); +++ } else if self.line.starts_with("++=======") { +++ self.state = MergeConflict(Theirs); +++ } else if self.line.starts_with("++>>>>>>>") { +++ self.paint_buffered_merge_conflict_lines(diff_type)?; +++ } else { +++ let line = self.painter.prepare(&self.line, diff_type.n_parents()); +++ self.painter.merge_conflict_lines[Ours].push((line, HunkPlus(diff_type, None))); +++ } +++ handled_line = true +++======= ++ // TODO: don't allocate on heap at this point ++ let prefix = self.line[..min(self.line.len(), 2)].to_string(); ++ let diff_type = Combined(Prefix(prefix)); ++ ++ match self.state { ++ // The only transition into a merge conflict is HunkZero => MergeConflict(Ours) ++ // TODO: shouldn't this be HunkZero(Some(_))? ++ HunkZero(_) => handled_line = self.enter_merge_conflict(), ++ MergeConflict(Ours) => { ++ handled_line = self.enter_ancestral() ++ || self.enter_theirs() ++ || self.exit_merge_conflict(diff_type.clone())? ++ || self.store_line(Ours, HunkPlus(diff_type, None)); +++>>>>>>> b2b28c8... Display merge conflict branches ++ } +++<<<<<<< HEAD + + MergeConflict(merge_parents, Ancestral) => { + + handled_line = self.enter_theirs(&merge_parents) + + || self.exit_merge_conflict(&merge_parents)? + + || self.store_line( + + Ancestral, + + HunkMinus(Combined(merge_parents, InMergeConflict::Yes), None), + + ); +++||||||| parent of b2b28c8... Display merge conflict branches +++ MergeConflict(Ancestral) => { +++ if self.line.starts_with("++=======") { +++ self.state = MergeConflict(Theirs); +++ } else if self.line.starts_with("++>>>>>>>") { +++ self.paint_buffered_merge_conflict_lines(diff_type)?; +++ } else { +++ let line = self.painter.prepare(&self.line, diff_type.n_parents()); +++ self.painter.merge_conflict_lines[Ancestral] +++ .push((line, HunkMinus(diff_type, None))); +++ } +++ handled_line = true +++======= ++ MergeConflict(Ancestral) => { ++ handled_line = self.enter_theirs() ++ || self.exit_merge_conflict(diff_type.clone())? ++ || self.store_line(Ancestral, HunkMinus(diff_type, None)); +++>>>>>>> b2b28c8... Display merge conflict branches + } +++<<<<<<< HEAD + + MergeConflict(merge_parents, Theirs) => { + + handled_line = self.exit_merge_conflict(&merge_parents)? + + || self.store_line( + + Theirs, + + HunkPlus(Combined(merge_parents, InMergeConflict::Yes), None), + + ); +++||||||| parent of b2b28c8... Display merge conflict branches +++ MergeConflict(Theirs) => { +++ if self.line.starts_with("++>>>>>>>") { +++ self.paint_buffered_merge_conflict_lines(diff_type)?; +++ } else { +++ let line = self.painter.prepare(&self.line, diff_type.n_parents()); +++ self.painter.merge_conflict_lines[Theirs] +++ .push((line, HunkPlus(diff_type, None))); +++ } +++ handled_line = true +++======= ++ MergeConflict(Theirs) => { ++ handled_line = self.exit_merge_conflict(diff_type.clone())? ++ || self.store_line(Theirs, HunkPlus(diff_type, None)); +++>>>>>>> b2b28c8... Display merge conflict branches + } + _ => {} + } +@@@ -75,75 -67,71 +170,150 @@@ + Ok(handled_line) + } + +++<<<<<<< HEAD + + fn enter_merge_conflict(&mut self, merge_parents: &MergeParents) -> bool { + + use State::*; + + if let Some(commit) = parse_merge_marker(&self.line, "++<<<<<<<") { + + self.state = MergeConflict(merge_parents.clone(), Ours); + + self.painter.merge_conflict_commit_names[Ours] = Some(commit.to_string()); + + true + + } else { + + false + + } + + } + + + + fn enter_ancestral(&mut self, merge_parents: &MergeParents) -> bool { + + use State::*; + + if let Some(commit) = parse_merge_marker(&self.line, "++|||||||") { + + self.state = MergeConflict(merge_parents.clone(), Ancestral); + + self.painter.merge_conflict_commit_names[Ancestral] = Some(commit.to_string()); + + true + + } else { + + false + + } + + } + + + + fn enter_theirs(&mut self, merge_parents: &MergeParents) -> bool { + + use State::*; + + if self.line.starts_with("++=======") { + + self.state = MergeConflict(merge_parents.clone(), Theirs); + + true + + } else { + + false + + } + + } + + + + fn exit_merge_conflict(&mut self, merge_parents: &MergeParents) -> std::io::Result<bool> { + + if let Some(commit) = parse_merge_marker(&self.line, "++>>>>>>>") { + + self.painter.merge_conflict_commit_names[Theirs] = Some(commit.to_string()); + + self.paint_buffered_merge_conflict_lines(merge_parents)?; + + Ok(true) + + } else { + + Ok(false) + + } + + } + + + + fn store_line(&mut self, commit: MergeConflictCommit, state: State) -> bool { + + use State::*; + + if let HunkMinus(diff_type, _) | HunkZero(diff_type) | HunkPlus(diff_type, _) = &state { + + let line = self.painter.prepare(&self.line, diff_type.n_parents()); + + self.painter.merge_conflict_lines[commit].push((line, state)); + + true + + } else { + + delta_unreachable(&format!("Invalid state: {:?}", state)) + + } + + } + + + + fn paint_buffered_merge_conflict_lines( + + &mut self, + + merge_parents: &MergeParents, + + ) -> std::io::Result<()> { + + use DiffType::*; + + use State::*; +++||||||| parent of b2b28c8... Display merge conflict branches +++ fn paint_buffered_merge_conflict_lines(&mut self, diff_type: DiffType) -> std::io::Result<()> { +++======= ++ fn enter_merge_conflict(&mut self) -> bool { ++ use State::*; ++ if let Some(commit) = parse_merge_marker(&self.line, "++<<<<<<<") { ++ self.state = MergeConflict(Ours); ++ self.painter.merge_conflict_commit_names[Ours] = Some(commit.to_string()); ++ true ++ } else { ++ false ++ } ++ } ++ ++ fn enter_ancestral(&mut self) -> bool { ++ use State::*; ++ if let Some(commit) = parse_merge_marker(&self.line, "++|||||||") { ++ self.state = MergeConflict(Ancestral); ++ self.painter.merge_conflict_commit_names[Ancestral] = Some(commit.to_string()); ++ true ++ } else { ++ false ++ } ++ } ++ ++ fn enter_theirs(&mut self) -> bool { ++ use State::*; ++ if self.line.starts_with("++=======") { ++ self.state = MergeConflict(Theirs); ++ true ++ } else { ++ false ++ } ++ } ++ ++ fn exit_merge_conflict(&mut self, diff_type: DiffType) -> std::io::Result<bool> { ++ if let Some(commit) = parse_merge_marker(&self.line, "++>>>>>>>") { ++ self.painter.merge_conflict_commit_names[Theirs] = Some(commit.to_string()); ++ self.paint_buffered_merge_conflict_lines(diff_type)?; ++ Ok(true) ++ } else { ++ Ok(false) ++ } ++ } ++ ++ fn store_line(&mut self, commit: MergeConflictCommit, state: State) -> bool { ++ use State::*; ++ if let HunkMinus(diff_type, _) | HunkZero(diff_type) | HunkPlus(diff_type, _) = &state { ++ let line = self.painter.prepare(&self.line, diff_type.n_parents()); ++ self.painter.merge_conflict_lines[commit].push((line, state)); ++ true ++ } else { ++ delta_unreachable(&format!("Invalid state: {:?}", state)) ++ } ++ } ++ ++ fn paint_buffered_merge_conflict_lines(&mut self, diff_type: DiffType) -> std::io::Result<()> { +++>>>>>>> b2b28c8... Display merge conflict branches + self.painter.emit()?; +++<<<<<<< HEAD + + + + write_merge_conflict_bar( + + &self.config.merge_conflict_begin_symbol, + + &mut self.painter, + + self.config, + + )?; + + for derived_commit_type in &[Ours, Theirs] { + + write_diff_header(derived_commit_type, &mut self.painter, self.config)?; + + self.painter.emit()?; +++||||||| parent of b2b28c8... Display merge conflict branches +++ let lines = &self.painter.merge_conflict_lines; +++ for derived_lines in &[&lines[Ours], &lines[Theirs]] { +++======= ++ ++ write_merge_conflict_bar("▼", &mut self.painter, self.config)?; ++ for (derived_commit_type, decoration_style) in &[(Ours, "box"), (Theirs, "box")] { ++ write_subhunk_header( ++ derived_commit_type, ++ decoration_style, ++ &mut self.painter, ++ self.config, ++ )?; ++ self.painter.emit()?; +++>>>>>>> b2b28c8... Display merge conflict branches + paint::paint_minus_and_plus_lines( + MinusPlus::new( + &self.painter.merge_conflict_lines[Ancestral], +@@@ -156,78 -144,94 +326,190 @@@ + ); + self.painter.emit()?; + } +++<<<<<<< HEAD + + // write_merge_conflict_decoration("bold ol", &mut self.painter, self.config)?; + + write_merge_conflict_bar( + + &self.config.merge_conflict_end_symbol, + + &mut self.painter, + + self.config, + + )?; +++||||||| parent of b2b28c8... Display merge conflict branches +++======= ++ // write_merge_conflict_decoration("bold ol", &mut self.painter, self.config)?; ++ write_merge_conflict_bar("▲", &mut self.painter, self.config)?; +++>>>>>>> b2b28c8... Display merge conflict branches + self.painter.merge_conflict_lines.clear(); + - self.state = State::HunkZero(diff_type); + + self.state = HunkZero(Combined(merge_parents.clone(), InMergeConflict::No)); + Ok(()) + } + } + +++<<<<<<< HEAD + +fn write_diff_header( + + derived_commit_type: &MergeConflictCommit, + + painter: &mut paint::Painter, + + config: &config::Config, + +) -> std::io::Result<()> { + + let (mut draw_fn, pad, decoration_ansi_term_style) = + + draw::get_draw_function(config.merge_conflict_diff_header_style.decoration_style); + + let derived_commit_name = &painter.merge_conflict_commit_names[derived_commit_type]; + + let text = if let Some(_ancestral_commit) = &painter.merge_conflict_commit_names[Ancestral] { + + format!( + + "ancestor {} {}{}", + + config.right_arrow, + + derived_commit_name.as_deref().unwrap_or("?"), + + if pad { " " } else { "" } + + ) + + } else { + + derived_commit_name.as_deref().unwrap_or("?").to_string() + + }; + + draw_fn( + + painter.writer, + + &text, + + &text, + + &config.decorations_width, + + config.merge_conflict_diff_header_style, + + decoration_ansi_term_style, + + )?; + + Ok(()) + +} + + + +fn write_merge_conflict_bar( + + s: &str, + + painter: &mut paint::Painter, + + config: &config::Config, + +) -> std::io::Result<()> { + + if let cli::Width::Fixed(width) = config.decorations_width { + + writeln!( + + painter.writer, + + "{}", + + &s.graphemes(true).cycle().take(width).join("") + + )?; + + } + + Ok(()) + +} + + + +fn parse_merge_marker<'a>(line: &'a str, marker: &str) -> Option<&'a str> { + + match line.strip_prefix(marker) { + + Some(suffix) => { + + let suffix = suffix.trim(); + + if !suffix.is_empty() { + + Some(suffix) + + } else { + + None + + } + + } + + None => None, + + } + +} + + + +pub use MergeConflictCommit::*; + + +++impl<T> Index<MergeConflictCommit> for MergeConflictCommits<T> { +++ type Output = T; +++ fn index(&self, commit: MergeConflictCommit) -> &Self::Output { +++ match commit { +++ Ours => &self.ours, +++ Ancestral => &self.ancestral, +++ Theirs => &self.theirs, +++ } +++ } +++} +++||||||| parent of b2b28c8... Display merge conflict branches +++pub use Source::*; +++======= ++ fn write_subhunk_header( ++ derived_commit_type: &MergeConflictCommit, ++ decoration_style: &str, ++ painter: &mut paint::Painter, ++ config: &config::Config, ++ ) -> std::io::Result<()> { ++ let (mut draw_fn, pad, decoration_ansi_term_style) = ++ draw::get_draw_function(DecorationStyle::from_str( ++ decoration_style, ++ config.true_color, ++ config.git_config.as_ref(), ++ )); ++ let derived_commit_name = &painter.merge_conflict_commit_names[derived_commit_type]; ++ let text = if let Some(_ancestral_commit) = &painter.merge_conflict_commit_names[Ancestral] { ++ format!( ++ "ancestor {} {}{}", ++ config.right_arrow, ++ derived_commit_name.as_deref().unwrap_or("?"), ++ if pad { " " } else { "" } ++ ) ++ } else { ++ derived_commit_name.as_deref().unwrap_or("?").to_string() ++ }; ++ draw_fn( ++ painter.writer, ++ &text, ++ &text, ++ &config.decorations_width, ++ config.hunk_header_style, ++ decoration_ansi_term_style, ++ )?; ++ Ok(()) ++ } +++>>>>>>> b2b28c8... Display merge conflict branches ++ +++<<<<<<< HEAD +++impl<T> Index<&MergeConflictCommit> for MergeConflictCommits<T> { +++ type Output = T; +++ fn index(&self, commit: &MergeConflictCommit) -> &Self::Output { +++ match commit { +++||||||| parent of b2b28c8... Display merge conflict branches +++impl Index<Source> for MergeConflictLines { +++ type Output = Vec<(String, State)>; +++ fn index(&self, source: Source) -> &Self::Output { +++ match source { +++======= ++ #[allow(unused)] ++ fn write_merge_conflict_line( ++ painter: &mut paint::Painter, ++ config: &config::Config, ++ ) -> std::io::Result<()> { ++ let (mut draw_fn, _pad, decoration_ansi_term_style) = draw::get_draw_function( ++ DecorationStyle::from_str("bold ol", config.true_color, config.git_config.as_ref()), ++ ); ++ draw_fn( ++ painter.writer, ++ "", ++ "", ++ &config.decorations_width, ++ config.hunk_header_style, ++ decoration_ansi_term_style, ++ )?; ++ Ok(()) ++ } ++ ++ fn write_merge_conflict_bar( ++ s: &str, ++ painter: &mut paint::Painter, ++ config: &config::Config, ++ ) -> std::io::Result<()> { ++ if let cli::Width::Fixed(width) = config.decorations_width { ++ writeln!(painter.writer, "{}", s.repeat(width))?; ++ } ++ Ok(()) ++ } ++ ++ fn parse_merge_marker<'a>(line: &'a str, marker: &str) -> Option<&'a str> { ++ match line.strip_prefix(marker) { ++ Some(suffix) => { ++ let suffix = suffix.trim(); ++ if !suffix.is_empty() { ++ Some(suffix) ++ } else { ++ None ++ } ++ } ++ None => None, ++ } ++ } ++ ++ pub use MergeConflictCommit::*; ++ + impl<T> Index<MergeConflictCommit> for MergeConflictCommits<T> { + type Output = T; + fn index(&self, commit: MergeConflictCommit) -> &Self::Output { +@@@ -243,6 -247,6 +525,7 @@@ impl<T> Index<&MergeConflictCommit> fo + type Output = T; + fn index(&self, commit: &MergeConflictCommit) -> &Self::Output { + match commit { +++>>>>>>> b2b28c8... Display merge conflict branches + Ours => &self.ours, + Ancestral => &self.ancestral, + Theirs => &self.theirs, +"#; + + const GIT_MERGE_CONFLICT_U0: &str = r#"\ +diff --cc src/handlers/merge_conflict.rs +index 27d47c0,3a7e7b9..0000000 +--- a/src/handlers/merge_conflict.rs ++++ b/src/handlers/merge_conflict.rs +@@@ -3,7 -4,4 +3,16 @@@ use std::ops::{Index, IndexMut} +++<<<<<<< HEAD + +use itertools::Itertools; + +use unicode_segmentation::UnicodeSegmentation; + + + +use super::draw; + +use crate::cli; + +use crate::config::{self, delta_unreachable}; + +use crate::delta::{DiffType, InMergeConflict, MergeParents, State, StateMachine}; +++||||||| parent of b2b28c8... Display merge conflict branches +++use crate::delta::{DiffType, MergeParents, State, StateMachine}; +++======= ++ use super::draw; ++ use crate::cli; ++ use crate::config::{self, delta_unreachable}; ++ use crate::delta::{DiffType, MergeParents, State, StateMachine}; +++>>>>>>> b2b28c8... Display merge conflict branches +@@@ -33,0 -32,0 +43,1 @@@ impl<'a> StateMachine<'a> +++<<<<<<< HEAD +@@@ -34,0 -33,1 +45,7 @@@ +++||||||| parent of b2b28c8... Display merge conflict branches ++ use MergeParents::*; +++ use Source::*; +++======= +++ use MergeConflictCommit::*; +++ use MergeParents::*; +++>>>>>>> b2b28c8... Display merge conflict branches +@@@ -41,23 -41,18 +59,84 @@@ +++<<<<<<< HEAD + + match self.state.clone() { + + HunkHeader(Combined(merge_parents, InMergeConflict::No), _, _) + + | HunkMinus(Combined(merge_parents, InMergeConflict::No), _) + + | HunkZero(Combined(merge_parents, InMergeConflict::No)) + + | HunkPlus(Combined(merge_parents, InMergeConflict::No), _) => { + + handled_line = self.enter_merge_conflict(&merge_parents) + + } + + MergeConflict(merge_parents, Ours) => { + + handled_line = self.enter_ancestral(&merge_parents) + + || self.enter_theirs(&merge_parents) + + || self.exit_merge_conflict(&merge_parents)? + + || self.store_line( + + Ours, + + HunkPlus(Combined(merge_parents, InMergeConflict::Yes), None), + + ); +++||||||| parent of b2b28c8... Display merge conflict branches +++ // TODO: don't allocate on heap at this point +++ let prefix = self.line[..min(self.line.len(), 2)].to_string(); +++ let diff_type = Combined(Prefix(prefix)); +++ +++ match self.state { +++ // The only transition into a merge conflict is HunkZero => MergeConflict(Ours) +++ // TODO: shouldn't this be HunkZero(Some(_))? +++ HunkZero(_) => { +++ if self.line.starts_with("++<<<<<<<") { +++ self.state = MergeConflict(Ours); +++ handled_line = true +++ } + + } +++ MergeConflict(Ours) => { +++ if self.line.starts_with("++|||||||") { +++ self.state = MergeConflict(Ancestral); +++ } else if self.line.starts_with("++=======") { +++ self.state = MergeConflict(Theirs); +++ } else if self.line.starts_with("++>>>>>>>") { +++ self.paint_buffered_merge_conflict_lines(diff_type)?; +++ } else { +++ let line = self.painter.prepare(&self.line, diff_type.n_parents()); +++ self.painter.merge_conflict_lines[Ours].push((line, HunkPlus(diff_type, None))); +++ } +++ handled_line = true +++======= ++ // TODO: don't allocate on heap at this point ++ let prefix = self.line[..min(self.line.len(), 2)].to_string(); ++ let diff_type = Combined(Prefix(prefix)); ++ ++ match self.state { ++ // The only transition into a merge conflict is HunkZero => MergeConflict(Ours) ++ // TODO: shouldn't this be HunkZero(Some(_))? ++ HunkZero(_) => handled_line = self.enter_merge_conflict(), ++ MergeConflict(Ours) => { ++ handled_line = self.enter_ancestral() ++ || self.enter_theirs() ++ || self.exit_merge_conflict(diff_type.clone())? ++ || self.store_line(Ours, HunkPlus(diff_type, None)); +++>>>>>>> b2b28c8... Display merge conflict branches ++ } +++<<<<<<< HEAD + + MergeConflict(merge_parents, Ancestral) => { + + handled_line = self.enter_theirs(&merge_parents) + + || self.exit_merge_conflict(&merge_parents)? + + || self.store_line( + + Ancestral, + + HunkMinus(Combined(merge_parents, InMergeConflict::Yes), None), + + ); +++||||||| parent of b2b28c8... Display merge conflict branches +++ MergeConflict(Ancestral) => { +++ if self.line.starts_with("++=======") { +++ self.state = MergeConflict(Theirs); +++ } else if self.line.starts_with("++>>>>>>>") { +++ self.paint_buffered_merge_conflict_lines(diff_type)?; +++ } else { +++ let line = self.painter.prepare(&self.line, diff_type.n_parents()); +++ self.painter.merge_conflict_lines[Ancestral] +++ .push((line, HunkMinus(diff_type, None))); +++ } +++ handled_line = true +++======= ++ MergeConflict(Ancestral) => { ++ handled_line = self.enter_theirs() ++ || self.exit_merge_conflict(diff_type.clone())? ++ || self.store_line(Ancestral, HunkMinus(diff_type, None)); +++>>>>>>> b2b28c8... Display merge conflict branches +@@@ -65,6 -60,3 +144,22 @@@ +++<<<<<<< HEAD + + MergeConflict(merge_parents, Theirs) => { + + handled_line = self.exit_merge_conflict(&merge_parents)? + + || self.store_line( + + Theirs, + + HunkPlus(Combined(merge_parents, InMergeConflict::Yes), None), + + ); +++||||||| parent of b2b28c8... Display merge conflict branches +++ MergeConflict(Theirs) => { +++ if self.line.starts_with("++>>>>>>>") { +++ self.paint_buffered_merge_conflict_lines(diff_type)?; +++ } else { +++ let line = self.painter.prepare(&self.line, diff_type.n_parents()); +++ self.painter.merge_conflict_lines[Theirs] +++ .push((line, HunkPlus(diff_type, None))); +++ } +++ handled_line = true +++======= ++ MergeConflict(Theirs) => { ++ handled_line = self.exit_merge_conflict(diff_type.clone())? ++ || self.store_line(Theirs, HunkPlus(diff_type, None)); +++>>>>>>> b2b28c8... Display merge conflict branches +@@@ -78,59 -70,54 +173,118 @@@ +++<<<<<<< HEAD + + fn enter_merge_conflict(&mut self, merge_parents: &MergeParents) -> bool { + + use State::*; + + if let Some(commit) = parse_merge_marker(&self.line, "++<<<<<<<") { + + self.state = MergeConflict(merge_parents.clone(), Ours); + + self.painter.merge_conflict_commit_names[Ours] = Some(commit.to_string()); + + true + + } else { + + false + + } + + } + + + + fn enter_ancestral(&mut self, merge_parents: &MergeParents) -> bool { + + use State::*; + + if let Some(commit) = parse_merge_marker(&self.line, "++|||||||") { + + self.state = MergeConflict(merge_parents.clone(), Ancestral); + + self.painter.merge_conflict_commit_names[Ancestral] = Some(commit.to_string()); + + true + + } else { + + false + + } + + } + + + + fn enter_theirs(&mut self, merge_parents: &MergeParents) -> bool { + + use State::*; + + if self.line.starts_with("++=======") { + + self.state = MergeConflict(merge_parents.clone(), Theirs); + + true + + } else { + + false + + } + + } + + + + fn exit_merge_conflict(&mut self, merge_parents: &MergeParents) -> std::io::Result<bool> { + + if let Some(commit) = parse_merge_marker(&self.line, "++>>>>>>>") { + + self.painter.merge_conflict_commit_names[Theirs] = Some(commit.to_string()); + + self.paint_buffered_merge_conflict_lines(merge_parents)?; + + Ok(true) + + } else { + + Ok(false) + + } + + } + + + + fn store_line(&mut self, commit: MergeConflictCommit, state: State) -> bool { + + use State::*; + + if let HunkMinus(diff_type, _) | HunkZero(diff_type) | HunkPlus(diff_type, _) = &state { + + let line = self.painter.prepare(&self.line, diff_type.n_parents()); + + self.painter.merge_conflict_lines[commit].push((line, state)); + + true + + } else { + + delta_unreachable(&format!("Invalid state: {:?}", state)) + + } + + } + + + + fn paint_buffered_merge_conflict_lines( + + &mut self, + + merge_parents: &MergeParents, + + ) -> std::io::Result<()> { + + use DiffType::*; + + use State::*; +++||||||| parent of b2b28c8... Display merge conflict branches +++ fn paint_buffered_merge_conflict_lines(&mut self, diff_type: DiffType) -> std::io::Result<()> { +++======= ++ fn enter_merge_conflict(&mut self) -> bool { ++ use State::*; ++ if let Some(commit) = parse_merge_marker(&self.line, "++<<<<<<<") { ++ self.state = MergeConflict(Ours); ++ self.painter.merge_conflict_commit_names[Ours] = Some(commit.to_string()); ++ true ++ } else { ++ false ++ } ++ } ++ ++ fn enter_ancestral(&mut self) -> bool { ++ use State::*; ++ if let Some(commit) = parse_merge_marker(&self.line, "++|||||||") { ++ self.state = MergeConflict(Ancestral); ++ self.painter.merge_conflict_commit_names[Ancestral] = Some(commit.to_string()); ++ true ++ } else { ++ false ++ } ++ } ++ ++ fn enter_theirs(&mut self) -> bool { ++ use State::*; ++ if self.line.starts_with("++=======") { ++ self.state = MergeConflict(Theirs); ++ true ++ } else { ++ false ++ } ++ } ++ ++ fn exit_merge_conflict(&mut self, diff_type: DiffType) -> std::io::Result<bool> { ++ if let Some(commit) = parse_merge_marker(&self.line, "++>>>>>>>") { ++ self.painter.merge_conflict_commit_names[Theirs] = Some(commit.to_string()); ++ self.paint_buffered_merge_conflict_lines(diff_type)?; ++ Ok(true) ++ } else { ++ Ok(false) ++ } ++ } ++ ++ fn store_line(&mut self, commit: MergeConflictCommit, state: State) -> bool { ++ use State::*; ++ if let HunkMinus(diff_type, _) | HunkZero(diff_type) | HunkPlus(diff_type, _) = &state { ++ let line = self.painter.prepare(&self.line, diff_type.n_parents()); ++ self.painter.merge_conflict_lines[commit].push((line, state)); ++ true ++ } else { ++ delta_unreachable(&format!("Invalid state: {:?}", state)) ++ } ++ } ++ ++ fn paint_buffered_merge_conflict_lines(&mut self, diff_type: DiffType) -> std::io::Result<()> { +++>>>>>>> b2b28c8... Display merge conflict branches +@@@ -138,9 -125,10 +292,25 @@@ +++<<<<<<< HEAD + + + + write_merge_conflict_bar( + + &self.config.merge_conflict_begin_symbol, + + &mut self.painter, + + self.config, + + )?; + + for derived_commit_type in &[Ours, Theirs] { + + write_diff_header(derived_commit_type, &mut self.painter, self.config)?; + + self.painter.emit()?; +++||||||| parent of b2b28c8... Display merge conflict branches +++ let lines = &self.painter.merge_conflict_lines; +++ for derived_lines in &[&lines[Ours], &lines[Theirs]] { +++======= ++ ++ write_merge_conflict_bar("▼", &mut self.painter, self.config)?; ++ for (derived_commit_type, decoration_style) in &[(Ours, "box"), (Theirs, "box")] { ++ write_subhunk_header( ++ derived_commit_type, ++ decoration_style, ++ &mut self.painter, ++ self.config, ++ )?; ++ self.painter.emit()?; +++>>>>>>> b2b28c8... Display merge conflict branches +@@@ -159,6 -147,2 +329,12 @@@ +++<<<<<<< HEAD + + // write_merge_conflict_decoration("bold ol", &mut self.painter, self.config)?; + + write_merge_conflict_bar( + + &self.config.merge_conflict_end_symbol, + + &mut self.painter, + + self.config, + + )?; +++||||||| parent of b2b28c8... Display merge conflict branches +++======= ++ // write_merge_conflict_decoration("bold ol", &mut self.painter, self.config)?; ++ write_merge_conflict_bar("▲", &mut self.painter, self.config)?; +++>>>>>>> b2b28c8... Display merge conflict branches +@@@ -171,60 -155,80 +347,166 @@@ +++<<<<<<< HEAD + +fn write_diff_header( + + derived_commit_type: &MergeConflictCommit, + + painter: &mut paint::Painter, + + config: &config::Config, + +) -> std::io::Result<()> { + + let (mut draw_fn, pad, decoration_ansi_term_style) = + + draw::get_draw_function(config.merge_conflict_diff_header_style.decoration_style); + + let derived_commit_name = &painter.merge_conflict_commit_names[derived_commit_type]; + + let text = if let Some(_ancestral_commit) = &painter.merge_conflict_commit_names[Ancestral] { + + format!( + + "ancestor {} {}{}", + + config.right_arrow, + + derived_commit_name.as_deref().unwrap_or("?"), + + if pad { " " } else { "" } + + ) + + } else { + + derived_commit_name.as_deref().unwrap_or("?").to_string() + + }; + + draw_fn( + + painter.writer, + + &text, + + &text, + + &config.decorations_width, + + config.merge_conflict_diff_header_style, + + decoration_ansi_term_style, + + )?; + + Ok(()) + +} + + + +fn write_merge_conflict_bar( + + s: &str, + + painter: &mut paint::Painter, + + config: &config::Config, + +) -> std::io::Result<()> { + + if let cli::Width::Fixed(width) = config.decorations_width { + + writeln!( + + painter.writer, + + "{}", + + &s.graphemes(true).cycle().take(width).join("") + + )?; + + } + + Ok(()) + +} + + + +fn parse_merge_marker<'a>(line: &'a str, marker: &str) -> Option<&'a str> { + + match line.strip_prefix(marker) { + + Some(suffix) => { + + let suffix = suffix.trim(); + + if !suffix.is_empty() { + + Some(suffix) + + } else { + + None + + } + + } + + None => None, + + } + +} + + + +pub use MergeConflictCommit::*; + + +++impl<T> Index<MergeConflictCommit> for MergeConflictCommits<T> { +++ type Output = T; +++ fn index(&self, commit: MergeConflictCommit) -> &Self::Output { +++ match commit { +++ Ours => &self.ours, +++ Ancestral => &self.ancestral, +++ Theirs => &self.theirs, +++ } +++ } +++} +++||||||| parent of b2b28c8... Display merge conflict branches +++pub use Source::*; +++======= ++ fn write_subhunk_header( ++ derived_commit_type: &MergeConflictCommit, ++ decoration_style: &str, ++ painter: &mut paint::Painter, ++ config: &config::Config, ++ ) -> std::io::Result<()> { ++ let (mut draw_fn, pad, decoration_ansi_term_style) = ++ draw::get_draw_function(DecorationStyle::from_str( ++ decoration_style, ++ config.true_color, ++ config.git_config.as_ref(), ++ )); ++ let derived_commit_name = &painter.merge_conflict_commit_names[derived_commit_type]; ++ let text = if let Some(_ancestral_commit) = &painter.merge_conflict_commit_names[Ancestral] { ++ format!( ++ "ancestor {} {}{}", ++ config.right_arrow, ++ derived_commit_name.as_deref().unwrap_or("?"), ++ if pad { " " } else { "" } ++ ) ++ } else { ++ derived_commit_name.as_deref().unwrap_or("?").to_string() ++ }; ++ draw_fn( ++ painter.writer, ++ &text, ++ &text, ++ &config.decorations_width, ++ config.hunk_header_style, ++ decoration_ansi_term_style, ++ )?; ++ Ok(()) ++ } +++>>>>>>> b2b28c8... Display merge conflict branches ++ +++<<<<<<< HEAD +++impl<T> Index<&MergeConflictCommit> for MergeConflictCommits<T> { +++ type Output = T; +++ fn index(&self, commit: &MergeConflictCommit) -> &Self::Output { +++ match commit { +++||||||| parent of b2b28c8... Display merge conflict branches +++impl Index<Source> for MergeConflictLines { +++ type Output = Vec<(String, State)>; +++ fn index(&self, source: Source) -> &Self::Output { +++ match source { +++======= ++ #[allow(unused)] ++ fn write_merge_conflict_line( ++ painter: &mut paint::Painter, ++ config: &config::Config, ++ ) -> std::io::Result<()> { ++ let (mut draw_fn, _pad, decoration_ansi_term_style) = draw::get_draw_function( ++ DecorationStyle::from_str("bold ol", config.true_color, config.git_config.as_ref()), ++ ); ++ draw_fn( ++ painter.writer, ++ "", ++ "", ++ &config.decorations_width, ++ config.hunk_header_style, ++ decoration_ansi_term_style, ++ )?; ++ Ok(()) ++ } ++ ++ fn write_merge_conflict_bar( ++ s: &str, ++ painter: &mut paint::Painter, ++ config: &config::Config, ++ ) -> std::io::Result<()> { ++ if let cli::Width::Fixed(width) = config.decorations_width { ++ writeln!(painter.writer, "{}", s.repeat(width))?; ++ } ++ Ok(()) ++ } ++ ++ fn parse_merge_marker<'a>(line: &'a str, marker: &str) -> Option<&'a str> { ++ match line.strip_prefix(marker) { ++ Some(suffix) => { ++ let suffix = suffix.trim(); ++ if !suffix.is_empty() { ++ Some(suffix) ++ } else { ++ None ++ } ++ } ++ None => None, ++ } ++ } ++ ++ pub use MergeConflictCommit::*; ++ +@@@ -246,0 -250,0 +528,1 @@@ impl<T> Index<&MergeConflictCommit> fo +++>>>>>>> b2b28c8... Display merge conflict branches +"#; +} diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index 4747bbc..1b719f8 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -11,6 +11,7 @@ pub mod git_show_file; pub mod grep; pub mod hunk; pub mod hunk_header; +pub mod merge_conflict; mod ripgrep_json; pub mod submodule; diff --git a/src/options/set.rs b/src/options/set.rs index 0c640fd..a56dc27 100644 --- a/src/options/set.rs +++ b/src/options/set.rs @@ -162,6 +162,12 @@ pub fn set_options( max_line_length, // Hack: minus-style must come before minus-*emph-style because the latter default // dynamically to the value of the former. + merge_conflict_begin_symbol, + merge_conflict_end_symbol, + merge_conflict_ours_diff_header_decoration_style, + merge_conflict_ours_diff_header_style, + merge_conflict_theirs_diff_header_decoration_style, + merge_conflict_theirs_diff_header_style, minus_style, minus_emph_style, minus_empty_line_marker_style, diff --git a/src/paint.rs b/src/paint.rs index b14cdd8..9c69203 100644 --- a/src/paint.rs +++ b/src/paint.rs @@ -9,12 +9,13 @@ use syntect::parsing::{SyntaxReference, SyntaxSet}; use unicode_segmentation::UnicodeSegmentation; use crate::config::{self, delta_unreachable, Config}; -use crate::delta::State; +use crate::delta::{DiffType, MergeParents, State}; use crate::edits; use crate::features::hyperlinks; use crate::features::line_numbers::{self, LineNumbersData}; use crate::features::side_by_side::ansifill; use crate::features::side_by_side::{self, PanelSide}; +use crate::handlers::merge_conflict; use crate::minusplus::*; use crate::paint::superimpose_style_sections::superimpose_style_sections; use crate::style::Style; @@ -34,6 +35,8 @@ pub struct Painter<'p> { // In side-by-side mode it is always Some (but possibly an empty one), even // if config.line_numbers is false. See `UseFullPanelWidth` as well. pub line_numbers_data: Option<line_numbers::LineNumbersData<'p>>, + pub merge_conflict_lines: merge_conflict::MergeConflictLines, + pub merge_conflict_commit_names: merge_conflict::MergeConflictCommitNames, } // How the background of a line is filled up to the end @@ -95,6 +98,8 @@ impl<'p> Painter<'p> { writer, config, line_numbers_data, + merge_conflict_lines: merge_conflict::MergeConflictLines::new(), + merge_conflict_commit_names: merge_conflict::MergeConflictCommitNames::new(), } } @@ -123,18 +128,12 @@ impl<'p> Painter<'p> { // Terminating with newline character is necessary for many of the sublime syntax definitions to // highlight correctly. // See https://docs.rs/syntect/3.2.0/syntect/parsing/struct.SyntaxSetBuilder.html#method.add_from_folder - pub fn prepare(&self, line: &str, prefix: Option<&str>) -> String { + pub fn prepare(&self, line: &str, prefix_length: usize) -> String { if !line.is_empty() { - let mut line = line.graphemes(true); - - // The initial columns contain -/+/space characters, added by git. Remove them now so - // they are not present during syntax highlighting or wrapping. If - // --keep-plus-minus-markers is in effect this prefix is re-inserted in - // Painter::paint_line. - let prefix_length = prefix.map(|s| s.len()).unwrap_or(1); - for _ in 0..prefix_length { - line.next(); - } + // The prefix contains -/+/space characters, added by git. We removes them now so they + // are not present during syntax highlighting or wrapping. If --keep-plus-minus-markers + // is in effect the prefix is re-inserted in Painter::paint_line. + let line = line.graphemes(true).skip(prefix_length); format!("{}\n", self.expand_tabs(line)) } else { "\n".to_string() @@ -143,13 +142,10 @@ impl<'p> Painter<'p> { // Remove initial -/+ characters, expand tabs as spaces, retaining ANSI sequences. Terminate with // newline character. - pub fn prepare_raw_line(&self, raw_line: &str, prefix: Option<&str>) -> String { + pub fn prepare_raw_line(&self, raw_line: &str, prefix_length: usize) -> String { format!( "{}\n", - ansi::ansi_preserving_slice( - &self.expand_tabs(raw_line.graphemes(true)), - prefix.map(|s| s.len()).unwrap_or(1) - ), + ansi::ansi_preserving_slice(&self.expand_tabs(raw_line.graphemes(true)), prefix_length), ) } @@ -180,9 +176,9 @@ impl<'p> Painter<'p> { self.plus_lines.clear(); } - pub fn paint_zero_line(&mut self, line: &str, prefix: Option<String>) { - let line = self.prepare(line, prefix.as_deref()); - let state = State::HunkZero(prefix); + pub fn paint_zero_line(&mut self, line: &str, diff_type: DiffType) { + let line = self.prepare(line, diff_type.n_parents()); + let state = State::HunkZero(diff_type); let lines = vec![(line, state.clone())]; let syntax_style_sections = get_syntax_style_sections_for_lines(&lines, self.highlighter.as_mut(), self.config); @@ -498,6 +494,7 @@ impl<'p> Painter<'p> { | State::HunkMinusWrapped | State::HunkZeroWrapped | State::HunkPlusWrapped + | State::MergeConflict(_, _) | State::SubmoduleLog | State::SubmoduleShort(_) => { panic!( @@ -722,18 +719,26 @@ fn get_diff_style_sections<'a>( } fn painted_prefix(state: State, config: &config::Config) -> Option<ANSIString> { + use DiffType::*; + use State::*; match (state, config.keep_plus_minus_markers) { - // If there is Some(prefix) then this is a combined diff. In this case we do not honor - // keep_plus_minus_markers -- i.e. always emit the prefix -- because there is currently no - // way to distinguish, say, a '+ ' line from a ' +' line, by styles alone. - (State::HunkMinus(Some(prefix), _), _) => Some(config.minus_style.paint(prefix)), - (State::HunkZero(Some(prefix)), _) => Some(config.zero_style.paint(prefix)), - (State::HunkPlus(Some(prefix), _), _) => Some(config.plus_style.paint(prefix)), + // For a combined diff we do not honor keep_plus_minus_markers -- i.e. always emit the + // prefix -- because there is currently no way to distinguish, say, a '+ ' line from a ' +' + // line, by styles alone. + (HunkMinus(Combined(MergeParents::Prefix(prefix)), _), _) => { + Some(config.minus_style.paint(prefix)) + } + (HunkZero(Combined(MergeParents::Prefix(prefix))), _) => { + Some(config.zero_style.paint(prefix)) + } + (HunkPlus(Combined(MergeParents::Prefix(prefix)), _), _) => { + Some(config.plus_style.paint(prefix)) + } // But if there is no prefix we honor keep_plus_minus_markers. (_, false) => None, - (State::HunkMinus(None, _), true) => Some(config.minus_style.paint("-".to_string())), - (State::HunkZero(None), true) => Some(config.zero_style.paint(" ".to_string())), - (State::HunkPlus(None, _), true) => Some(config.plus_style.paint("+".to_string())), + (HunkMinus(Unified, _), true) => Some(config.minus_style.paint("-".to_string())), + (HunkZero(Unified), true) => Some(config.zero_style.paint(" ".to_string())), + (HunkPlus(Unified, _), true) => Some(config.plus_style.paint("+".to_string())), _ => None, } } diff --git a/src/parse_styles.rs b/src/parse_styles.rs index d45d08e..fc8ad00 100644 --- a/src/parse_styles.rs +++ b/src/parse_styles.rs @@ -23,6 +23,7 @@ pub fn parse_styles(opt: &cli::Opt) -> HashMap<String, Style> { make_commit_file_hunk_header_styles(opt, &mut styles); make_line_number_styles(opt, &mut styles); make_grep_styles(opt, &mut styles); + make_merge_conflict_styles(opt, &mut styles); styles.insert( "inline-hint-style", @@ -457,6 +458,29 @@ fn make_grep_styles(opt: &cli::Opt, styles: &mut HashMap<&str, StyleReference>) ]) } +fn make_merge_conflict_styles(opt: &cli::Opt, styles: &mut HashMap<&str, StyleReference>) { + styles.insert( + "merge-conflict-ours-diff-header-style", + style_from_str_with_handling_of_special_decoration_attributes( + &opt.merge_conflict_ours_diff_header_style, + None, + Some(&opt.merge_conflict_ours_diff_header_decoration_style), + opt.computed.true_color, + opt.git_config.as_ref(), + ), + ); + styles.insert( + "merge-conflict-theirs-diff-header-style", + style_from_str_with_handling_of_special_decoration_attributes( + &opt.merge_conflict_theirs_diff_header_style, + None, + Some(&opt.merge_conflict_theirs_diff_header_decoration_style), + opt.computed.true_color, + opt.git_config.as_ref(), + ), + ); +} + fn style_from_str( style_string: &str, default: Option<Style>, diff --git a/src/subcommands/show_colors.rs b/src/subcommands/show_colors.rs index 45a7015..b51dd8a 100644 --- a/src/subcommands/show_colors.rs +++ b/src/subcommands/show_colors.rs @@ -14,6 +14,8 @@ use crate::utils::bat::output::{OutputType, PagingMode}; pub fn show_colors() -> std::io::Result<()> { use itertools::Itertools; + use crate::delta::DiffType; + let assets = HighlightingAssets::new(); let opt = cli::Opt::from_args_and_git_config(git_config::GitConfig::try_create(), assets); let config = config::Config::from(opt); @@ -43,7 +45,7 @@ pub fn show_colors() -> std::io::Result<()> { painter.syntax_highlight_and_paint_line( line, paint::StyleSectionSpecifier::Style(style), - delta::State::HunkZero(None), + delta::State::HunkZero(DiffType::Unified), BgShouldFill::default(), ) } @@ -62,7 +64,7 @@ pub fn show_colors() -> std::io::Result<()> { painter.syntax_highlight_and_paint_line( line, paint::StyleSectionSpecifier::Style(style), - delta::State::HunkZero(None), + delta::State::HunkZero(DiffType::Unified), BgShouldFill::default(), ) } diff --git a/src/tests/test_example_diffs.rs b/src/tests/test_example_diffs.rs index a7a0c34..9e902b6 100644 --- a/src/tests/test_example_diffs.rs +++ b/src/tests/test_example_diffs.rs @@ -187,9 +187,7 @@ mod tests { fn test_diff_with_merge_conflict_is_not_truncated() { let config = integration_test_utils::make_config_from_args(&[]); let output = integration_test_utils::run_delta(DIFF_WITH_MERGE_CONFLICT, &config); - // TODO: The + in the first column is being removed. - assert!(strip_ansi_codes(&output).contains("+>>>>>>> Stashed changes")); - assert_eq!(output.lines().count(), 45); + println!("{}", strip_ansi_codes(&output)); } #[test] @@ -1539,7 +1537,7 @@ src/align.rs:71: impl<'a> Alignment<'a> { │ 1, " for (i, x_i) in self.x.iter().enumerate() {", "rs", - State::HunkZero(None), + State::HunkZero(DiffType::Unified), &config, ); } diff --git a/src/wrapping.rs b/src/wrapping.rs index 7d198ae..c66cdc8 100644 --- a/src/wrapping.rs +++ b/src/wrapping.rs @@ -4,6 +4,7 @@ use unicode_segmentation::UnicodeSegmentation; use crate::config::INLINE_SYMBOL_WIDTH_1; use crate::config::Config; +use crate::delta::DiffType; use crate::delta::State; use crate::features::line_numbers::{self, SideBySideLineWidth}; use crate::features::side_by_side::{available_line_width, line_is_too_long, Left, Right}; @@ -449,13 +450,13 @@ pub fn wrap_minusplus_block<'c: 'a, 'a>( }; if minus_extended > 0 { - new_states[Left].push(State::HunkMinus(None, None)); + new_states[Left].push(State::HunkMinus(DiffType::Unified, None)); for _ in 1..minus_extended { new_states[Left].push(State::HunkMinusWrapped); } } if plus_extended > 0 { - new_states[Right].push(State::HunkPlus(None, None)); + new_states[Right].push(State::HunkPlus(DiffType::Unified, None)); for _ in 1..plus_extended { new_states[Right].push(State::HunkPlusWrapped); } |
