1use 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#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
24pub enum MergeConflict {
25 File(FileId),
27 Binding {
29 directory_id: FileId,
31 name: LogicalName,
33 },
34}
35
36#[derive(Clone, Copy, Debug, Eq, PartialEq)]
38pub enum MergeConflictSide {
39 Base,
41 Ours,
43 Theirs,
45}
46
47#[derive(Clone, Debug, Eq, PartialEq)]
53pub enum MergeConflictResolution {
54 Select(MergeConflictSide),
56 File(Option<FileRecord>),
58 Binding(Option<FileId>),
60}
61
62#[derive(Clone, Debug, Eq, PartialEq)]
64pub struct MergeGenerationRequest {
65 pub base_generation: ObjectId,
67 pub base: GenerationRoot,
69 pub ours_generation: Option<ObjectId>,
72 pub ours: GenerationRoot,
74 pub theirs_generation: ObjectId,
76 pub theirs: GenerationRoot,
78 pub retain_theirs_parent: bool,
80 pub maximum_changes: u32,
82 pub maximum_conflicts: u32,
84 pub resolutions: BTreeMap<MergeConflict, MergeConflictResolution>,
86}
87
88#[derive(Clone, Debug, Eq, PartialEq)]
90pub enum MergeGenerationOutcome {
91 Prepared {
93 generation_root: ObjectId,
95 root: GenerationRoot,
97 generation_id: GenerationId,
99 },
100 Conflicted {
102 conflicts: Vec<MergeConflict>,
104 truncated: bool,
106 },
107}
108
109#[derive(Debug, Error)]
111pub enum MergeGenerationError {
112 #[error("generation merge structure is invalid")]
114 InvalidDiff,
115 #[error("generation merge exceeds its change bound")]
117 ChangeLimit,
118 #[error("generation merge diff allocation failed")]
120 AllocationFailed,
121 #[error(transparent)]
123 Object(#[from] ObjectStoreError),
124 #[error(transparent)]
126 Decode(#[from] CanonicalDecodeError),
127 #[error(transparent)]
129 FileTable(#[from] FileTableMutationError),
130 #[error(transparent)]
132 Tree(#[from] TreeMutationError),
133 #[error(transparent)]
135 ExtentRead(#[from] ExtentReadError),
136 #[error(transparent)]
138 ExtentMutation(#[from] ExtentMutationError),
139 #[error(transparent)]
141 Checkpoint(#[from] CheckpointError),
142 #[error(transparent)]
144 Cancelled(#[from] CancellationError),
145 #[error(transparent)]
147 Work(#[from] WorkError),
148}
149
150pub type MergeGenerationResult =
152 MeasuredResult<OperationReceipt<MergeGenerationOutcome>, MergeGenerationError>;
153
154#[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 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 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 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;