1use crate::{FileId, GenerationId};
9use serde::{Deserialize, Serialize};
10use std::collections::{BTreeMap, BTreeSet};
11use std::sync::Arc;
12use thiserror::Error;
13
14const RESOLUTION_KEY_DOMAIN: &[u8] = b"acyclic-fs-resolution-key-v1\0";
15
16#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
18pub enum ConflictKey {
19 File(FileId),
21 Binding {
23 directory_id: FileId,
25 name: Vec<u8>,
27 },
28 ContentRange {
30 file_id: FileId,
32 offset: u64,
34 length: u64,
36 },
37 Metadata(FileId),
39 HardLinks(FileId),
41 Directory(FileId),
43 Rename {
45 file_id: FileId,
47 from: String,
49 to: String,
51 },
52 SpecialPayload(FileId),
54}
55
56#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
58#[non_exhaustive]
59pub enum ConflictKind {
60 Record,
62 Text,
64 Binary,
66 Metadata,
68 Binding,
70 Rename,
72 Directory,
74 HardLink,
76 SymbolicLink,
78 Special,
80}
81
82#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
84#[non_exhaustive]
85pub enum ConflictValue {
86 Absent,
88 Text(String),
90 Binary(Vec<u8>),
92 Metadata(Vec<u8>),
94 Binding(Option<FileId>),
96 Record {
98 generation: GenerationId,
100 file_id: FileId,
102 },
103 RecordReference {
105 generation: GenerationId,
107 file_id: FileId,
109 },
110 BindingReference {
112 generation: GenerationId,
114 directory_id: FileId,
116 name: Vec<u8>,
118 },
119}
120
121#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
123pub struct ConflictView {
124 pub key: ConflictKey,
126 pub path: Option<String>,
128 pub kind: ConflictKind,
130 pub base: ConflictValue,
132 pub ours: ConflictValue,
134 pub theirs: ConflictValue,
136}
137
138#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
140pub enum ConflictSide {
141 Base,
143 Ours,
145 Theirs,
147}
148
149#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
151#[non_exhaustive]
152pub enum MergeResolution {
153 Select(ConflictSide),
155 Text(String),
157 Binary(Vec<u8>),
159 Metadata(Vec<u8>),
161 Binding(Option<FileId>),
163 Unresolved,
165}
166
167#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
169#[serde(transparent)]
170pub struct ResolutionKey([u8; 32]);
171
172impl ResolutionKey {
173 #[must_use]
175 pub fn new(
176 base: GenerationId,
177 ours: GenerationId,
178 theirs: GenerationId,
179 conflict: &ConflictKey,
180 driver_fingerprint: &[u8],
181 ) -> Self {
182 let mut hasher = blake3::Hasher::new();
183 hasher.update(RESOLUTION_KEY_DOMAIN);
184 hasher.update(base.digest().as_bytes());
185 hasher.update(ours.digest().as_bytes());
186 hasher.update(theirs.digest().as_bytes());
187 hash_conflict_key(&mut hasher, conflict);
188 hasher.update(
189 &u64::try_from(driver_fingerprint.len())
190 .unwrap_or(u64::MAX)
191 .to_le_bytes(),
192 );
193 hasher.update(driver_fingerprint);
194 Self(*hasher.finalize().as_bytes())
195 }
196
197 #[must_use]
199 pub const fn into_bytes(self) -> [u8; 32] {
200 self.0
201 }
202}
203
204fn hash_conflict_key(hasher: &mut blake3::Hasher, key: &ConflictKey) {
205 match key {
206 ConflictKey::File(file_id) => {
207 hasher.update(&[0]);
208 hasher.update(&file_id.into_bytes());
209 }
210 ConflictKey::Binding { directory_id, name } => {
211 hasher.update(&[1]);
212 hasher.update(&directory_id.into_bytes());
213 hasher.update(&u64::try_from(name.len()).unwrap_or(u64::MAX).to_le_bytes());
214 hasher.update(name);
215 }
216 ConflictKey::ContentRange {
217 file_id,
218 offset,
219 length,
220 } => {
221 hasher.update(&[2]);
222 hasher.update(&file_id.into_bytes());
223 hasher.update(&offset.to_le_bytes());
224 hasher.update(&length.to_le_bytes());
225 }
226 ConflictKey::Metadata(file_id) => {
227 hasher.update(&[3]);
228 hasher.update(&file_id.into_bytes());
229 }
230 ConflictKey::HardLinks(file_id) => {
231 hasher.update(&[4]);
232 hasher.update(&file_id.into_bytes());
233 }
234 ConflictKey::Directory(file_id) => {
235 hasher.update(&[5]);
236 hasher.update(&file_id.into_bytes());
237 }
238 ConflictKey::Rename { file_id, from, to } => {
239 hasher.update(&[6]);
240 hasher.update(&file_id.into_bytes());
241 hash_sized(hasher, from.as_bytes());
242 hash_sized(hasher, to.as_bytes());
243 }
244 ConflictKey::SpecialPayload(file_id) => {
245 hasher.update(&[7]);
246 hasher.update(&file_id.into_bytes());
247 }
248 }
249}
250
251fn hash_sized(hasher: &mut blake3::Hasher, bytes: &[u8]) {
252 hasher.update(&u64::try_from(bytes.len()).unwrap_or(u64::MAX).to_le_bytes());
253 hasher.update(bytes);
254}
255
256#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
258pub enum MergeDriverMode {
259 Deterministic,
261 Effectful,
263}
264
265pub trait MergeDriver: Send + Sync {
267 fn fingerprint(&self) -> &[u8];
269
270 fn mode(&self) -> MergeDriverMode {
272 MergeDriverMode::Effectful
273 }
274
275 fn resolve(&self, conflict: &ConflictView) -> Result<MergeResolution, DriverError>;
277}
278
279#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
281pub struct MergePlan {
282 pub base: GenerationId,
284 pub ours: GenerationId,
286 pub theirs: GenerationId,
288 pub conflicts: Vec<ConflictView>,
290 pub truncated: bool,
292}
293
294#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
296pub struct UnpublishedMergeCandidate {
297 pub plan: MergePlan,
299 pub resolutions: BTreeMap<ConflictKey, MergeResolution>,
301}
302
303#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
305pub struct CachedMergeResolution {
306 pub mode: MergeDriverMode,
308 pub resolution: MergeResolution,
310}
311
312pub trait MergeResolutionCache {
314 fn get(&self, key: ResolutionKey) -> Option<CachedMergeResolution>;
316
317 fn insert(&mut self, key: ResolutionKey, resolution: CachedMergeResolution);
319}
320
321#[derive(Default)]
323pub struct MemoryMergeResolutionCache {
324 entries: BTreeMap<ResolutionKey, CachedMergeResolution>,
325}
326
327impl MergeResolutionCache for MemoryMergeResolutionCache {
328 fn get(&self, key: ResolutionKey) -> Option<CachedMergeResolution> {
329 self.entries.get(&key).cloned()
330 }
331
332 fn insert(&mut self, key: ResolutionKey, resolution: CachedMergeResolution) {
333 self.entries.insert(key, resolution);
334 }
335}
336
337#[derive(Clone, Debug, Error, Eq, PartialEq)]
339pub enum MergePlanResolutionError {
340 #[error("merge plan contains duplicate conflict {0:?}")]
342 DuplicateConflict(ConflictKey),
343 #[error("no merge driver is registered for conflict {0:?}")]
345 MissingDriver(ConflictKey),
346 #[error(transparent)]
348 Driver(#[from] DriverError),
349 #[error("effectful merge resolution became stale after replanning")]
351 StaleEffectful,
352}
353
354pub fn resolve_merge_plan(
360 plan: MergePlan,
361 registry: &MergeDriverRegistry,
362 cache: &mut impl MergeResolutionCache,
363 replanning: bool,
364) -> Result<UnpublishedMergeCandidate, MergePlanResolutionError> {
365 let mut resolutions = BTreeMap::new();
366 let mut conflicts = BTreeSet::new();
367 for conflict in &plan.conflicts {
368 if !conflicts.insert(conflict.key.clone()) {
369 return Err(MergePlanResolutionError::DuplicateConflict(
370 conflict.key.clone(),
371 ));
372 }
373 let path = conflict.path.as_deref().unwrap_or("");
374 let driver = registry
375 .select(path)
376 .ok_or_else(|| MergePlanResolutionError::MissingDriver(conflict.key.clone()))?;
377 let key = ResolutionKey::new(
378 plan.base,
379 plan.ours,
380 plan.theirs,
381 &conflict.key,
382 driver.fingerprint(),
383 );
384 let resolution = match cache.get(key) {
385 Some(cached) => cached.resolution,
386 None if replanning && driver.mode() == MergeDriverMode::Effectful => {
387 return Err(MergePlanResolutionError::StaleEffectful);
388 }
389 None => {
390 let resolution = driver.resolve(conflict)?;
391 cache.insert(
392 key,
393 CachedMergeResolution {
394 mode: driver.mode(),
395 resolution: resolution.clone(),
396 },
397 );
398 resolution
399 }
400 };
401 resolutions.insert(conflict.key.clone(), resolution);
402 }
403 Ok(UnpublishedMergeCandidate { plan, resolutions })
404}
405
406#[derive(Clone, Debug, Error, Eq, PartialEq)]
408pub enum DriverError {
409 #[error("merge driver does not support this conflict kind")]
411 Unsupported,
412 #[error("merge driver received invalid conflict input: {0}")]
414 InvalidInput(String),
415 #[error("merge driver failed: {0}")]
417 Failed(String),
418}
419
420#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
422pub struct AttributeRule {
423 pub pattern: String,
425 pub driver: String,
427}
428
429#[derive(Clone, Debug, Error, Eq, PartialEq)]
431pub enum DriverRegistrationError {
432 #[error("merge driver name cannot be empty")]
434 EmptyName,
435 #[error("merge driver '{0}' is already registered")]
437 Duplicate(String),
438 #[error("attribute rule references unknown merge driver '{0}'")]
440 UnknownDriver(String),
441 #[error("invalid .gitattributes merge rule on line {0}")]
443 InvalidAttributeRule(usize),
444}
445
446#[derive(Default)]
448pub struct MergeDriverRegistry {
449 drivers: BTreeMap<String, Arc<dyn MergeDriver>>,
450 rules: Vec<AttributeRule>,
451 default: Option<String>,
452}
453
454impl MergeDriverRegistry {
455 #[must_use]
457 pub fn new() -> Self {
458 Self::default()
459 }
460
461 pub fn register(
463 &mut self,
464 name: impl Into<String>,
465 driver: Arc<dyn MergeDriver>,
466 ) -> Result<(), DriverRegistrationError> {
467 let name = name.into();
468 if name.is_empty() {
469 return Err(DriverRegistrationError::EmptyName);
470 }
471 if self.drivers.contains_key(&name) {
472 return Err(DriverRegistrationError::Duplicate(name));
473 }
474 self.drivers.insert(name, driver);
475 Ok(())
476 }
477
478 pub fn set_rules(&mut self, rules: Vec<AttributeRule>) -> Result<(), DriverRegistrationError> {
480 for rule in &rules {
481 if !self.drivers.contains_key(&rule.driver) {
482 return Err(DriverRegistrationError::UnknownDriver(rule.driver.clone()));
483 }
484 }
485 self.rules = rules;
486 Ok(())
487 }
488
489 pub fn set_git_attributes(&mut self, text: &str) -> Result<(), DriverRegistrationError> {
491 let mut rules = Vec::new();
492 for (index, raw) in text.lines().enumerate() {
493 let line = raw.trim();
494 if line.is_empty() || line.starts_with('#') {
495 continue;
496 }
497 let mut fields = line.split_whitespace();
498 let pattern = fields.next().unwrap_or_default();
499 let attributes = fields.collect::<Vec<_>>();
500 let driver = attributes
501 .iter()
502 .rev()
503 .find_map(|attribute| attribute.strip_prefix("merge="));
504 if pattern.is_empty() || driver.is_some_and(str::is_empty) {
505 return Err(DriverRegistrationError::InvalidAttributeRule(index + 1));
506 }
507 let Some(driver) = driver else {
508 continue;
512 };
513 rules.push(AttributeRule {
514 pattern: pattern.to_owned(),
515 driver: driver.to_owned(),
516 });
517 }
518 self.set_rules(rules)
519 }
520
521 pub fn set_default(&mut self, name: impl Into<String>) -> Result<(), DriverRegistrationError> {
523 let name = name.into();
524 if !self.drivers.contains_key(&name) {
525 return Err(DriverRegistrationError::UnknownDriver(name));
526 }
527 self.default = Some(name);
528 Ok(())
529 }
530
531 #[must_use]
533 pub fn select(&self, path: &str) -> Option<&dyn MergeDriver> {
534 let selected = self
535 .rules
536 .iter()
537 .rfind(|rule| glob_matches(&rule.pattern, path))
538 .map(|rule| rule.driver.as_str())
539 .or(self.default.as_deref())?;
540 self.drivers.get(selected).map(AsRef::as_ref)
541 }
542}
543
544fn glob_matches(pattern: &str, path: &str) -> bool {
545 if pattern == "**" || pattern == "*" {
546 return true;
547 }
548 if let Some(suffix) = pattern.strip_prefix("**/*") {
549 return path.ends_with(suffix);
550 }
551 if let Some(suffix) = pattern.strip_prefix('*') {
552 return path.ends_with(suffix);
553 }
554 if let Some(prefix) = pattern.strip_suffix("/**") {
555 return path == prefix || path.starts_with(&format!("{prefix}/"));
556 }
557 pattern == path
558}
559
560pub struct DefaultTextMergeDriver;
562
563impl MergeDriver for DefaultTextMergeDriver {
564 fn fingerprint(&self) -> &[u8] {
565 b"acyclic-default-text-v1"
566 }
567
568 fn mode(&self) -> MergeDriverMode {
569 MergeDriverMode::Deterministic
570 }
571
572 fn resolve(&self, conflict: &ConflictView) -> Result<MergeResolution, DriverError> {
573 let (ConflictValue::Text(base), ConflictValue::Text(ours), ConflictValue::Text(theirs)) =
574 (&conflict.base, &conflict.ours, &conflict.theirs)
575 else {
576 return Err(DriverError::Unsupported);
577 };
578 let merged = crate::text_merge::merge_text(base, ours, theirs, "ours", "theirs");
579 if merged.clean && merged.content == *ours {
580 Ok(MergeResolution::Select(ConflictSide::Ours))
581 } else if merged.clean && merged.content == *theirs {
582 Ok(MergeResolution::Select(ConflictSide::Theirs))
583 } else {
584 Ok(MergeResolution::Text(merged.content))
585 }
586 }
587}
588
589#[cfg(test)]
590#[allow(clippy::expect_used)]
591mod tests {
592 use super::*;
593
594 struct MarkerDriver(&'static [u8]);
595
596 impl MergeDriver for MarkerDriver {
597 fn fingerprint(&self) -> &[u8] {
598 self.0
599 }
600
601 fn mode(&self) -> MergeDriverMode {
602 MergeDriverMode::Deterministic
603 }
604
605 fn resolve(&self, _: &ConflictView) -> Result<MergeResolution, DriverError> {
606 Err(DriverError::Unsupported)
607 }
608 }
609
610 fn generation(byte: u8) -> GenerationId {
611 GenerationId::new(crate::Digest::from_bytes([byte; 32]))
612 }
613
614 #[test]
615 fn attribute_selection_is_ordered_and_text_projection_is_deterministic() {
616 let mut registry = MergeDriverRegistry::new();
617 registry
618 .register("text", Arc::new(DefaultTextMergeDriver))
619 .expect("register driver");
620 registry
621 .set_rules(vec![AttributeRule {
622 pattern: "*.rs".to_owned(),
623 driver: "text".to_owned(),
624 }])
625 .expect("set rule");
626 let driver = registry.select("src/lib.rs").expect("matching driver");
627 let conflict = ConflictView {
628 key: ConflictKey::Metadata(FileId::new()),
629 path: Some("src/lib.rs".to_owned()),
630 kind: ConflictKind::Text,
631 base: ConflictValue::Text("base".to_owned()),
632 ours: ConflictValue::Text("ours".to_owned()),
633 theirs: ConflictValue::Text("theirs".to_owned()),
634 };
635 assert!(matches!(
636 driver.resolve(&conflict).expect("resolve"),
637 MergeResolution::Text(text)
638 if crate::text_merge::has_conflict_markers(&text)
639 && text.contains("<<<<<<< ours")
640 && text.contains(">>>>>>> theirs")
641 ));
642 }
643
644 #[test]
645 fn duplicate_registration_keeps_the_original_driver() {
646 let mut registry = MergeDriverRegistry::new();
647 registry
648 .register("driver", Arc::new(MarkerDriver(b"original")))
649 .expect("register original");
650 assert_eq!(
651 registry.register("driver", Arc::new(MarkerDriver(b"replacement"))),
652 Err(DriverRegistrationError::Duplicate("driver".to_owned()))
653 );
654 registry.set_default("driver").expect("select original");
655 assert_eq!(
656 registry
657 .select("anything")
658 .expect("registered driver")
659 .fingerprint(),
660 b"original"
661 );
662 }
663
664 #[test]
665 fn git_attributes_ignore_unrelated_attributes_and_accept_merge_among_them() {
666 let mut registry = MergeDriverRegistry::new();
667 registry
668 .register("text", Arc::new(DefaultTextMergeDriver))
669 .expect("register driver");
670 registry
671 .set_git_attributes("*.json text eol=lf\n*.bin binary\n*.rs text merge=text eol=lf\n")
672 .expect("ordinary Git attributes");
673 assert!(registry.select("data.json").is_none());
674 assert!(registry.select("asset.bin").is_none());
675 assert!(registry.select("src/lib.rs").is_some());
676 assert_eq!(
677 registry.set_git_attributes("*.rs merge="),
678 Err(DriverRegistrationError::InvalidAttributeRule(1))
679 );
680 }
681
682 #[test]
683 fn resolution_reuse_is_bound_to_exact_sides_and_driver_version() {
684 let mut registry = MergeDriverRegistry::new();
685 registry
686 .register("text", Arc::new(DefaultTextMergeDriver))
687 .expect("register driver");
688 registry.set_default("text").expect("default driver");
689 let conflict = ConflictView {
690 key: ConflictKey::ContentRange {
691 file_id: FileId::new(),
692 offset: 0,
693 length: 1,
694 },
695 path: Some("file.txt".to_owned()),
696 kind: ConflictKind::Text,
697 base: ConflictValue::Text("base".to_owned()),
698 ours: ConflictValue::Text("ours".to_owned()),
699 theirs: ConflictValue::Text("theirs".to_owned()),
700 };
701 let plan = MergePlan {
702 base: generation(1),
703 ours: generation(2),
704 theirs: generation(3),
705 conflicts: vec![conflict],
706 truncated: false,
707 };
708 let mut cache = MemoryMergeResolutionCache::default();
709 let first = resolve_merge_plan(plan.clone(), ®istry, &mut cache, false)
710 .expect("initial resolution");
711 assert_eq!(
712 resolve_merge_plan(plan, ®istry, &mut cache, true).expect("exact cache reuse"),
713 first
714 );
715 }
716
717 #[test]
718 fn duplicate_conflict_keys_are_rejected_before_driver_execution() {
719 let mut registry = MergeDriverRegistry::new();
720 registry
721 .register("text", Arc::new(DefaultTextMergeDriver))
722 .expect("register driver");
723 registry.set_default("text").expect("default driver");
724 let conflict = ConflictView {
725 key: ConflictKey::Metadata(FileId::new()),
726 path: Some("file.txt".to_owned()),
727 kind: ConflictKind::Text,
728 base: ConflictValue::Text("base".to_owned()),
729 ours: ConflictValue::Text("ours".to_owned()),
730 theirs: ConflictValue::Text("theirs".to_owned()),
731 };
732 let plan = MergePlan {
733 base: generation(1),
734 ours: generation(2),
735 theirs: generation(3),
736 conflicts: vec![conflict.clone(), conflict.clone()],
737 truncated: false,
738 };
739
740 assert_eq!(
741 resolve_merge_plan(
742 plan,
743 ®istry,
744 &mut MemoryMergeResolutionCache::default(),
745 false,
746 ),
747 Err(MergePlanResolutionError::DuplicateConflict(conflict.key))
748 );
749 }
750}