mod conflict;
mod exist;
mod flat;
mod kind;
mod labels;
mod list;
pub mod order;
mod place;
mod record;
mod same;
mod tree;
use std::collections::BTreeMap;
use std::fmt;
use crate::codec::schema::{Kind, field};
use crate::codec::{
Content, Mode, Problem, ReadError, Rec, Role, Site, SpecPath, Val, keyed_records, read_file,
write_content,
};
use crate::identity::{Key, Label};
use crate::yaml::{Location, Position};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Input {
Base,
Ours,
Theirs,
}
impl fmt::Display for Input {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::Base => "base",
Self::Ours => "ours",
Self::Theirs => "theirs",
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Merged {
pub bytes: Vec<u8>,
pub conflicts: usize,
pub disputed: Vec<Label>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MergeError {
NotAFormatFile,
Unreadable(Vec<(Input, ReadError)>),
BothChanged,
Unwritable(ReadError),
Unplaced(Key),
}
pub fn merge_file(
path: &SpecPath,
base: &[u8],
ours: &[u8],
theirs: &[u8],
) -> Result<Merged, MergeError> {
let role = Role::of(path).ok_or(MergeError::NotAFormatFile)?;
let (base, ours, theirs) = read_inputs(path, &role, [base, ours, theirs])?;
let (content, disputed) = merge_content(base, ours, theirs)?;
let bytes = write_content(path, role, content);
let (_, written) = read_file(path, &bytes, Mode::Strict).map_err(MergeError::Unwritable)?;
if let Some(error) = twice_in(path, &written).into_iter().next() {
return Err(MergeError::Unwritable(error));
}
Ok(Merged {
conflicts: written.records().map(count).sum(),
bytes,
disputed,
})
}
fn read_inputs(
path: &SpecPath,
role: &Role,
inputs: [&[u8]; 3],
) -> Result<(Option<Content>, Content, Content), MergeError> {
let mut errors = Vec::new();
let mut read = |input: Input, bytes: &[u8]| {
if input == Input::Base && bytes.is_empty() && *role != Role::Lint {
return None;
}
let read = read_file(path, bytes, Mode::Strict);
match read.map(|(_, content)| (twice_in(path, &content), content)) {
Ok((twice, content)) if twice.is_empty() => Some(content),
Ok((twice, _)) => {
errors.extend(twice.into_iter().map(|error| (input, error)));
None
}
Err(error) => {
errors.push((input, error));
None
}
}
};
let [base, ours, theirs] = inputs;
let base = read(Input::Base, base);
let ours = read(Input::Ours, ours);
let theirs = read(Input::Theirs, theirs);
match (ours, theirs, errors.is_empty()) {
(Some(ours), Some(theirs), true) => Ok((base, ours, theirs)),
_ => Err(MergeError::Unreadable(errors)),
}
}
fn merge_content(
base: Option<Content>,
ours: Content,
theirs: Content,
) -> Result<(Content, Vec<Label>), MergeError> {
match (base, ours, theirs) {
(base, Content::Record(ours), Content::Record(theirs)) if ours.kind == Kind::Labels => {
let base = match base {
Some(Content::Record(base)) => Some(base),
_ => None,
};
let (record, disputed) = labels::merge(base.as_ref(), &ours, &theirs);
Ok((Content::Record(record), disputed))
}
(base, ours @ Content::Record(_), theirs @ Content::Record(_))
| (base, ours @ Content::Registry(_), theirs @ Content::Registry(_)) => {
let merged =
tree::merge(base.as_ref(), &ours, &theirs).map_err(MergeError::Unplaced)?;
Ok((merged, Vec::new()))
}
(base, Content::Lint(ours), Content::Lint(theirs)) => {
let base = match base {
Some(Content::Lint(base)) => base,
_ => Vec::new(),
};
Ok((Content::Lint(lint(&base, ours, theirs)?), Vec::new()))
}
_ => Err(MergeError::NotAFormatFile),
}
}
fn lint(base: &[u8], ours: Vec<u8>, theirs: Vec<u8>) -> Result<Vec<u8>, MergeError> {
if ours == theirs || theirs == base {
Ok(ours)
} else if ours == base {
Ok(theirs)
} else {
Err(MergeError::BothChanged)
}
}
fn twice_in(path: &SpecPath, content: &Content) -> Vec<ReadError> {
let site = |position: Position| Site {
path: path.clone(),
location: Some(Location::At(position)),
};
let mut seen = BTreeMap::new();
let mut errors = Vec::new();
for record in keyed_records(content.records()) {
let (Some(key), Some(Val::Scalar { position, .. })) =
(record.key(), record.get(field::KEY))
else {
continue;
};
match seen.get(&key) {
Some(&first) => errors.push(ReadError {
sites: vec![site(first), site(*position)],
problem: Problem::DuplicateKey(key),
}),
None => {
seen.insert(key, *position);
}
}
}
errors
}
fn count(record: &Rec) -> usize {
record.conflicts.len() + record.children().map(count).sum::<usize>()
}