Skip to main content

acyclic_fs/
merge_driver.rs

1//! Declarative merge-driver boundary over immutable conflict inputs.
2//!
3//! Drivers never receive a checkout, store, or publication authority. They
4//! transform a complete immutable conflict view into a resolution proposal;
5//! callers then submit that proposal to the filesystem's validation and
6//! publication path.
7
8use 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/// Exact stable identity of one conflict region.
17#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
18pub enum ConflictKey {
19    /// A path-independent file record.
20    File(FileId),
21    /// One directory name binding.
22    Binding {
23        /// Stable parent directory.
24        directory_id: FileId,
25        /// Opaque canonical name bytes.
26        name: Vec<u8>,
27    },
28    /// One regular-file byte range.
29    ContentRange {
30        /// Stable file identity.
31        file_id: FileId,
32        /// Inclusive start byte.
33        offset: u64,
34        /// Range length.
35        length: u64,
36    },
37    /// Metadata attached to one stable file record.
38    Metadata(FileId),
39    /// Hard-link topology involving one stable record.
40    HardLinks(FileId),
41    /// One directory record or subtree.
42    Directory(FileId),
43    /// One rename relationship represented independently from path spelling.
44    Rename {
45        /// Stable renamed record.
46        file_id: FileId,
47        /// Canonical source path.
48        from: String,
49        /// Canonical destination path.
50        to: String,
51    },
52    /// Symbolic-link, device, reparse, or another special payload.
53    SpecialPayload(FileId),
54}
55
56/// Semantic conflict category exposed to policy and user interfaces.
57#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
58#[non_exhaustive]
59pub enum ConflictKind {
60    /// Complete path-independent file record whose detailed kind is not loaded.
61    Record,
62    /// UTF-8 regular content eligible for marker projection.
63    Text,
64    /// Opaque regular content.
65    Binary,
66    /// File metadata.
67    Metadata,
68    /// Directory name or rename topology.
69    Binding,
70    /// Competing rename topology.
71    Rename,
72    /// Directory record or subtree.
73    Directory,
74    /// Hard-link topology.
75    HardLink,
76    /// Symbolic-link payload.
77    SymbolicLink,
78    /// Symbolic-link, device, reparse, or another opaque payload.
79    Special,
80}
81
82/// One immutable side of a conflict.
83#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
84#[non_exhaustive]
85pub enum ConflictValue {
86    /// The item is absent on this side.
87    Absent,
88    /// Complete UTF-8 content.
89    Text(String),
90    /// Complete opaque content or special payload.
91    Binary(Vec<u8>),
92    /// Canonical serialized metadata.
93    Metadata(Vec<u8>),
94    /// Stable binding target, or `None` for deletion.
95    Binding(Option<FileId>),
96    /// Exact generation and stable record for lazy inspection.
97    Record {
98        /// Immutable generation containing the record.
99        generation: GenerationId,
100        /// Stable record identity.
101        file_id: FileId,
102    },
103    /// Lazy immutable file-record lookup that may resolve to absence.
104    RecordReference {
105        /// Immutable generation to inspect.
106        generation: GenerationId,
107        /// Stable record identity to look up.
108        file_id: FileId,
109    },
110    /// Lazy immutable directory-binding lookup that may resolve to absence.
111    BindingReference {
112        /// Immutable generation to inspect.
113        generation: GenerationId,
114        /// Stable parent directory.
115        directory_id: FileId,
116        /// Encoding-tagged canonical name bytes.
117        name: Vec<u8>,
118    },
119}
120
121/// Complete immutable input supplied to one merge driver.
122#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
123pub struct ConflictView {
124    /// Stable conflict-region key.
125    pub key: ConflictKey,
126    /// Best-effort portable path; identity remains authoritative.
127    pub path: Option<String>,
128    /// Semantic category.
129    pub kind: ConflictKind,
130    /// Common-ancestor value.
131    pub base: ConflictValue,
132    /// Target-side value.
133    pub ours: ConflictValue,
134    /// Source-side value.
135    pub theirs: ConflictValue,
136}
137
138/// Side selected without synthesizing new state.
139#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
140pub enum ConflictSide {
141    /// Common ancestor.
142    Base,
143    /// Target side.
144    Ours,
145    /// Source side.
146    Theirs,
147}
148
149/// Declarative resolution proposal. Validation remains filesystem-owned.
150#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
151#[non_exhaustive]
152pub enum MergeResolution {
153    /// Select one exact immutable side.
154    Select(ConflictSide),
155    /// Install synthesized UTF-8 content.
156    Text(String),
157    /// Install synthesized opaque content.
158    Binary(Vec<u8>),
159    /// Install canonical serialized metadata.
160    Metadata(Vec<u8>),
161    /// Install or remove one exact directory binding.
162    Binding(Option<FileId>),
163    /// Leave the conflict for a later caller or human.
164    Unresolved,
165}
166
167/// Content-addressed key for safe resolution reuse.
168#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
169#[serde(transparent)]
170pub struct ResolutionKey([u8; 32]);
171
172impl ResolutionKey {
173    /// Hashes exact merge inputs, conflict identity, and driver fingerprint.
174    #[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    /// Returns canonical key bytes.
198    #[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/// Whether a driver may be safely rerun after the merge plan changes.
257#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
258pub enum MergeDriverMode {
259    /// Equal immutable input always produces an equivalent result without effects.
260    Deterministic,
261    /// Resolution may consult an effectful service or require a human decision.
262    Effectful,
263}
264
265/// Pure merge driver operating without filesystem authority.
266pub trait MergeDriver: Send + Sync {
267    /// Stable implementation/version fingerprint used by resolution caching.
268    fn fingerprint(&self) -> &[u8];
269
270    /// Declares whether replanning may rerun this driver.
271    fn mode(&self) -> MergeDriverMode {
272        MergeDriverMode::Effectful
273    }
274
275    /// Resolves one complete immutable conflict.
276    fn resolve(&self, conflict: &ConflictView) -> Result<MergeResolution, DriverError>;
277}
278
279/// Immutable typed merge input, separate from resolution and publication.
280#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
281pub struct MergePlan {
282    /// Exact common ancestor.
283    pub base: GenerationId,
284    /// Exact target side.
285    pub ours: GenerationId,
286    /// Exact source side.
287    pub theirs: GenerationId,
288    /// Complete bounded conflict set.
289    pub conflicts: Vec<ConflictView>,
290    /// Whether additional conflicts exceeded the retained bound.
291    pub truncated: bool,
292}
293
294/// Validated declarations that still have no publication authority.
295#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
296pub struct UnpublishedMergeCandidate {
297    /// Immutable input plan.
298    pub plan: MergePlan,
299    /// Exact declarative resolutions by conflict identity.
300    pub resolutions: BTreeMap<ConflictKey, MergeResolution>,
301}
302
303/// Exact cache entry, including driver semantics for audit and recovery.
304#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
305pub struct CachedMergeResolution {
306    /// Driver execution semantics.
307    pub mode: MergeDriverMode,
308    /// Declarative result.
309    pub resolution: MergeResolution,
310}
311
312/// Resolution cache keyed by exact generations, conflict, and driver version.
313pub trait MergeResolutionCache {
314    /// Returns one exact cached resolution.
315    fn get(&self, key: ResolutionKey) -> Option<CachedMergeResolution>;
316
317    /// Stores one exact resolution.
318    fn insert(&mut self, key: ResolutionKey, resolution: CachedMergeResolution);
319}
320
321/// Process-local exact resolution cache.
322#[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/// Merge-plan resolution failure.
338#[derive(Clone, Debug, Error, Eq, PartialEq)]
339pub enum MergePlanResolutionError {
340    /// The immutable plan contains the same semantic conflict more than once.
341    #[error("merge plan contains duplicate conflict {0:?}")]
342    DuplicateConflict(ConflictKey),
343    /// No registered driver accepts one conflict.
344    #[error("no merge driver is registered for conflict {0:?}")]
345    MissingDriver(ConflictKey),
346    /// Driver execution failed.
347    #[error(transparent)]
348    Driver(#[from] DriverError),
349    /// Replanning invalidated an effectful or deferred decision.
350    #[error("effectful merge resolution became stale after replanning")]
351    StaleEffectful,
352}
353
354/// Resolves an immutable plan without granting drivers workspace authority.
355///
356/// Exact cache hits are reusable for every driver mode. During replanning,
357/// deterministic drivers may run again, while missing effectful results return
358/// stale and require an explicit caller decision.
359pub 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/// Merge-driver failure.
407#[derive(Clone, Debug, Error, Eq, PartialEq)]
408pub enum DriverError {
409    /// Driver does not accept this semantic kind.
410    #[error("merge driver does not support this conflict kind")]
411    Unsupported,
412    /// Driver rejected malformed or incomplete inputs.
413    #[error("merge driver received invalid conflict input: {0}")]
414    InvalidInput(String),
415    /// Driver-specific deterministic failure.
416    #[error("merge driver failed: {0}")]
417    Failed(String),
418}
419
420/// Git-attribute-like path rule selecting a registered driver.
421#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
422pub struct AttributeRule {
423    /// Portable glob pattern supporting exact paths, `*`, and `**`.
424    pub pattern: String,
425    /// Registered driver name.
426    pub driver: String,
427}
428
429/// Registry construction failure.
430#[derive(Clone, Debug, Error, Eq, PartialEq)]
431pub enum DriverRegistrationError {
432    /// Empty driver names are not stable configuration keys.
433    #[error("merge driver name cannot be empty")]
434    EmptyName,
435    /// A name was already registered.
436    #[error("merge driver '{0}' is already registered")]
437    Duplicate(String),
438    /// An attribute rule references an unknown driver.
439    #[error("attribute rule references unknown merge driver '{0}'")]
440    UnknownDriver(String),
441    /// One non-comment attribute line is not a supported `pattern merge=driver` rule.
442    #[error("invalid .gitattributes merge rule on line {0}")]
443    InvalidAttributeRule(usize),
444}
445
446/// Typed driver registry with ordered Git-style path rules.
447#[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    /// Creates an empty registry.
456    #[must_use]
457    pub fn new() -> Self {
458        Self::default()
459    }
460
461    /// Registers one named driver.
462    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    /// Sets ordered selection rules. Later matching rules win, like attributes.
479    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    /// Parses the supported Git-attribute subset and atomically replaces path rules.
490    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                // Acyclic owns only merge-driver selection. Ordinary Git attributes
509                // such as text, binary, eol, and linguist hints remain valid input
510                // but have no merge-registry effect.
511                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    /// Selects the fallback driver by registered name.
522    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    /// Selects a driver for a portable path.
532    #[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
560/// Conservative UTF-8 driver with diff3-style marker projection.
561pub 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(), &registry, &mut cache, false)
710            .expect("initial resolution");
711        assert_eq!(
712            resolve_merge_plan(plan, &registry, &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                &registry,
744                &mut MemoryMergeResolutionCache::default(),
745                false,
746            ),
747            Err(MergePlanResolutionError::DuplicateConflict(conflict.key))
748        );
749    }
750}