use std::sync::atomic::{AtomicBool, Ordering};
use super::memory::Memory;
use super::{
ContentHunk, ContentMerge, MarkerOptions, RenameError, TreeError, TreeMerge, TreeMergeOptions,
};
use crate::{EntryMode, ObjectId, Objects, Repository, Tree, index};
#[derive(Clone, Copy, Debug)]
#[non_exhaustive]
pub struct PreparationLimits {
pub trees: TreeMergeOptions,
pub index: index::Limits,
pub max_marker_bytes: usize,
}
impl Default for PreparationLimits {
fn default() -> Self {
Self {
trees: TreeMergeOptions::default(),
index: index::Limits::default(),
max_marker_bytes: 64 * 1024 * 1024,
}
}
}
#[derive(Clone, Debug)]
pub struct MergeCheckout {
tree: ObjectId,
index: index::Index,
blobs: Vec<(ObjectId, Vec<u8>)>,
trees: Vec<Tree>,
}
impl MergeCheckout {
pub(crate) fn with_index(mut self, index: index::Index) -> Result<Self, PublicationError> {
if index.object_format() != self.index.object_format() {
return Err(PublicationError::ObjectFormat);
}
self.index = index;
Ok(self)
}
pub fn tree_id(&self) -> ObjectId {
self.tree
}
pub fn index(&self) -> &index::Index {
&self.index
}
pub fn install_objects(&self, loose: &crate::LooseObjects) -> Result<(), PublicationError> {
self.install_objects_controlled(loose, &AtomicBool::new(false))
}
pub fn install_objects_controlled(
&self,
loose: &crate::LooseObjects,
cancel: &AtomicBool,
) -> Result<(), PublicationError> {
check(cancel)?;
if loose.object_format() != self.index.object_format() {
return Err(PublicationError::ObjectFormat);
}
for (_, bytes) in &self.blobs {
check(cancel)?;
loose.write_blob(bytes)?;
}
for tree in &self.trees {
check(cancel)?;
loose.write_tree(tree)?;
}
Ok(())
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum PublicationError {
#[error("preparing merge paths")]
Paths(#[from] RenameError),
#[error("preparing merge conflict objects")]
Tree(#[from] TreeError),
#[error("preparing merge index")]
Index(#[from] index::Error),
#[error("installing merge objects")]
Objects(#[from] crate::loose::Error),
#[error("publishing merge checkout")]
Checkout(#[source] Box<crate::checkout::Failure>),
#[error("merge publication object format mismatch")]
ObjectFormat,
#[error("generated merge blob identity mismatch")]
ObjectIdentity,
#[error("merge preparation limit: {0}")]
Limit(&'static str),
#[error("merge publication cancelled")]
Cancelled,
}
impl TreeMerge {
pub fn prepare_checkout(
&self,
objects: &Objects,
markers: &MarkerOptions,
) -> Result<MergeCheckout, PublicationError> {
self.prepare_checkout_with(
objects,
markers,
PreparationLimits::default(),
&AtomicBool::new(false),
)
}
pub fn prepare_checkout_with(
&self,
objects: &Objects,
markers: &MarkerOptions,
limits: PreparationLimits,
cancel: &AtomicBool,
) -> Result<MergeCheckout, PublicationError> {
check(cancel)?;
if objects.object_format() != self.tree.object_format() {
return Err(PublicationError::ObjectFormat);
}
let mut memory = Memory::default();
for tree in &self.trees {
if tree.object_format() != objects.object_format() {
return Err(PublicationError::ObjectFormat);
}
if !memory.trees.contains_key(&tree.id()) {
memory.order.push(tree.id());
}
memory.trees.insert(tree.id(), tree.clone());
}
for (id, bytes) in &self.blobs {
check(cancel)?;
if *id != ObjectId::for_blob(objects.object_format(), bytes) {
return Err(PublicationError::ObjectIdentity);
}
memory.blobs.insert(*id, bytes.clone());
}
if !memory.trees.contains_key(&self.tree.id()) {
memory.order.push(self.tree.id());
}
memory.trees.insert(self.tree.id(), self.tree.clone());
let mut leaves =
super::renames::flatten(objects, self.tree.id(), limits.trees, cancel, &memory)?;
let mut staged = Vec::new();
let mut conflict_paths = std::collections::BTreeSet::new();
let mut remaining = limits.max_marker_bytes;
for conflict in &self.conflicts {
check(cancel)?;
let entries = conflict.index_entries()?;
for entry in &entries {
check(cancel)?;
if entry.mode != index::Mode::Gitlink {
memory.read_blob(objects, entry.id)?;
}
}
conflict_paths.extend(entries.iter().map(|entry| entry.path.clone()));
staged.extend(entries);
if let Some(content) = &conflict.content {
remaining = remaining
.checked_sub(render_bound(content, markers)?)
.ok_or(PublicationError::Limit("marker bytes"))?;
if let Some(leaf) = leaves.get_mut(&conflict.path) {
let bytes = content.render(markers);
let id = ObjectId::for_blob(objects.object_format(), &bytes);
memory.blobs.insert(id, bytes);
leaf.id = id;
}
}
}
for (path, value) in &leaves {
if !conflict_paths.contains(path) {
staged.push(index::Entry::new(path.clone(), mode(value.mode)?, value.id));
}
}
let index = index::Index::new(objects.object_format(), staged, limits.index)?;
let tree = memory.build(objects, leaves)?;
check(cancel)?;
let trees = memory
.order
.iter()
.map(|id| memory.trees[id].clone())
.collect();
Ok(MergeCheckout {
tree,
index,
blobs: memory.blobs.into_iter().collect(),
trees,
})
}
}
impl Repository {
pub fn checkout_merge(
&self,
baseline: Option<ObjectId>,
plan: &MergeCheckout,
) -> Result<crate::checkout::Report, PublicationError> {
self.checkout_merge_with(
baseline,
plan,
crate::checkout::Limits::default(),
&AtomicBool::new(false),
)
}
pub fn checkout_merge_with(
&self,
baseline: Option<ObjectId>,
plan: &MergeCheckout,
limits: crate::checkout::Limits,
cancel: &AtomicBool,
) -> Result<crate::checkout::Report, PublicationError> {
let _operation = crate::locks::operation();
check(cancel)?;
if self.object_format() != plan.index.object_format()
|| baseline.is_some_and(|id| id.format() != self.object_format())
{
return Err(PublicationError::ObjectFormat);
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
{
plan.install_objects_controlled(&self.loose_objects(), cancel)?;
crate::checkout::run_with_index(
self,
baseline,
plan.tree,
plan.index.entries(),
limits,
cancel,
)
.map_err(|failure| PublicationError::Checkout(Box::new(failure)))
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
self.checkout_tree_with(baseline, Some(plan.tree), limits, cancel)
.map_err(|failure| PublicationError::Checkout(Box::new(failure)))
}
}
}
fn check(cancel: &AtomicBool) -> Result<(), PublicationError> {
if cancel.load(Ordering::Relaxed) {
Err(PublicationError::Cancelled)
} else {
Ok(())
}
}
fn mode(mode: EntryMode) -> Result<index::Mode, PublicationError> {
Ok(match mode {
EntryMode::Blob => index::Mode::Regular,
EntryMode::Executable => index::Mode::Executable,
EntryMode::Symlink => index::Mode::Symlink,
EntryMode::Gitlink => index::Mode::Gitlink,
EntryMode::Tree => return Err(TreeError::DirectoryStages.into()),
})
}
fn render_bound(
content: &ContentMerge,
markers: &MarkerOptions,
) -> Result<usize, PublicationError> {
let limit = || PublicationError::Limit("marker bytes");
let mut count = 0usize;
for hunk in content.hunks() {
let bytes = match hunk {
ContentHunk::Resolved(bytes) => bytes.len(),
ContentHunk::Conflict { ours, .. } if content.is_binary() => ours.len(),
ContentHunk::Conflict { base, ours, theirs } => {
let label = markers
.base
.len()
.max(markers.ours.len())
.max(markers.theirs.len());
let lines = markers
.width
.get()
.checked_add(label)
.and_then(|n| n.checked_add(2))
.and_then(|n| n.checked_mul(4))
.ok_or_else(limit)?;
base.len()
.checked_add(ours.len())
.and_then(|n| n.checked_add(theirs.len()))
.and_then(|n| n.checked_add(lines))
.and_then(|n| n.checked_add(3))
.ok_or_else(limit)?
}
};
count = count.checked_add(bytes).ok_or_else(limit)?;
}
Ok(count)
}