summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDan Davison <dandavison7@gmail.com>2021-11-29 20:54:48 -0500
committerDan Davison <dandavison7@gmail.com>2021-12-05 11:25:05 -0500
commit6745f42ddadeccfa30628c70d39b8f9abbff35f0 (patch)
tree19cdd504eba5632d6bb0d6a72559b1ee1774c057
parente7294060ef3b8af0d2307eea4359123232f85646 (diff)
Display merge conflicts
-rw-r--r--README.md3
-rw-r--r--etc/examples/822-hunk-header-within-merge-conflict.diff545
-rw-r--r--src/cli.rs44
-rw-r--r--src/config.rs11
-rw-r--r--src/delta.rs43
-rw-r--r--src/features/side_by_side.rs15
-rw-r--r--src/handlers/diff_header_diff.rs5
-rw-r--r--src/handlers/draw.rs2
-rw-r--r--src/handlers/hunk.rs81
-rw-r--r--src/handlers/hunk_header.rs27
-rw-r--r--src/handlers/merge_conflict.rs1340
-rw-r--r--src/handlers/mod.rs1
-rw-r--r--src/options/set.rs6
-rw-r--r--src/paint.rs63
-rw-r--r--src/parse_styles.rs24
-rw-r--r--src/subcommands/show_colors.rs6
-rw-r--r--src/tests/test_example_diffs.rs6
-rw-r--r--src/wrapping.rs5
18 files changed, 2134 insertions, 93 deletions
diff --git a/README.md b/README.md
index d7f3c8b..6d42004 100644
--- a/README.md
+++ b/README.md
@@ -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,
diff --git a/src/cli.rs b/src/cli.rs
index ee0f827..19b200f 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -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);
}