use std::collections::BTreeSet;
use std::sync::atomic::AtomicBool;
use super::{Error, Snapshot, clean};
use crate::index::Index;
use crate::merge::{MarkerOptions, PreparationLimits, TreeMerge, TreeMergeOptions};
use crate::{ObjectId, Objects, Tree};
#[derive(Clone, Debug)]
pub struct ApplyInput {
pub index: ObjectId,
pub occupied: Vec<Vec<u8>>,
}
#[derive(Clone, Copy, Debug, Default)]
#[non_exhaustive]
pub struct ApplyOptions {
pub restore_index: bool,
pub trees: TreeMergeOptions,
pub preparation: PreparationLimits,
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct Apply {
pub tracked: TreeMerge,
pub index_merge: Option<TreeMerge>,
pub with_untracked: Option<TreeMerge>,
pub index: Option<Index>,
}
impl Apply {
pub fn is_clean(&self) -> bool {
self.tracked.is_clean()
&& self.index_merge.as_ref().is_none_or(TreeMerge::is_clean)
&& self.with_untracked.as_ref().is_none_or(TreeMerge::is_clean)
}
pub fn prepare_checkout(
&self,
objects: &Objects,
markers: &MarkerOptions,
) -> Result<crate::merge::MergeCheckout, Error> {
let index = self.index.clone().ok_or(Error::IndexConflict)?;
let mut working = self.working().clone();
if let Some(staged) = &self.index_merge {
working.trees.splice(0..0, staged.trees.iter().cloned());
working.blobs.splice(0..0, staged.blobs.iter().cloned());
}
Ok(working
.prepare_checkout(objects, markers)?
.with_index(index)?)
}
pub fn working(&self) -> &TreeMerge {
self.with_untracked.as_ref().unwrap_or(&self.tracked)
}
}
impl Objects {
pub fn apply_stash(
&self,
saved: &Snapshot,
input: ApplyInput,
options: ApplyOptions,
cancel: &AtomicBool,
) -> Result<Apply, Error> {
if cancel.load(std::sync::atomic::Ordering::Relaxed) {
return Err(Error::Cancelled);
}
let current = self.read_tree(input.index)?;
let current_index = draft(self, &clean(current.clone()), options.preparation, cancel)?;
check_untracked(
self,
saved,
&input,
¤t_index,
options.preparation,
cancel,
)?;
let empty = Tree::new(input.index.format(), vec![])?;
let mut memory = crate::merge::ReplayTrees::default();
let index_merge = if options.restore_index {
Some(memory.merge(
self,
[saved.base.tree(), input.index, saved.index.tree()],
empty.clone(),
options.trees,
cancel,
None,
)?)
} else {
None
};
if index_merge
.as_ref()
.is_some_and(|result| !result.is_clean())
{
return Ok(Apply {
tracked: clean(current),
index_merge,
with_untracked: None,
index: None,
});
}
let tracked = memory.merge(
self,
[saved.base.tree(), input.index, saved.worktree.tree()],
empty.clone(),
options.trees,
cancel,
None,
)?;
let index = if !tracked.is_clean() {
draft(self, &tracked, options.preparation, cancel)?
} else if let Some(result) = &index_merge {
draft(self, result, options.preparation, cancel)?
} else {
let merged = draft(self, &tracked, options.preparation, cancel)?;
let existing: BTreeSet<&[u8]> = current_index
.entries()
.iter()
.map(|e| e.path.as_slice())
.collect();
let additions: BTreeSet<Vec<u8>> = merged
.entries()
.iter()
.filter(|entry| !existing.contains(entry.path.as_slice()))
.map(|entry| entry.path.clone())
.collect();
let entries = current_index
.entries()
.iter()
.filter(|entry| !collision(&additions, &entry.path))
.cloned()
.chain(
merged
.entries()
.iter()
.filter(|entry| additions.contains(&entry.path))
.cloned(),
)
.collect();
Index::new(input.index.format(), entries, options.preparation.index)?
};
let mut with_untracked = if tracked.is_clean() {
let working_index = draft(self, &tracked, options.preparation, cancel)?;
check_untracked(
self,
saved,
&input,
&working_index,
options.preparation,
cancel,
)?;
saved
.untracked
.as_ref()
.map(|commit| {
memory.merge(
self,
[empty.id(), tracked.tree.id(), commit.tree()],
empty.clone(),
options.trees,
cancel,
None,
)
})
.transpose()?
} else {
None
};
if let Some(result) = &mut with_untracked {
result.trees.splice(0..0, tracked.trees.iter().cloned());
result.blobs.splice(0..0, tracked.blobs.iter().cloned());
}
Ok(Apply {
tracked,
index_merge,
with_untracked,
index: Some(index),
})
}
}
fn draft(
objects: &Objects,
result: &TreeMerge,
limits: PreparationLimits,
cancel: &AtomicBool,
) -> Result<Index, Error> {
Ok(result
.prepare_checkout_with(objects, &MarkerOptions::default(), limits, cancel)?
.index()
.clone())
}
fn check_untracked(
objects: &Objects,
saved: &Snapshot,
input: &ApplyInput,
current: &Index,
limits: PreparationLimits,
cancel: &AtomicBool,
) -> Result<(), Error> {
if input.occupied.len() > limits.index.max_entries
|| input
.occupied
.iter()
.any(|p| p.len() > limits.index.max_path_bytes)
|| input
.occupied
.iter()
.fold(0usize, |total, path| total.saturating_add(path.len()))
> limits.index.max_bytes
{
return Err(Error::Limit);
}
let Some(untracked) = &saved.untracked else {
return Ok(());
};
let saved = draft(
objects,
&clean(objects.read_tree(untracked.tree())?),
limits,
cancel,
)?;
let occupied: BTreeSet<Vec<u8>> = current
.entries()
.iter()
.map(|e| e.path.clone())
.chain(input.occupied.iter().cloned())
.collect();
for entry in saved.entries() {
if collision(&occupied, &entry.path) {
return Err(Error::UntrackedCollision(entry.path.clone()));
}
}
Ok(())
}
fn collision(occupied: &BTreeSet<Vec<u8>>, path: &[u8]) -> bool {
if occupied.contains(path) {
return true;
}
for (position, byte) in path.iter().enumerate() {
if *byte == b'/' && occupied.contains(&path[..position]) {
return true;
}
}
occupied.range(path.to_vec()..).next().is_some_and(|next| {
next.strip_prefix(path)
.is_some_and(|tail| tail.starts_with(b"/"))
})
}