shrink-conflicts 0.1.0

Tries to minimise diff3-style git conflicts
Documentation
use std::fmt::Display;

#[derive(Debug, Clone, Copy, PartialEq)]
enum State {
    Context,
    Left,
    Common,
    Right,
}

pub fn run(file: String) -> anyhow::Result<String> {
    use std::fmt::Write;
    let mut output = String::new();
    let mut state = State::Context;
    let mut conflict = Conflict::default();
    for l in file.lines() {
        if l.len() >= 7 {
            match &l[..7] {
                "<<<<<<<" => {
                    if state == State::Context {
                        state = State::Left;
                        continue;
                    }
                }
                "|||||||" => {
                    if state == State::Left {
                        state = State::Common;
                        continue;
                    }
                }
                "=======" => {
                    if state == State::Common {
                        state = State::Right;
                        continue;
                    }
                }
                "+++++++" => match l.split_once(' ') {
                    Some((_, "Contents of side #1")) => {
                        if state == State::Left {
                            state = State::Left;
                            continue;
                        }
                    }
                    Some((_, "Contents of side #2")) => {
                        if state == State::Common {
                            state = State::Right;
                            continue;
                        }
                    }
                    _ => (),
                },
                "-------" => match l.split_once(' ') {
                    Some((_, "Contents of base")) => {
                        if state == State::Left {
                            state = State::Common;
                            continue;
                        }
                    }
                    _ => (),
                },
                ">>>>>>>" => {
                    if state == State::Right {
                        state = State::Context;

                        conflict.minimise();
                        write!(output, "{conflict}")?;
                        conflict.clear();

                        continue;
                    }
                }
                "%%%%%%%" => {
                    eprintln!(
                        "WARN: jj's \"diff\"-style conflict markers are not \
                        supported. Set conflict-marker-style=\"snapshot\"."
                    );
                }
                _ => (),
            }
        }
        match state {
            State::Left => conflict.left.push(l.to_string()),
            State::Common => conflict.common.push(l.to_string()),
            State::Right => conflict.right.push(l.to_string()),
            State::Context => writeln!(output, "{l}")?,
        }
    }
    match state {
        State::Context => (),
        State::Left => {
            writeln!(output, "<<<<<<<")?;
            for l in conflict.left {
                writeln!(output, "{l}")?;
            }
        }
        State::Common => {
            writeln!(output, "<<<<<<<")?;
            for l in conflict.left {
                writeln!(output, "{l}")?;
            }
            writeln!(output, "|||||||")?;
            for l in conflict.common {
                writeln!(output, "{l}")?;
            }
        }
        State::Right => {
            writeln!(output, "<<<<<<<")?;
            for l in conflict.left {
                writeln!(output, "{l}")?;
            }
            writeln!(output, "|||||||")?;
            for l in conflict.common {
                writeln!(output, "{l}")?;
            }
            writeln!(output, "=======")?;
            for l in conflict.right {
                writeln!(output, "{l}")?;
            }
        }
    }
    if let Some(c) = file.as_bytes().last() {
        if *c != b'\n' {
            output.remove(output.len() - 1);
        }
    }
    Ok(output)
}

#[derive(Default, Debug, Clone)]
struct Conflict {
    pre: Vec<String>,
    left: Vec<String>,
    common: Vec<String>,
    right: Vec<String>,
    post: Vec<String>,
}

impl Display for Conflict {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        for l in &self.pre {
            writeln!(f, "{l}")?;
        }
        if !self.is_resolved() {
            writeln!(f, "<<<<<<<")?;
            for l in &self.left {
                writeln!(f, "{l}")?;
            }
            writeln!(f, "|||||||")?;
            for l in &self.common {
                writeln!(f, "{l}")?;
            }
            writeln!(f, "=======")?;
            for l in &self.right {
                writeln!(f, "{l}")?;
            }
            writeln!(f, ">>>>>>>")?;
        }
        for l in &self.post {
            writeln!(f, "{l}")?;
        }
        Ok(())
    }
}

impl Conflict {
    fn minimise(&mut self) {
        let mut prefix = 0;
        for ((l, c), r) in self
            .left
            .iter()
            .zip(self.common.iter())
            .zip(self.right.iter())
        {
            if l == c && c == r {
                prefix += 1;
            } else {
                break;
            }
        }
        self.pre.extend(self.left.drain(..prefix));
        self.common.drain(..prefix);
        self.right.drain(..prefix);

        let mut suffix = 0;
        for ((l, c), r) in self
            .left
            .iter()
            .rev()
            .zip(self.common.iter().rev())
            .zip(self.right.iter().rev())
        {
            if l == c && c == r {
                suffix += 1;
            } else {
                break;
            }
        }
        self.post
            .extend(self.left.drain(self.left.len() - suffix..));
        self.common.drain(self.common.len() - suffix..);
        self.right.drain(self.right.len() - suffix..);

        if self.left == self.common {
            self.pre.extend(self.right.drain(..));
            self.left.clear();
            self.common.clear();
        }
        if self.right == self.common {
            self.pre.extend(self.left.drain(..));
            self.right.clear();
            self.common.clear();
        }
    }

    fn is_resolved(&self) -> bool {
        self.left.is_empty() && self.common.is_empty() && self.right.is_empty()
    }

    fn clear(&mut self) {
        self.pre.clear();
        self.left.clear();
        self.common.clear();
        self.right.clear();
        self.post.clear();
    }
}