use crate::{FileId, GenerationId};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
use std::sync::Arc;
use thiserror::Error;
const RESOLUTION_KEY_DOMAIN: &[u8] = b"acyclic-fs-resolution-key-v1\0";
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
pub enum ConflictKey {
File(FileId),
Binding {
directory_id: FileId,
name: Vec<u8>,
},
ContentRange {
file_id: FileId,
offset: u64,
length: u64,
},
Metadata(FileId),
HardLinks(FileId),
Directory(FileId),
Rename {
file_id: FileId,
from: String,
to: String,
},
SpecialPayload(FileId),
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum ConflictKind {
Record,
Text,
Binary,
Metadata,
Binding,
Rename,
Directory,
HardLink,
SymbolicLink,
Special,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum ConflictValue {
Absent,
Text(String),
Binary(Vec<u8>),
Metadata(Vec<u8>),
Binding(Option<FileId>),
Record {
generation: GenerationId,
file_id: FileId,
},
RecordReference {
generation: GenerationId,
file_id: FileId,
},
BindingReference {
generation: GenerationId,
directory_id: FileId,
name: Vec<u8>,
},
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct ConflictView {
pub key: ConflictKey,
pub path: Option<String>,
pub kind: ConflictKind,
pub base: ConflictValue,
pub ours: ConflictValue,
pub theirs: ConflictValue,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum ConflictSide {
Base,
Ours,
Theirs,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum MergeResolution {
Select(ConflictSide),
Text(String),
Binary(Vec<u8>),
Metadata(Vec<u8>),
Binding(Option<FileId>),
Unresolved,
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ResolutionKey([u8; 32]);
impl ResolutionKey {
#[must_use]
pub fn new(
base: GenerationId,
ours: GenerationId,
theirs: GenerationId,
conflict: &ConflictKey,
driver_fingerprint: &[u8],
) -> Self {
let mut hasher = blake3::Hasher::new();
hasher.update(RESOLUTION_KEY_DOMAIN);
hasher.update(base.digest().as_bytes());
hasher.update(ours.digest().as_bytes());
hasher.update(theirs.digest().as_bytes());
hash_conflict_key(&mut hasher, conflict);
hasher.update(
&u64::try_from(driver_fingerprint.len())
.unwrap_or(u64::MAX)
.to_le_bytes(),
);
hasher.update(driver_fingerprint);
Self(*hasher.finalize().as_bytes())
}
#[must_use]
pub const fn into_bytes(self) -> [u8; 32] {
self.0
}
}
fn hash_conflict_key(hasher: &mut blake3::Hasher, key: &ConflictKey) {
match key {
ConflictKey::File(file_id) => {
hasher.update(&[0]);
hasher.update(&file_id.into_bytes());
}
ConflictKey::Binding { directory_id, name } => {
hasher.update(&[1]);
hasher.update(&directory_id.into_bytes());
hasher.update(&u64::try_from(name.len()).unwrap_or(u64::MAX).to_le_bytes());
hasher.update(name);
}
ConflictKey::ContentRange {
file_id,
offset,
length,
} => {
hasher.update(&[2]);
hasher.update(&file_id.into_bytes());
hasher.update(&offset.to_le_bytes());
hasher.update(&length.to_le_bytes());
}
ConflictKey::Metadata(file_id) => {
hasher.update(&[3]);
hasher.update(&file_id.into_bytes());
}
ConflictKey::HardLinks(file_id) => {
hasher.update(&[4]);
hasher.update(&file_id.into_bytes());
}
ConflictKey::Directory(file_id) => {
hasher.update(&[5]);
hasher.update(&file_id.into_bytes());
}
ConflictKey::Rename { file_id, from, to } => {
hasher.update(&[6]);
hasher.update(&file_id.into_bytes());
hash_sized(hasher, from.as_bytes());
hash_sized(hasher, to.as_bytes());
}
ConflictKey::SpecialPayload(file_id) => {
hasher.update(&[7]);
hasher.update(&file_id.into_bytes());
}
}
}
fn hash_sized(hasher: &mut blake3::Hasher, bytes: &[u8]) {
hasher.update(&u64::try_from(bytes.len()).unwrap_or(u64::MAX).to_le_bytes());
hasher.update(bytes);
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum MergeDriverMode {
Deterministic,
Effectful,
}
pub trait MergeDriver: Send + Sync {
fn fingerprint(&self) -> &[u8];
fn mode(&self) -> MergeDriverMode {
MergeDriverMode::Effectful
}
fn resolve(&self, conflict: &ConflictView) -> Result<MergeResolution, DriverError>;
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct MergePlan {
pub base: GenerationId,
pub ours: GenerationId,
pub theirs: GenerationId,
pub conflicts: Vec<ConflictView>,
pub truncated: bool,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct UnpublishedMergeCandidate {
pub plan: MergePlan,
pub resolutions: BTreeMap<ConflictKey, MergeResolution>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct CachedMergeResolution {
pub mode: MergeDriverMode,
pub resolution: MergeResolution,
}
pub trait MergeResolutionCache {
fn get(&self, key: ResolutionKey) -> Option<CachedMergeResolution>;
fn insert(&mut self, key: ResolutionKey, resolution: CachedMergeResolution);
}
#[derive(Default)]
pub struct MemoryMergeResolutionCache {
entries: BTreeMap<ResolutionKey, CachedMergeResolution>,
}
impl MergeResolutionCache for MemoryMergeResolutionCache {
fn get(&self, key: ResolutionKey) -> Option<CachedMergeResolution> {
self.entries.get(&key).cloned()
}
fn insert(&mut self, key: ResolutionKey, resolution: CachedMergeResolution) {
self.entries.insert(key, resolution);
}
}
#[derive(Clone, Debug, Error, Eq, PartialEq)]
pub enum MergePlanResolutionError {
#[error("merge plan contains duplicate conflict {0:?}")]
DuplicateConflict(ConflictKey),
#[error("no merge driver is registered for conflict {0:?}")]
MissingDriver(ConflictKey),
#[error(transparent)]
Driver(#[from] DriverError),
#[error("effectful merge resolution became stale after replanning")]
StaleEffectful,
}
pub fn resolve_merge_plan(
plan: MergePlan,
registry: &MergeDriverRegistry,
cache: &mut impl MergeResolutionCache,
replanning: bool,
) -> Result<UnpublishedMergeCandidate, MergePlanResolutionError> {
let mut resolutions = BTreeMap::new();
let mut conflicts = BTreeSet::new();
for conflict in &plan.conflicts {
if !conflicts.insert(conflict.key.clone()) {
return Err(MergePlanResolutionError::DuplicateConflict(
conflict.key.clone(),
));
}
let path = conflict.path.as_deref().unwrap_or("");
let driver = registry
.select(path)
.ok_or_else(|| MergePlanResolutionError::MissingDriver(conflict.key.clone()))?;
let key = ResolutionKey::new(
plan.base,
plan.ours,
plan.theirs,
&conflict.key,
driver.fingerprint(),
);
let resolution = match cache.get(key) {
Some(cached) => cached.resolution,
None if replanning && driver.mode() == MergeDriverMode::Effectful => {
return Err(MergePlanResolutionError::StaleEffectful);
}
None => {
let resolution = driver.resolve(conflict)?;
cache.insert(
key,
CachedMergeResolution {
mode: driver.mode(),
resolution: resolution.clone(),
},
);
resolution
}
};
resolutions.insert(conflict.key.clone(), resolution);
}
Ok(UnpublishedMergeCandidate { plan, resolutions })
}
#[derive(Clone, Debug, Error, Eq, PartialEq)]
pub enum DriverError {
#[error("merge driver does not support this conflict kind")]
Unsupported,
#[error("merge driver received invalid conflict input: {0}")]
InvalidInput(String),
#[error("merge driver failed: {0}")]
Failed(String),
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct AttributeRule {
pub pattern: String,
pub driver: String,
}
#[derive(Clone, Debug, Error, Eq, PartialEq)]
pub enum DriverRegistrationError {
#[error("merge driver name cannot be empty")]
EmptyName,
#[error("merge driver '{0}' is already registered")]
Duplicate(String),
#[error("attribute rule references unknown merge driver '{0}'")]
UnknownDriver(String),
#[error("invalid .gitattributes merge rule on line {0}")]
InvalidAttributeRule(usize),
}
#[derive(Default)]
pub struct MergeDriverRegistry {
drivers: BTreeMap<String, Arc<dyn MergeDriver>>,
rules: Vec<AttributeRule>,
default: Option<String>,
}
impl MergeDriverRegistry {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn register(
&mut self,
name: impl Into<String>,
driver: Arc<dyn MergeDriver>,
) -> Result<(), DriverRegistrationError> {
let name = name.into();
if name.is_empty() {
return Err(DriverRegistrationError::EmptyName);
}
if self.drivers.contains_key(&name) {
return Err(DriverRegistrationError::Duplicate(name));
}
self.drivers.insert(name, driver);
Ok(())
}
pub fn set_rules(&mut self, rules: Vec<AttributeRule>) -> Result<(), DriverRegistrationError> {
for rule in &rules {
if !self.drivers.contains_key(&rule.driver) {
return Err(DriverRegistrationError::UnknownDriver(rule.driver.clone()));
}
}
self.rules = rules;
Ok(())
}
pub fn set_git_attributes(&mut self, text: &str) -> Result<(), DriverRegistrationError> {
let mut rules = Vec::new();
for (index, raw) in text.lines().enumerate() {
let line = raw.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let mut fields = line.split_whitespace();
let pattern = fields.next().unwrap_or_default();
let attributes = fields.collect::<Vec<_>>();
let driver = attributes
.iter()
.rev()
.find_map(|attribute| attribute.strip_prefix("merge="));
if pattern.is_empty() || driver.is_some_and(str::is_empty) {
return Err(DriverRegistrationError::InvalidAttributeRule(index + 1));
}
let Some(driver) = driver else {
continue;
};
rules.push(AttributeRule {
pattern: pattern.to_owned(),
driver: driver.to_owned(),
});
}
self.set_rules(rules)
}
pub fn set_default(&mut self, name: impl Into<String>) -> Result<(), DriverRegistrationError> {
let name = name.into();
if !self.drivers.contains_key(&name) {
return Err(DriverRegistrationError::UnknownDriver(name));
}
self.default = Some(name);
Ok(())
}
#[must_use]
pub fn select(&self, path: &str) -> Option<&dyn MergeDriver> {
let selected = self
.rules
.iter()
.rfind(|rule| glob_matches(&rule.pattern, path))
.map(|rule| rule.driver.as_str())
.or(self.default.as_deref())?;
self.drivers.get(selected).map(AsRef::as_ref)
}
}
fn glob_matches(pattern: &str, path: &str) -> bool {
if pattern == "**" || pattern == "*" {
return true;
}
if let Some(suffix) = pattern.strip_prefix("**/*") {
return path.ends_with(suffix);
}
if let Some(suffix) = pattern.strip_prefix('*') {
return path.ends_with(suffix);
}
if let Some(prefix) = pattern.strip_suffix("/**") {
return path == prefix || path.starts_with(&format!("{prefix}/"));
}
pattern == path
}
pub struct DefaultTextMergeDriver;
impl MergeDriver for DefaultTextMergeDriver {
fn fingerprint(&self) -> &[u8] {
b"acyclic-default-text-v1"
}
fn mode(&self) -> MergeDriverMode {
MergeDriverMode::Deterministic
}
fn resolve(&self, conflict: &ConflictView) -> Result<MergeResolution, DriverError> {
let (ConflictValue::Text(base), ConflictValue::Text(ours), ConflictValue::Text(theirs)) =
(&conflict.base, &conflict.ours, &conflict.theirs)
else {
return Err(DriverError::Unsupported);
};
let merged = crate::text_merge::merge_text(base, ours, theirs, "ours", "theirs");
if merged.clean && merged.content == *ours {
Ok(MergeResolution::Select(ConflictSide::Ours))
} else if merged.clean && merged.content == *theirs {
Ok(MergeResolution::Select(ConflictSide::Theirs))
} else {
Ok(MergeResolution::Text(merged.content))
}
}
}
#[cfg(test)]
#[allow(clippy::expect_used)]
mod tests {
use super::*;
struct MarkerDriver(&'static [u8]);
impl MergeDriver for MarkerDriver {
fn fingerprint(&self) -> &[u8] {
self.0
}
fn mode(&self) -> MergeDriverMode {
MergeDriverMode::Deterministic
}
fn resolve(&self, _: &ConflictView) -> Result<MergeResolution, DriverError> {
Err(DriverError::Unsupported)
}
}
fn generation(byte: u8) -> GenerationId {
GenerationId::new(crate::Digest::from_bytes([byte; 32]))
}
#[test]
fn attribute_selection_is_ordered_and_text_projection_is_deterministic() {
let mut registry = MergeDriverRegistry::new();
registry
.register("text", Arc::new(DefaultTextMergeDriver))
.expect("register driver");
registry
.set_rules(vec![AttributeRule {
pattern: "*.rs".to_owned(),
driver: "text".to_owned(),
}])
.expect("set rule");
let driver = registry.select("src/lib.rs").expect("matching driver");
let conflict = ConflictView {
key: ConflictKey::Metadata(FileId::new()),
path: Some("src/lib.rs".to_owned()),
kind: ConflictKind::Text,
base: ConflictValue::Text("base".to_owned()),
ours: ConflictValue::Text("ours".to_owned()),
theirs: ConflictValue::Text("theirs".to_owned()),
};
assert!(matches!(
driver.resolve(&conflict).expect("resolve"),
MergeResolution::Text(text)
if crate::text_merge::has_conflict_markers(&text)
&& text.contains("<<<<<<< ours")
&& text.contains(">>>>>>> theirs")
));
}
#[test]
fn duplicate_registration_keeps_the_original_driver() {
let mut registry = MergeDriverRegistry::new();
registry
.register("driver", Arc::new(MarkerDriver(b"original")))
.expect("register original");
assert_eq!(
registry.register("driver", Arc::new(MarkerDriver(b"replacement"))),
Err(DriverRegistrationError::Duplicate("driver".to_owned()))
);
registry.set_default("driver").expect("select original");
assert_eq!(
registry
.select("anything")
.expect("registered driver")
.fingerprint(),
b"original"
);
}
#[test]
fn git_attributes_ignore_unrelated_attributes_and_accept_merge_among_them() {
let mut registry = MergeDriverRegistry::new();
registry
.register("text", Arc::new(DefaultTextMergeDriver))
.expect("register driver");
registry
.set_git_attributes("*.json text eol=lf\n*.bin binary\n*.rs text merge=text eol=lf\n")
.expect("ordinary Git attributes");
assert!(registry.select("data.json").is_none());
assert!(registry.select("asset.bin").is_none());
assert!(registry.select("src/lib.rs").is_some());
assert_eq!(
registry.set_git_attributes("*.rs merge="),
Err(DriverRegistrationError::InvalidAttributeRule(1))
);
}
#[test]
fn resolution_reuse_is_bound_to_exact_sides_and_driver_version() {
let mut registry = MergeDriverRegistry::new();
registry
.register("text", Arc::new(DefaultTextMergeDriver))
.expect("register driver");
registry.set_default("text").expect("default driver");
let conflict = ConflictView {
key: ConflictKey::ContentRange {
file_id: FileId::new(),
offset: 0,
length: 1,
},
path: Some("file.txt".to_owned()),
kind: ConflictKind::Text,
base: ConflictValue::Text("base".to_owned()),
ours: ConflictValue::Text("ours".to_owned()),
theirs: ConflictValue::Text("theirs".to_owned()),
};
let plan = MergePlan {
base: generation(1),
ours: generation(2),
theirs: generation(3),
conflicts: vec![conflict],
truncated: false,
};
let mut cache = MemoryMergeResolutionCache::default();
let first = resolve_merge_plan(plan.clone(), ®istry, &mut cache, false)
.expect("initial resolution");
assert_eq!(
resolve_merge_plan(plan, ®istry, &mut cache, true).expect("exact cache reuse"),
first
);
}
#[test]
fn duplicate_conflict_keys_are_rejected_before_driver_execution() {
let mut registry = MergeDriverRegistry::new();
registry
.register("text", Arc::new(DefaultTextMergeDriver))
.expect("register driver");
registry.set_default("text").expect("default driver");
let conflict = ConflictView {
key: ConflictKey::Metadata(FileId::new()),
path: Some("file.txt".to_owned()),
kind: ConflictKind::Text,
base: ConflictValue::Text("base".to_owned()),
ours: ConflictValue::Text("ours".to_owned()),
theirs: ConflictValue::Text("theirs".to_owned()),
};
let plan = MergePlan {
base: generation(1),
ours: generation(2),
theirs: generation(3),
conflicts: vec![conflict.clone(), conflict.clone()],
truncated: false,
};
assert_eq!(
resolve_merge_plan(
plan,
®istry,
&mut MemoryMergeResolutionCache::default(),
false,
),
Err(MergePlanResolutionError::DuplicateConflict(conflict.key))
);
}
}