use std::collections::{BTreeMap, BTreeSet};
use std::sync::atomic::{AtomicBool, Ordering};
use super::{ContentMerge, ContentOptions, MarkerOptions, content_with};
use crate::{EntryMode, ObjectId, Objects, Tree, TreeEntry, TreeValue};
#[derive(Clone, Copy, Debug)]
#[non_exhaustive]
pub struct TreeOptions {
pub content: ContentOptions,
pub max_depth: usize,
pub max_entries: usize,
}
impl Default for TreeOptions {
fn default() -> Self {
Self {
content: ContentOptions::default(),
max_depth: 128,
max_entries: 1_000_000,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum ConflictKind {
AddAdd,
ModifyDelete,
Content,
Type,
Mode,
Symlink,
Gitlink,
RenameRename,
RenameDelete,
DirectoryRename,
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct TreeConflict {
pub path: Vec<u8>,
pub kind: ConflictKind,
pub base: Option<TreeValue>,
pub ours: Option<TreeValue>,
pub theirs: Option<TreeValue>,
pub content: Option<ContentMerge>,
pub rename_source: Option<Vec<u8>>,
pub stage_paths: Option<[Vec<u8>; 3]>,
}
impl TreeConflict {
pub fn index_entries(&self) -> Result<Vec<crate::index::Entry>, TreeError> {
use crate::index::{Entry, Mode, Stage};
let mut entries = Vec::new();
for (position, (side, stage)) in [
(self.base, Stage::Base),
(self.ours, Stage::Ours),
(self.theirs, Stage::Theirs),
]
.into_iter()
.enumerate()
{
let Some(side) = side else {
continue;
};
let mode = match side.mode {
EntryMode::Blob => Mode::Regular,
EntryMode::Executable => Mode::Executable,
EntryMode::Symlink => Mode::Symlink,
EntryMode::Gitlink => Mode::Gitlink,
EntryMode::Tree => return Err(TreeError::DirectoryStages),
};
let path = self
.stage_paths
.as_ref()
.map_or(&self.path, |paths| &paths[position]);
entries.push(Entry::new(path.clone(), mode, side.id).with_stage(stage));
}
Ok(entries)
}
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct TreeMerge {
pub tree: Tree,
pub conflicts: Vec<TreeConflict>,
pub blobs: Vec<(ObjectId, Vec<u8>)>,
pub trees: Vec<Tree>,
}
impl TreeMerge {
pub fn is_clean(&self) -> bool {
self.conflicts.is_empty()
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum TreeError {
#[error("reading merge tree")]
ReadTree(#[from] crate::ReadTreeError),
#[error("reading merge blob")]
ReadBlob(#[from] crate::ReadBlobError),
#[error("constructing merge tree")]
Tree(#[from] crate::TreeError),
#[error("merging tree content")]
Content(#[from] super::Error),
#[error("merge traversal limit: {0}")]
Limit(&'static str),
#[error("merge cancelled")]
Cancelled,
#[error("running path-selected merge driver")]
Driver {
path: Vec<u8>,
#[source]
source: Box<dyn std::error::Error + Send + Sync>,
},
#[error("directory conflicts cannot be represented as leaf index stages")]
DirectoryStages,
}
impl Objects {
pub fn merge_tree_entries(
&self,
base: ObjectId,
ours: ObjectId,
theirs: ObjectId,
) -> Result<TreeMerge, TreeError> {
self.merge_tree_entries_with(
base,
ours,
theirs,
TreeOptions::default(),
&AtomicBool::new(false),
)
}
pub fn merge_tree_entries_with(
&self,
base: ObjectId,
ours: ObjectId,
theirs: ObjectId,
options: TreeOptions,
cancel: &AtomicBool,
) -> Result<TreeMerge, TreeError> {
merge_prepared(
self,
[base, ours, theirs],
options,
cancel,
&super::memory::Memory::default(),
)
}
}
pub(super) fn merge_prepared(
objects: &Objects,
roots: [ObjectId; 3],
options: TreeOptions,
cancel: &AtomicBool,
memory: &super::memory::Memory,
) -> Result<TreeMerge, TreeError> {
merge_prepared_with_driver(objects, roots, options, cancel, memory, None)
}
pub(super) fn merge_prepared_with_driver<'a, 'd>(
objects: &'a Objects,
roots: [ObjectId; 3],
options: TreeOptions,
cancel: &'a AtomicBool,
memory: &'a super::memory::Memory,
driver: Option<&'a mut (dyn super::TreeDriver + 'd)>,
) -> Result<TreeMerge, TreeError> {
let mut merger = Merger {
objects,
options,
cancel,
memory,
driver,
remaining: options.max_entries,
conflicts: Vec::new(),
blobs: Vec::new(),
trees: Vec::new(),
};
let [base, ours, theirs] = roots;
let tree = merger.directory(Some(base), Some(ours), Some(theirs), &[], 0)?;
merger.check()?;
Ok(TreeMerge {
tree,
conflicts: merger.conflicts,
blobs: merger.blobs,
trees: merger.trees,
})
}
struct Merger<'a, 'd> {
objects: &'a Objects,
memory: &'a super::memory::Memory,
driver: Option<&'a mut (dyn super::TreeDriver + 'd)>,
options: TreeOptions,
cancel: &'a AtomicBool,
remaining: usize,
conflicts: Vec<TreeConflict>,
blobs: Vec<(ObjectId, Vec<u8>)>,
trees: Vec<Tree>,
}
impl Merger<'_, '_> {
fn check(&self) -> Result<(), TreeError> {
if self.cancel.load(Ordering::Relaxed) {
Err(TreeError::Cancelled)
} else {
Ok(())
}
}
fn read(&mut self, id: Option<ObjectId>) -> Result<BTreeMap<Vec<u8>, TreeValue>, TreeError> {
self.check()?;
let Some(id) = id else {
return Ok(BTreeMap::new());
};
let tree = self.memory.read_tree(self.objects, id)?;
self.remaining = self
.remaining
.checked_sub(tree.entries().len())
.ok_or(TreeError::Limit("entries"))?;
Ok(tree
.entries()
.iter()
.map(|entry| {
(
entry.name.clone(),
TreeValue {
mode: entry.mode,
id: entry.id,
},
)
})
.collect())
}
fn directory(
&mut self,
base: Option<ObjectId>,
ours: Option<ObjectId>,
theirs: Option<ObjectId>,
path: &[u8],
depth: usize,
) -> Result<Tree, TreeError> {
self.check()?;
if depth > self.options.max_depth {
return Err(TreeError::Limit("depth"));
}
let b = self.read(base)?;
let o = self.read(ours)?;
let t = self.read(theirs)?;
let names: BTreeSet<_> = b.keys().chain(o.keys()).chain(t.keys()).collect();
let mut entries = Vec::new();
for name in names {
self.check()?;
let mut child = path.to_vec();
if !child.is_empty() {
child.push(b'/');
}
child.extend_from_slice(name);
if let Some(value) = self.entry(
b.get(name).copied(),
o.get(name).copied(),
t.get(name).copied(),
&child,
depth,
)? {
entries.push(TreeEntry {
name: name.clone(),
mode: value.mode,
id: value.id,
});
}
}
let tree = Tree::new(self.objects.object_format(), entries)?;
self.trees.push(tree.clone());
Ok(tree)
}
fn entry(
&mut self,
base: Option<TreeValue>,
ours: Option<TreeValue>,
theirs: Option<TreeValue>,
path: &[u8],
depth: usize,
) -> Result<Option<TreeValue>, TreeError> {
if ours == theirs || base == theirs {
return Ok(ours);
}
if base == ours {
return Ok(theirs);
}
if directories(base, ours, theirs) {
let tree = self.directory(
base.map(|v| v.id),
ours.map(|v| v.id),
theirs.map(|v| v.id),
path,
depth + 1,
)?;
return Ok(Some(TreeValue {
mode: EntryMode::Tree,
id: tree.id(),
}));
}
let mut conflict = TreeConflict {
path: path.to_vec(),
kind: ConflictKind::Type,
base,
ours,
theirs,
content: None,
rename_source: None,
stage_paths: None,
};
if let (Some(o), Some(t)) = (ours, theirs) {
if regular(o.mode) && regular(t.mode) && base.is_none_or(|b| regular(b.mode)) {
return self.file(conflict, o, t);
}
conflict.kind = if o.mode == EntryMode::Gitlink && t.mode == EntryMode::Gitlink {
ConflictKind::Gitlink
} else if o.mode == EntryMode::Symlink && t.mode == EntryMode::Symlink {
ConflictKind::Symlink
} else {
ConflictKind::Type
};
} else {
conflict.kind = ConflictKind::ModifyDelete;
}
self.conflicts.push(conflict);
Ok(ours.or(theirs))
}
fn file(
&mut self,
mut conflict: TreeConflict,
ours: TreeValue,
theirs: TreeValue,
) -> Result<Option<TreeValue>, TreeError> {
let (mode, mode_conflict) =
if ours.mode == theirs.mode || conflict.base.is_some_and(|b| b.mode == theirs.mode) {
(ours.mode, false)
} else if conflict.base.is_some_and(|b| b.mode == ours.mode) {
(theirs.mode, false)
} else {
(ours.mode, true)
};
let base = match conflict.base {
Some(b) => self.memory.read_blob(self.objects, b.id)?,
None => Vec::new(),
};
let a = self.memory.read_blob(self.objects, ours.id)?;
let b = self.memory.read_blob(self.objects, theirs.id)?;
let content = if let Some(driver) = self.driver.as_deref_mut() {
driver
.merge(
&conflict.path,
&base,
&a,
&b,
self.options.content,
self.cancel,
)
.map_err(|source| TreeError::Driver {
path: conflict.path.clone(),
source,
})?
} else {
content_with(&base, &a, &b, self.options.content, self.cancel)?
};
self.check()?;
if content.is_clean() && !mode_conflict {
let bytes = content.render(&MarkerOptions::default());
let id = ObjectId::for_blob(self.objects.object_format(), &bytes);
self.blobs.push((id, bytes));
return Ok(Some(TreeValue { mode, id }));
}
conflict.kind = if mode_conflict {
ConflictKind::Mode
} else if conflict.base.is_none() {
ConflictKind::AddAdd
} else {
ConflictKind::Content
};
conflict.content = Some(content);
self.conflicts.push(conflict);
Ok(Some(TreeValue { mode, id: ours.id }))
}
}
fn regular(mode: EntryMode) -> bool {
matches!(mode, EntryMode::Blob | EntryMode::Executable)
}
fn directories(
base: Option<TreeValue>,
ours: Option<TreeValue>,
theirs: Option<TreeValue>,
) -> bool {
ours.is_some_and(|v| v.mode == EntryMode::Tree)
&& theirs.is_some_and(|v| v.mode == EntryMode::Tree)
&& base.is_none_or(|v| v.mode == EntryMode::Tree)
}