Skip to main content

acyclic_fs/kernel/
merge.rs

1//! Canonical bounded three-way generation merge.
2
3use super::{
4    CanonicalDecodeError, CheckpointError, CheckpointRequest, DecodeLimits, ExtentKind,
5    ExtentMutation, ExtentMutationError, ExtentMutationOptions, ExtentRangeRequest,
6    ExtentReadError, ExtentSlice, FilePayload, FileRecord, FileTableMutation,
7    FileTableMutationError, GenerationRoot, LogicalName, PersistentDiffError, TreeMutation,
8    TreeMutationError, apply_extent_mutations_async, apply_file_table_mutations_async,
9    apply_tree_mutations_async, build_checkpoint_async, diff_file_records_async,
10    diff_tree_entries_async, plan_extent_range_async,
11};
12use crate::async_storage::AsyncObjectStore;
13use crate::cancellation::{CancellationError, CancellationToken};
14use crate::foundation::{FileId, GenerationId};
15use crate::performance::{
16    MeasuredResult, OperationFailure, OperationReceipt, WorkBudget, WorkCounters, WorkError,
17};
18use crate::storage::{ByteRange, ObjectId, ObjectStoreError};
19use std::collections::{BTreeMap, BTreeSet};
20use thiserror::Error;
21
22/// One exact unresolved three-way merge region.
23#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
24pub enum MergeConflict {
25    /// Competing changes to one path-independent record or directory metadata.
26    File(FileId),
27    /// Competing changes to one exact name in a stable directory.
28    Binding {
29        /// Stable parent directory.
30        directory_id: FileId,
31        /// Exact conflicting component.
32        name: LogicalName,
33    },
34}
35
36/// Exact immutable side selected for one otherwise conflicting region.
37#[derive(Clone, Copy, Debug, Eq, PartialEq)]
38pub enum MergeConflictSide {
39    /// Common-ancestor value.
40    Base,
41    /// Target-side value.
42    Ours,
43    /// Source-side value.
44    Theirs,
45}
46
47/// Declarative resolution for one exact merge conflict.
48///
49/// Synthesized records and bindings are already-authenticated immutable
50/// inputs. The merge kernel validates their identity and shape before they can
51/// participate in the unpublished candidate.
52#[derive(Clone, Debug, Eq, PartialEq)]
53pub enum MergeConflictResolution {
54    /// Select one of the three immutable input sides.
55    Select(MergeConflictSide),
56    /// Install or remove a complete path-independent file record.
57    File(Option<FileRecord>),
58    /// Install or remove one exact directory binding.
59    Binding(Option<FileId>),
60}
61
62/// Immutable inputs for one bounded generation merge.
63#[derive(Clone, Debug, Eq, PartialEq)]
64pub struct MergeGenerationRequest {
65    /// Immutable common ancestor object.
66    pub base_generation: ObjectId,
67    /// Authenticated common ancestor root.
68    pub base: GenerationRoot,
69    /// Exact authenticated object for `ours`, when it is already immutable.
70    /// Omitting it creates the required parent checkpoint for a private candidate.
71    pub ours_generation: Option<ObjectId>,
72    /// Current sparse candidate rooted at `base_generation`.
73    pub ours: GenerationRoot,
74    /// Authenticated second-parent object.
75    pub theirs_generation: ObjectId,
76    /// Authenticated second-parent root.
77    pub theirs: GenerationRoot,
78    /// Whether the source generation is retained as a second parent.
79    pub retain_theirs_parent: bool,
80    /// Single total changed-record/name frontier bound.
81    pub maximum_changes: u32,
82    /// Maximum exact conflict regions retained in the terminal result.
83    pub maximum_conflicts: u32,
84    /// Caller-validated side selections for exact conflicting regions.
85    pub resolutions: BTreeMap<MergeConflict, MergeConflictResolution>,
86}
87
88/// Terminal result of canonical merge preparation.
89#[derive(Clone, Debug, Eq, PartialEq)]
90pub enum MergeGenerationOutcome {
91    /// Complete authenticated unpublished two-parent generation.
92    Prepared {
93        /// Candidate generation-root object.
94        generation_root: ObjectId,
95        /// Decoded candidate root without a redundant object-store reread.
96        root: GenerationRoot,
97        /// Candidate generation identity.
98        generation_id: GenerationId,
99    },
100    /// No candidate was produced.
101    Conflicted {
102        /// Stable conflicting regions.
103        conflicts: Vec<MergeConflict>,
104        /// Additional conflicts exceeded the supplied bound.
105        truncated: bool,
106    },
107}
108
109/// Canonical merge failures.
110#[derive(Debug, Error)]
111pub enum MergeGenerationError {
112    /// Merge roots or persistent diff structure are contradictory.
113    #[error("generation merge structure is invalid")]
114    InvalidDiff,
115    /// Caller supplied a zero or exceeded change frontier.
116    #[error("generation merge exceeds its change bound")]
117    ChangeLimit,
118    /// Bounded diff allocation failed.
119    #[error("generation merge diff allocation failed")]
120    AllocationFailed,
121    /// Immutable storage failed.
122    #[error(transparent)]
123    Object(#[from] ObjectStoreError),
124    /// Canonical page decoding failed.
125    #[error(transparent)]
126    Decode(#[from] CanonicalDecodeError),
127    /// Sparse file-table application failed.
128    #[error(transparent)]
129    FileTable(#[from] FileTableMutationError),
130    /// Sparse directory-tree application failed.
131    #[error(transparent)]
132    Tree(#[from] TreeMutationError),
133    /// Sparse extent planning failed.
134    #[error(transparent)]
135    ExtentRead(#[from] ExtentReadError),
136    /// Sparse extent path-copy application failed.
137    #[error(transparent)]
138    ExtentMutation(#[from] ExtentMutationError),
139    /// Checkpoint or two-parent construction failed.
140    #[error(transparent)]
141    Checkpoint(#[from] CheckpointError),
142    /// Operation was cancelled before safe completion.
143    #[error(transparent)]
144    Cancelled(#[from] CancellationError),
145    /// Exact work overflowed or exceeded its admitted budget.
146    #[error(transparent)]
147    Work(#[from] WorkError),
148}
149
150/// Receipt-bearing merge result.
151pub type MergeGenerationResult =
152    MeasuredResult<OperationReceipt<MergeGenerationOutcome>, MergeGenerationError>;
153
154/// Performs one sparse bounded three-way merge and constructs its two-parent checkpoint.
155///
156/// Equal Merkle roots are skipped by the shared diff kernels. The complete
157/// algorithm, including directory binding resolution and hard-link deltas, is
158/// owned here so every facade and language binding has identical semantics and
159/// physical work accounting.
160///
161/// # Errors
162///
163/// Rejects malformed or foreign roots, zero/truncated frontiers, cancellation,
164/// storage corruption, sparse application, checkpoint, or work-budget failures.
165#[allow(clippy::too_many_lines)]
166pub async fn merge_generation_async<S: AsyncObjectStore>(
167    store: &S,
168    request: MergeGenerationRequest,
169    limits: DecodeLimits,
170    budget: WorkBudget,
171    cancellation: &CancellationToken,
172) -> MergeGenerationResult {
173    cancellation
174        .check()
175        .map_err(|error| OperationFailure::before_work(error.into()))?;
176    if request.maximum_changes == 0 || request.maximum_conflicts == 0 {
177        return Err(OperationFailure::before_work(
178            MergeGenerationError::ChangeLimit,
179        ));
180    }
181    if request.base.volume_id != request.ours.volume_id
182        || request.base.volume_id != request.theirs.volume_id
183        || request.base.root_file_id != request.ours.root_file_id
184        || request.base.root_file_id != request.theirs.root_file_id
185    {
186        return Err(OperationFailure::before_work(
187            MergeGenerationError::InvalidDiff,
188        ));
189    }
190    let mut work = WorkCounters::default();
191    let ours = diff_file_records_async(
192        store,
193        Some(request.base.file_table),
194        Some(request.ours.file_table),
195        request.maximum_changes,
196        limits,
197        remaining(work, budget)?,
198        cancellation,
199    )
200    .await
201    .map_err(|failure| map_diff_failure(failure, work))?;
202    work = add(work, ours.work)?;
203    let theirs = diff_file_records_async(
204        store,
205        Some(request.base.file_table),
206        Some(request.theirs.file_table),
207        request.maximum_changes,
208        limits,
209        remaining(work, budget)?,
210        cancellation,
211    )
212    .await
213    .map_err(|failure| map_diff_failure(failure, work))?;
214    work = add(work, theirs.work)?;
215    if ours.truncated || theirs.truncated {
216        return Err(OperationFailure::new(
217            MergeGenerationError::ChangeLimit,
218            work,
219        ));
220    }
221    let ours = changes_by_file(ours.changes);
222    let theirs = changes_by_file(theirs.changes);
223    let identities = ours
224        .keys()
225        .chain(theirs.keys())
226        .copied()
227        .collect::<BTreeSet<_>>();
228    if identities.len() > usize::try_from(request.maximum_changes).unwrap_or(usize::MAX) {
229        return Err(OperationFailure::new(
230            MergeGenerationError::ChangeLimit,
231            work,
232        ));
233    }
234    let mut remaining_changes = request
235        .maximum_changes
236        .checked_sub(u32::try_from(identities.len()).unwrap_or(u32::MAX))
237        .ok_or_else(|| OperationFailure::new(MergeGenerationError::ChangeLimit, work))?;
238    let mut resolutions = BTreeMap::new();
239    let mut conflicts = Vec::new();
240    let mut truncated = false;
241    for file_id in identities {
242        let base = ours
243            .get(&file_id)
244            .map(|change| change.0)
245            .or_else(|| theirs.get(&file_id).map(|change| change.0))
246            .flatten();
247        let ours_value = ours.get(&file_id).map_or(base, |change| change.1);
248        let theirs_value = theirs.get(&file_id).map_or(base, |change| change.1);
249        let mut resolved = resolve_optional(base, ours_value, theirs_value);
250        if matches!(resolved, OptionalResolution::Conflict)
251            && is_directory(base)
252            && is_directory(ours_value)
253            && is_directory(theirs_value)
254        {
255            if remaining_changes == 0 {
256                return Err(OperationFailure::new(
257                    MergeGenerationError::ChangeLimit,
258                    work,
259                ));
260            }
261            let directory = merge_directory_record_with_resolutions_async(
262                store,
263                file_id,
264                base.ok_or_else(|| invalid(work))?,
265                ours_value.ok_or_else(|| invalid(work))?,
266                theirs_value.ok_or_else(|| invalid(work))?,
267                remaining_changes,
268                request
269                    .maximum_conflicts
270                    .saturating_sub(u32::try_from(conflicts.len()).unwrap_or(u32::MAX)),
271                &request.resolutions,
272                limits,
273                remaining(work, budget)?,
274                cancellation,
275            )
276            .await
277            .map_err(|failure| failure.map_with_prior_work(work, std::convert::identity))?;
278            work = add(work, directory.work)?;
279            remaining_changes = remaining_changes
280                .checked_sub(directory.value.examined_changes)
281                .ok_or_else(|| OperationFailure::new(MergeGenerationError::ChangeLimit, work))?;
282            conflicts.extend(directory.value.conflicts);
283            truncated |= directory.value.truncated;
284            let Some(record) = directory.value.record else {
285                continue;
286            };
287            resolved = OptionalResolution::Resolved(Some(record));
288        }
289        if matches!(resolved, OptionalResolution::Conflict)
290            && base.is_some()
291            && ours_value.is_some()
292            && theirs_value.is_some()
293            && is_extent_regular(base)
294            && is_extent_regular(ours_value)
295            && is_extent_regular(theirs_value)
296        {
297            let regular = merge_regular_record_async(
298                store,
299                base.ok_or_else(|| invalid(work))?,
300                ours_value.ok_or_else(|| invalid(work))?,
301                theirs_value.ok_or_else(|| invalid(work))?,
302                remaining_changes,
303                limits,
304                remaining(work, budget)?,
305                cancellation,
306            )
307            .await
308            .map_err(|failure| failure.map_with_prior_work(work, std::convert::identity))?;
309            work = add(work, regular.work)?;
310            if let Some((record, examined)) = regular.value {
311                remaining_changes = remaining_changes.checked_sub(examined).ok_or_else(|| {
312                    OperationFailure::new(MergeGenerationError::ChangeLimit, work)
313                })?;
314                resolved = OptionalResolution::Resolved(Some(record));
315            }
316        }
317        if matches!(resolved, OptionalResolution::Conflict)
318            && base.is_some()
319            && ours_value.is_some()
320            && theirs_value.is_some()
321        {
322            resolved = merge_file_fields(
323                base.ok_or_else(|| invalid(work))?,
324                ours_value.ok_or_else(|| invalid(work))?,
325                theirs_value.ok_or_else(|| invalid(work))?,
326            )
327            .map_or(OptionalResolution::Conflict, |record| {
328                OptionalResolution::Resolved(Some(record))
329            });
330        }
331        let OptionalResolution::Resolved(resolved) = resolved else {
332            if let Some(resolution) = request.resolutions.get(&MergeConflict::File(file_id)) {
333                let resolved = match resolution {
334                    MergeConflictResolution::Select(side) => {
335                        select_optional(*side, base, ours_value, theirs_value)
336                    }
337                    MergeConflictResolution::File(record) => {
338                        if record.is_some_and(|record| record.file_id != file_id) {
339                            return Err(invalid(work));
340                        }
341                        *record
342                    }
343                    MergeConflictResolution::Binding(_) => return Err(invalid(work)),
344                };
345                resolutions.insert(file_id, (ours_value, resolved));
346                continue;
347            }
348            if conflicts.len() < usize::try_from(request.maximum_conflicts).unwrap_or(usize::MAX) {
349                conflicts.push(MergeConflict::File(file_id));
350            } else {
351                truncated = true;
352            }
353            continue;
354        };
355        resolutions.insert(file_id, (ours_value, resolved));
356    }
357    if !conflicts.is_empty() || truncated {
358        return Ok(OperationReceipt {
359            value: MergeGenerationOutcome::Conflicted {
360                conflicts,
361                truncated,
362            },
363            work,
364        });
365    }
366    adjust_link_counts(
367        store,
368        &mut resolutions,
369        &mut remaining_changes,
370        limits,
371        budget,
372        cancellation,
373        &mut work,
374    )
375    .await?;
376    let mutations = resolutions
377        .into_iter()
378        .filter(|(_, (before, after))| before != after)
379        .filter_map(|(file_id, (before, after))| file_table_mutation(file_id, before, after))
380        .collect::<Vec<_>>();
381    let merged_table = if mutations.is_empty() {
382        request.ours.file_table
383    } else {
384        let applied = apply_file_table_mutations_async(
385            store,
386            request.ours.file_table,
387            mutations,
388            request.maximum_changes,
389            limits,
390            remaining(work, budget)?,
391            cancellation,
392        )
393        .await
394        .map_err(|failure| failure.map_with_prior_work(work, MergeGenerationError::FileTable))?;
395        work = add(work, applied.work)?;
396        applied.root
397    };
398    let (ours_parent_root, ours_parent_generation) = match request.ours_generation {
399        Some(object) if object.kind == crate::storage::ObjectKind::GenerationRoot => {
400            (object, GenerationId::new(object.digest))
401        }
402        Some(_) => {
403            return Err(OperationFailure::new(
404                MergeGenerationError::InvalidDiff,
405                work,
406            ));
407        }
408        None => {
409            let ours_parent = build_checkpoint_async(
410                store,
411                CheckpointRequest {
412                    base: request.base_generation,
413                    file_table: request.ours.file_table,
414                    merge_parent: None,
415                },
416                limits,
417                remaining(work, budget)?,
418                cancellation,
419            )
420            .await
421            .map_err(|failure| {
422                failure.map_with_prior_work(work, MergeGenerationError::Checkpoint)
423            })?;
424            work = add(work, ours_parent.work)?;
425            (ours_parent.root, ours_parent.generation_id)
426        }
427    };
428    let merged = build_checkpoint_async(
429        store,
430        CheckpointRequest {
431            base: ours_parent_root,
432            file_table: merged_table,
433            merge_parent: request
434                .retain_theirs_parent
435                .then_some(request.theirs_generation),
436        },
437        limits,
438        remaining(work, budget)?,
439        cancellation,
440    )
441    .await
442    .map_err(|failure| failure.map_with_prior_work(work, MergeGenerationError::Checkpoint))?;
443    work = add(work, merged.work)?;
444    let root = GenerationRoot {
445        volume_id: request.ours.volume_id,
446        root_file_id: request.ours.root_file_id,
447        file_table: merged_table,
448        parents: if request.retain_theirs_parent {
449            vec![
450                ours_parent_generation,
451                GenerationId::new(request.theirs_generation.digest),
452            ]
453        } else {
454            vec![ours_parent_generation]
455        },
456        required_features: request.ours.required_features,
457    };
458    Ok(OperationReceipt {
459        value: MergeGenerationOutcome::Prepared {
460            generation_root: merged.root,
461            root,
462            generation_id: merged.generation_id,
463        },
464        work,
465    })
466}
467
468fn is_extent_regular(record: Option<FileRecord>) -> bool {
469    matches!(
470        record.map(|value| value.payload),
471        Some(FilePayload::Regular { .. })
472    )
473}
474
475#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
476async fn merge_regular_record_async<S: AsyncObjectStore>(
477    store: &S,
478    base: FileRecord,
479    ours: FileRecord,
480    theirs: FileRecord,
481    maximum_changes: u32,
482    limits: DecodeLimits,
483    budget: WorkBudget,
484    cancellation: &CancellationToken,
485) -> Result<OperationReceipt<Option<(FileRecord, u32)>>, OperationFailure<MergeGenerationError>> {
486    let Some(kind) = resolve_three(&base.kind, &ours.kind, &theirs.kind) else {
487        return Ok(OperationReceipt {
488            value: None,
489            work: WorkCounters::default(),
490        });
491    };
492    let Some(metadata) = resolve_three(&base.metadata, &ours.metadata, &theirs.metadata) else {
493        return Ok(OperationReceipt {
494            value: None,
495            work: WorkCounters::default(),
496        });
497    };
498    let (
499        FilePayload::Regular {
500            logical_bytes: base_bytes,
501            extents: base_root,
502        },
503        FilePayload::Regular {
504            logical_bytes: ours_bytes,
505            extents: ours_root,
506        },
507        FilePayload::Regular {
508            logical_bytes: theirs_bytes,
509            extents: theirs_root,
510        },
511    ) = (base.payload, ours.payload, theirs.payload)
512    else {
513        return Ok(OperationReceipt {
514            value: None,
515            work: WorkCounters::default(),
516        });
517    };
518    let Some(logical_bytes) = resolve_three(&base_bytes, &ours_bytes, &theirs_bytes) else {
519        return Ok(OperationReceipt {
520            value: None,
521            work: WorkCounters::default(),
522        });
523    };
524    if base_bytes != ours_bytes || base_bytes != theirs_bytes || logical_bytes == 0 {
525        return Ok(OperationReceipt {
526            value: None,
527            work: WorkCounters::default(),
528        });
529    }
530    let request = |root| ExtentRangeRequest {
531        root,
532        file_size: logical_bytes,
533        range: ByteRange {
534            offset: 0,
535            length: logical_bytes,
536        },
537        maximum_spans: maximum_changes,
538        limits,
539        budget,
540    };
541    let mut work = WorkCounters::default();
542    let base_plan = plan_extent_range_async(store, request(base_root), cancellation)
543        .await
544        .map_err(|failure| failure.map_with_prior_work(work, MergeGenerationError::ExtentRead))?;
545    work = add(work, base_plan.work)?;
546    let ours_plan = plan_extent_range_async(
547        store,
548        ExtentRangeRequest {
549            budget: remaining(work, budget)?,
550            ..request(ours_root)
551        },
552        cancellation,
553    )
554    .await
555    .map_err(|failure| failure.map_with_prior_work(work, MergeGenerationError::ExtentRead))?;
556    work = add(work, ours_plan.work)?;
557    let theirs_plan = plan_extent_range_async(
558        store,
559        ExtentRangeRequest {
560            budget: remaining(work, budget)?,
561            ..request(theirs_root)
562        },
563        cancellation,
564    )
565    .await
566    .map_err(|failure| failure.map_with_prior_work(work, MergeGenerationError::ExtentRead))?;
567    work = add(work, theirs_plan.work)?;
568    let mut indices = [0_usize; 3];
569    let plans = [&base_plan.spans, &ours_plan.spans, &theirs_plan.spans];
570    let mut offset = 0_u64;
571    let mut mutations = Vec::new();
572    while offset < logical_bytes {
573        let spans = [
574            plans[0].get(indices[0]).ok_or_else(|| invalid(work))?,
575            plans[1].get(indices[1]).ok_or_else(|| invalid(work))?,
576            plans[2].get(indices[2]).ok_or_else(|| invalid(work))?,
577        ];
578        let end = spans
579            .iter()
580            .map(|span| span.offset.saturating_add(span.length))
581            .min()
582            .ok_or_else(|| invalid(work))?;
583        if end <= offset {
584            return Err(invalid(work));
585        }
586        let kinds = [
587            sliced_extent_kind(spans[0], offset)?,
588            sliced_extent_kind(spans[1], offset)?,
589            sliced_extent_kind(spans[2], offset)?,
590        ];
591        let Some(resolved) = resolve_three(&kinds[0], &kinds[1], &kinds[2]) else {
592            return Ok(OperationReceipt { value: None, work });
593        };
594        if resolved != kinds[1] {
595            mutations.push(ExtentMutation::Replace {
596                offset,
597                length: end - offset,
598                kind: resolved,
599                extend: false,
600            });
601            if mutations.len() > usize::try_from(maximum_changes).unwrap_or(usize::MAX) {
602                return Err(OperationFailure::new(
603                    MergeGenerationError::ChangeLimit,
604                    work,
605                ));
606            }
607        }
608        offset = end;
609        // `spans[index]` above is exactly `plans[index][indices[index]]`, fetched
610        // moments earlier in this same loop iteration and not mutated since, so
611        // reusing it here avoids re-indexing `plans`/`indices` by a loop variable.
612        for (index_slot, span) in indices.iter_mut().zip(spans.iter()) {
613            if span.offset + span.length == end {
614                *index_slot += 1;
615            }
616        }
617    }
618    let root = if mutations.is_empty() {
619        ours_root
620    } else {
621        let applied = apply_extent_mutations_async(
622            store,
623            ours_root,
624            ours_bytes,
625            &mutations,
626            ExtentMutationOptions {
627                maximum_mutations: maximum_changes,
628                limits,
629                budget: remaining(work, budget)?,
630            },
631            cancellation,
632        )
633        .await
634        .map_err(|failure| {
635            failure.map_with_prior_work(work, MergeGenerationError::ExtentMutation)
636        })?;
637        work = add(work, applied.work)?;
638        applied.root
639    };
640    Ok(OperationReceipt {
641        value: Some((
642            FileRecord {
643                file_id: ours.file_id,
644                kind,
645                link_count: ours.link_count,
646                metadata,
647                payload: FilePayload::Regular {
648                    logical_bytes,
649                    extents: root,
650                },
651            },
652            u32::try_from(mutations.len()).unwrap_or(u32::MAX),
653        )),
654        work,
655    })
656}
657
658fn sliced_extent_kind(
659    span: &ExtentSlice,
660    offset: u64,
661) -> Result<ExtentKind, OperationFailure<MergeGenerationError>> {
662    let delta = offset
663        .checked_sub(span.offset)
664        .ok_or_else(|| OperationFailure::before_work(MergeGenerationError::InvalidDiff))?;
665    Ok(match span.kind {
666        ExtentKind::Hole => ExtentKind::Hole,
667        ExtentKind::AllocatedZero => ExtentKind::AllocatedZero,
668        ExtentKind::Content {
669            object,
670            object_offset,
671        } => ExtentKind::Content {
672            object,
673            object_offset: object_offset
674                .checked_add(delta)
675                .ok_or_else(|| OperationFailure::before_work(MergeGenerationError::InvalidDiff))?,
676        },
677    })
678}
679
680type RecordChange = (Option<FileRecord>, Option<FileRecord>);
681
682fn changes_by_file(
683    changes: Vec<super::persistent_diff::ValueChange<FileId, FileRecord>>,
684) -> BTreeMap<FileId, RecordChange> {
685    changes
686        .into_iter()
687        .map(|change| (change.key, (change.before, change.after)))
688        .collect()
689}
690
691async fn adjust_link_counts<S: AsyncObjectStore>(
692    store: &S,
693    resolutions: &mut BTreeMap<FileId, RecordChange>,
694    remaining_changes: &mut u32,
695    limits: DecodeLimits,
696    budget: WorkBudget,
697    cancellation: &CancellationToken,
698    work: &mut WorkCounters,
699) -> Result<(), OperationFailure<MergeGenerationError>> {
700    // A selected parent binding can leave an introduced directory unbound.
701    // Its children must not contribute links to the candidate. Cache each
702    // exact directory transition so pruning a nested orphan only pays for the
703    // transitions that actually change, not for every prior sibling.
704    let mut transitions =
705        BTreeMap::<FileId, (Option<ObjectId>, Option<ObjectId>, BTreeMap<FileId, i64>)>::new();
706    loop {
707        cancellation.check().map_err(|error| {
708            OperationFailure::new(MergeGenerationError::Cancelled(error), *work)
709        })?;
710        let mut deltas = BTreeMap::<FileId, i64>::new();
711        for (directory_id, (before, after)) in resolutions.iter() {
712            let before_entries = directory_entries(*before);
713            let after_entries = directory_entries(*after);
714            if before_entries == after_entries {
715                // Pruning an unbound directory removes its earlier contribution.
716                // Retaining that cached transition would keep its children alive
717                // without a binding from the root.
718                transitions.remove(directory_id);
719                continue;
720            }
721            if transitions
722                .get(directory_id)
723                .is_none_or(|(cached_before, cached_after, _)| {
724                    *cached_before != before_entries || *cached_after != after_entries
725                })
726            {
727                if *remaining_changes == 0 {
728                    return Err(OperationFailure::new(
729                        MergeGenerationError::ChangeLimit,
730                        *work,
731                    ));
732                }
733                let entries = diff_tree_entries_async(
734                    store,
735                    before_entries,
736                    after_entries,
737                    *remaining_changes,
738                    limits,
739                    remaining(*work, budget)?,
740                    cancellation,
741                )
742                .await
743                .map_err(|failure| map_diff_failure(failure, *work))?;
744                *work = add(*work, entries.work)?;
745                if entries.truncated {
746                    return Err(OperationFailure::new(
747                        MergeGenerationError::ChangeLimit,
748                        *work,
749                    ));
750                }
751                *remaining_changes = remaining_changes
752                    .checked_sub(u32::try_from(entries.changes.len()).unwrap_or(u32::MAX))
753                    .ok_or_else(|| {
754                        OperationFailure::new(MergeGenerationError::ChangeLimit, *work)
755                    })?;
756                let mut contribution = BTreeMap::<FileId, i64>::new();
757                for binding in entries.changes {
758                    if let Some(before) = binding.before {
759                        *contribution.entry(before.file_id).or_default() -= 1;
760                    }
761                    if let Some(after) = binding.after {
762                        *contribution.entry(after.file_id).or_default() += 1;
763                    }
764                }
765                transitions.insert(*directory_id, (before_entries, after_entries, contribution));
766            }
767            if let Some((_, _, contribution)) = transitions.get(directory_id) {
768                for (file_id, delta) in contribution {
769                    *deltas.entry(*file_id).or_default() += delta;
770                }
771            }
772        }
773        if deltas
774            .iter()
775            .any(|(file_id, delta)| *delta != 0 && !resolutions.contains_key(file_id))
776        {
777            return Err(invalid(*work));
778        }
779        let mut pruned_directory = false;
780        for (file_id, (ours, resolved)) in resolutions.iter_mut() {
781            let delta = deltas.get(file_id).copied().unwrap_or(0);
782            let links = i128::from(ours.map_or(0, |record| record.link_count))
783                .checked_add(i128::from(delta))
784                .and_then(|value| u64::try_from(value).ok())
785                .ok_or_else(|| invalid(*work))?;
786            if resolved.is_none() {
787                if links != 0 {
788                    return Err(invalid(*work));
789                }
790                continue;
791            }
792            if links == 0 {
793                pruned_directory |= is_directory(*resolved);
794                *resolved = None;
795            } else if !pruned_directory && let Some(record) = resolved {
796                record.link_count = links;
797            }
798        }
799        if !pruned_directory {
800            return Ok(());
801        }
802    }
803}
804
805pub(crate) struct DirectoryMergeResult {
806    pub(crate) record: Option<FileRecord>,
807    pub(crate) conflicts: Vec<MergeConflict>,
808    pub(crate) truncated: bool,
809    pub(crate) examined_changes: u32,
810}
811
812#[allow(clippy::too_many_arguments)]
813#[cfg(test)]
814pub(crate) async fn merge_directory_record_async<S: AsyncObjectStore>(
815    store: &S,
816    directory_id: FileId,
817    base: FileRecord,
818    ours: FileRecord,
819    theirs: FileRecord,
820    maximum_changes: u32,
821    maximum_conflicts: u32,
822    limits: DecodeLimits,
823    budget: WorkBudget,
824    cancellation: &CancellationToken,
825) -> Result<OperationReceipt<DirectoryMergeResult>, OperationFailure<MergeGenerationError>> {
826    merge_directory_record_with_resolutions_async(
827        store,
828        directory_id,
829        base,
830        ours,
831        theirs,
832        maximum_changes,
833        maximum_conflicts,
834        &BTreeMap::new(),
835        limits,
836        budget,
837        cancellation,
838    )
839    .await
840}
841
842#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
843async fn merge_directory_record_with_resolutions_async<S: AsyncObjectStore>(
844    store: &S,
845    directory_id: FileId,
846    base: FileRecord,
847    ours: FileRecord,
848    theirs: FileRecord,
849    maximum_changes: u32,
850    maximum_conflicts: u32,
851    resolutions: &BTreeMap<MergeConflict, MergeConflictResolution>,
852    limits: DecodeLimits,
853    budget: WorkBudget,
854    cancellation: &CancellationToken,
855) -> Result<OperationReceipt<DirectoryMergeResult>, OperationFailure<MergeGenerationError>> {
856    let mut work = WorkCounters::default();
857    let conflict = || DirectoryMergeResult {
858        record: None,
859        conflicts: (maximum_conflicts != 0)
860            .then_some(MergeConflict::File(directory_id))
861            .into_iter()
862            .collect(),
863        truncated: maximum_conflicts == 0,
864        examined_changes: 0,
865    };
866    let Some(metadata) = resolve_three(&base.metadata, &ours.metadata, &theirs.metadata) else {
867        return Ok(OperationReceipt {
868            value: conflict(),
869            work,
870        });
871    };
872    let Some(link_count) = resolve_three(&base.link_count, &ours.link_count, &theirs.link_count)
873    else {
874        return Ok(OperationReceipt {
875            value: conflict(),
876            work,
877        });
878    };
879    let (Some(base_entries), Some(ours_entries), Some(theirs_entries)) = (
880        directory_entries(Some(base)),
881        directory_entries(Some(ours)),
882        directory_entries(Some(theirs)),
883    ) else {
884        return Err(OperationFailure::before_work(
885            MergeGenerationError::InvalidDiff,
886        ));
887    };
888    let ours_diff = diff_tree_entries_async(
889        store,
890        Some(base_entries),
891        Some(ours_entries),
892        maximum_changes,
893        limits,
894        remaining(work, budget)?,
895        cancellation,
896    )
897    .await
898    .map_err(|failure| map_diff_failure(failure, work))?;
899    work = add(work, ours_diff.work)?;
900    let theirs_diff = diff_tree_entries_async(
901        store,
902        Some(base_entries),
903        Some(theirs_entries),
904        maximum_changes,
905        limits,
906        remaining(work, budget)?,
907        cancellation,
908    )
909    .await
910    .map_err(|failure| map_diff_failure(failure, work))?;
911    work = add(work, theirs_diff.work)?;
912    if ours_diff.truncated || theirs_diff.truncated {
913        return Err(OperationFailure::new(
914            MergeGenerationError::ChangeLimit,
915            work,
916        ));
917    }
918    let ours_changes = ours_diff
919        .changes
920        .into_iter()
921        .map(|change| (change.key, (change.before, change.after)))
922        .collect::<BTreeMap<_, _>>();
923    let theirs_changes = theirs_diff
924        .changes
925        .into_iter()
926        .map(|change| (change.key, (change.before, change.after)))
927        .collect::<BTreeMap<_, _>>();
928    let names = ours_changes
929        .keys()
930        .chain(theirs_changes.keys())
931        .cloned()
932        .collect::<BTreeSet<_>>();
933    if names.len() > usize::try_from(maximum_changes).unwrap_or(usize::MAX) {
934        return Err(OperationFailure::new(
935            MergeGenerationError::ChangeLimit,
936            work,
937        ));
938    }
939    let examined_changes = u32::try_from(names.len()).unwrap_or(u32::MAX);
940    let mut conflicts = Vec::new();
941    let mut truncated = false;
942    let mut mutations = Vec::new();
943    for name in names {
944        let base_value = ours_changes
945            .get(&name)
946            .map(|change| change.0.clone())
947            .or_else(|| theirs_changes.get(&name).map(|change| change.0.clone()))
948            .flatten();
949        let ours_value = ours_changes
950            .get(&name)
951            .map_or_else(|| base_value.clone(), |change| change.1.clone());
952        let theirs_value = theirs_changes
953            .get(&name)
954            .map_or_else(|| base_value.clone(), |change| change.1.clone());
955        let resolved = resolve_three(&base_value, &ours_value, &theirs_value).or_else(|| {
956            resolutions
957                .get(&MergeConflict::Binding {
958                    directory_id,
959                    name: name.clone(),
960                })
961                .and_then(|resolution| match resolution {
962                    MergeConflictResolution::Select(side) => {
963                        Some(select_value(*side, &base_value, &ours_value, &theirs_value))
964                    }
965                    MergeConflictResolution::Binding(value) => {
966                        [base_value.clone(), ours_value.clone(), theirs_value.clone()]
967                            .into_iter()
968                            .find(|entry| entry.as_ref().map(|entry| entry.file_id) == *value)
969                    }
970                    MergeConflictResolution::File(_) => None,
971                })
972        });
973        let Some(resolved) = resolved else {
974            if conflicts.len() < usize::try_from(maximum_conflicts).unwrap_or(usize::MAX) {
975                conflicts.push(MergeConflict::Binding { directory_id, name });
976            } else {
977                truncated = true;
978            }
979            continue;
980        };
981        if resolved != ours_value
982            && let Some(mutation) = tree_mutation(name, ours_value, resolved)
983        {
984            mutations.push(mutation);
985        }
986    }
987    if !conflicts.is_empty() || truncated {
988        return Ok(OperationReceipt {
989            value: DirectoryMergeResult {
990                record: None,
991                conflicts,
992                truncated,
993                examined_changes,
994            },
995            work,
996        });
997    }
998    let entries = if mutations.is_empty() {
999        ours_entries
1000    } else {
1001        let applied = apply_tree_mutations_async(
1002            store,
1003            ours_entries,
1004            mutations,
1005            maximum_changes,
1006            limits,
1007            remaining(work, budget)?,
1008            cancellation,
1009        )
1010        .await
1011        .map_err(|failure| failure.map_with_prior_work(work, MergeGenerationError::Tree))?;
1012        work = add(work, applied.work)?;
1013        applied.root
1014    };
1015    Ok(OperationReceipt {
1016        value: DirectoryMergeResult {
1017            record: Some(FileRecord {
1018                file_id: directory_id,
1019                kind: super::FileKind::Directory,
1020                link_count,
1021                metadata,
1022                payload: FilePayload::Directory { entries },
1023            }),
1024            conflicts,
1025            truncated,
1026            examined_changes,
1027        },
1028        work,
1029    })
1030}
1031
1032enum OptionalResolution {
1033    Resolved(Option<FileRecord>),
1034    Conflict,
1035}
1036
1037fn resolve_optional(
1038    base: Option<FileRecord>,
1039    ours: Option<FileRecord>,
1040    theirs: Option<FileRecord>,
1041) -> OptionalResolution {
1042    if ours == theirs || theirs == base {
1043        OptionalResolution::Resolved(ours)
1044    } else if ours == base {
1045        OptionalResolution::Resolved(theirs)
1046    } else {
1047        OptionalResolution::Conflict
1048    }
1049}
1050
1051fn select_optional(
1052    side: MergeConflictSide,
1053    base: Option<FileRecord>,
1054    ours: Option<FileRecord>,
1055    theirs: Option<FileRecord>,
1056) -> Option<FileRecord> {
1057    match side {
1058        MergeConflictSide::Base => base,
1059        MergeConflictSide::Ours => ours,
1060        MergeConflictSide::Theirs => theirs,
1061    }
1062}
1063
1064fn select_value<T: Clone>(side: MergeConflictSide, base: &T, ours: &T, theirs: &T) -> T {
1065    match side {
1066        MergeConflictSide::Base => base.clone(),
1067        MergeConflictSide::Ours => ours.clone(),
1068        MergeConflictSide::Theirs => theirs.clone(),
1069    }
1070}
1071
1072pub(crate) fn resolve_three<T: Clone + Eq>(base: &T, ours: &T, theirs: &T) -> Option<T> {
1073    if ours == theirs || theirs == base {
1074        Some(ours.clone())
1075    } else if ours == base {
1076        Some(theirs.clone())
1077    } else {
1078        None
1079    }
1080}
1081
1082pub(crate) fn merge_file_fields(
1083    base: FileRecord,
1084    ours: FileRecord,
1085    theirs: FileRecord,
1086) -> Option<FileRecord> {
1087    Some(FileRecord {
1088        file_id: ours.file_id,
1089        kind: resolve_three(&base.kind, &ours.kind, &theirs.kind)?,
1090        link_count: ours.link_count,
1091        metadata: resolve_three(&base.metadata, &ours.metadata, &theirs.metadata)?,
1092        payload: resolve_three(&base.payload, &ours.payload, &theirs.payload)?,
1093    })
1094}
1095
1096fn is_directory(record: Option<FileRecord>) -> bool {
1097    matches!(record, Some(record) if record.kind == super::FileKind::Directory)
1098}
1099fn directory_entries(record: Option<FileRecord>) -> Option<ObjectId> {
1100    match record?.payload {
1101        FilePayload::Directory { entries } => Some(entries),
1102        _ => None,
1103    }
1104}
1105pub(crate) fn file_table_mutation(
1106    file_id: FileId,
1107    before: Option<FileRecord>,
1108    after: Option<FileRecord>,
1109) -> Option<FileTableMutation> {
1110    match (before, after) {
1111        (None, Some(record)) => Some(FileTableMutation::Insert(record)),
1112        (Some(expected), None) => Some(FileTableMutation::Remove {
1113            file_id,
1114            expected: Some(expected),
1115        }),
1116        (Some(expected), Some(replacement)) => Some(FileTableMutation::Replace {
1117            expected,
1118            replacement,
1119        }),
1120        (None, None) => None,
1121    }
1122}
1123pub(crate) fn tree_mutation(
1124    name: LogicalName,
1125    before: Option<super::TreeEntry>,
1126    after: Option<super::TreeEntry>,
1127) -> Option<TreeMutation> {
1128    match (before, after) {
1129        (None, Some(entry)) => Some(TreeMutation::Insert(entry)),
1130        (Some(expected), None) => Some(TreeMutation::Remove {
1131            name,
1132            expected_file_id: Some(expected.file_id),
1133        }),
1134        (Some(expected), Some(entry)) => Some(TreeMutation::Replace {
1135            entry,
1136            expected_file_id: expected.file_id,
1137        }),
1138        (None, None) => None,
1139    }
1140}
1141fn invalid(work: WorkCounters) -> OperationFailure<MergeGenerationError> {
1142    OperationFailure::new(MergeGenerationError::InvalidDiff, work)
1143}
1144fn remaining(
1145    work: WorkCounters,
1146    budget: WorkBudget,
1147) -> Result<WorkBudget, OperationFailure<MergeGenerationError>> {
1148    work.remaining(budget)
1149        .map_err(|error| OperationFailure::new(error.into(), work))
1150}
1151fn add(
1152    prior: WorkCounters,
1153    next: WorkCounters,
1154) -> Result<WorkCounters, OperationFailure<MergeGenerationError>> {
1155    prior
1156        .checked_add(next)
1157        .map_err(|error| OperationFailure::new(error.into(), prior))
1158}
1159fn map_diff_failure(
1160    failure: OperationFailure<PersistentDiffError>,
1161    prior: WorkCounters,
1162) -> OperationFailure<MergeGenerationError> {
1163    failure.map_with_prior_work(prior, |error| match error {
1164        PersistentDiffError::WrongRootKind | PersistentDiffError::InvalidLimit => {
1165            MergeGenerationError::InvalidDiff
1166        }
1167        PersistentDiffError::AllocationFailed => MergeGenerationError::AllocationFailed,
1168        PersistentDiffError::Storage(error) => MergeGenerationError::Object(error),
1169        PersistentDiffError::Decode(error) => MergeGenerationError::Decode(error),
1170        PersistentDiffError::Work(error) => MergeGenerationError::Work(error),
1171        PersistentDiffError::Cancelled => MergeGenerationError::Cancelled(CancellationError),
1172    })
1173}
1174
1175#[cfg(test)]
1176#[path = "tests/merge.rs"]
1177mod tests;