Skip to content

Commit

Permalink
Display merge conflict branches
Browse files Browse the repository at this point in the history
- Clean up state machine transitions by factoring out dedicated methods
  • Loading branch information
dandavison committed Dec 4, 2021
1 parent 6c0fc7e commit b2b28c8
Show file tree
Hide file tree
Showing 5 changed files with 214 additions and 59 deletions.
2 changes: 1 addition & 1 deletion src/delta.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ 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)
MergeConflict(merge_conflict::Source),
MergeConflict(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).
Expand Down
2 changes: 1 addition & 1 deletion src/handlers/draw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
7 changes: 6 additions & 1 deletion src/handlers/hunk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ 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::*;
Expand All @@ -140,7 +142,10 @@ fn new_line_state(new_line: &str, prev_state: &State) -> Option<State> {
| HunkPlus(Unified, _)
| HunkHeader(Unified, _, _) => Unified,
HunkHeader(Combined(Number(n)), _, _) => Combined(Number(*n)),
HunkMinus(Combined(Prefix(prefix)), _)
// 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)),
Expand Down
254 changes: 201 additions & 53 deletions src/handlers/merge_conflict.rs
Original file line number Diff line number Diff line change
@@ -1,28 +1,36 @@
use std::cmp::min;
use std::ops::{Index, IndexMut};

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::DecorationStyle;

#[derive(Clone, Debug, PartialEq)]
pub enum Source {
pub enum MergeConflictCommit {
Ours,
Ancestral,
Theirs,
}

pub struct MergeConflictLines {
ours: Vec<(String, State)>,
ancestral: Vec<(String, State)>,
theirs: Vec<(String, State)>,
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 MergeParents::*;
use Source::*;
use State::*;

let mut handled_line = false;
Expand All @@ -37,88 +45,218 @@ impl<'a> StateMachine<'a> {
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
}
}
HunkZero(_) => handled_line = self.enter_merge_conflict(),
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
handled_line = self.enter_ancestral()
|| self.enter_theirs()
|| self.exit_merge_conflict(diff_type.clone())?
|| self.store_line(Ours, HunkPlus(diff_type, None));
}
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
handled_line = self.enter_theirs()
|| self.exit_merge_conflict(diff_type.clone())?
|| self.store_line(Ancestral, HunkMinus(diff_type, None));
}
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
handled_line = self.exit_merge_conflict(diff_type.clone())?
|| self.store_line(Theirs, HunkPlus(diff_type, None));
}
_ => {}
}

Ok(handled_line)
}

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<()> {
self.painter.emit()?;
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()?;
paint::paint_minus_and_plus_lines(
MinusPlus::new(&lines[Ancestral], derived_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.output_buffer.push_str("\n\n");
self.painter.emit()?;
}
// write_merge_conflict_decoration("bold ol", &mut self.painter, self.config)?;
write_merge_conflict_bar("▲", &mut self.painter, self.config)?;
self.painter.merge_conflict_lines.clear();
self.state = State::HunkZero(diff_type);
Ok(())
}
}

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(())
}

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 {
Ours => &self.ours,
Ancestral => &self.ancestral,
Theirs => &self.theirs,
}
}
}

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 {
Ours => &mut self.ours,
Ancestral => &mut self.ancestral,
Theirs => &mut self.theirs,
Expand All @@ -141,3 +279,13 @@ impl MergeConflictLines {
self[Theirs].clear();
}
}

impl MergeConflictCommitNames {
pub fn new() -> Self {
Self {
ours: None,
ancestral: None,
theirs: None,
}
}
}
8 changes: 5 additions & 3 deletions src/paint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ 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::MergeConflictLines;
use crate::handlers::merge_conflict;
use crate::minusplus::*;
use crate::paint::superimpose_style_sections::superimpose_style_sections;
use crate::style::Style;
Expand All @@ -35,7 +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: MergeConflictLines,
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
Expand Down Expand Up @@ -97,7 +98,8 @@ impl<'p> Painter<'p> {
writer,
config,
line_numbers_data,
merge_conflict_lines: MergeConflictLines::new(),
merge_conflict_lines: merge_conflict::MergeConflictLines::new(),
merge_conflict_commit_names: merge_conflict::MergeConflictCommitNames::new(),
}
}

Expand Down

0 comments on commit b2b28c8

Please sign in to comment.