1use crate::foundation::{FileId, GenerationId, OperationId, VolumeId};
8use crate::kernel::{
9 ExtentKind, FileKind, FileMetadata, FilePayload, LogicalName, MetadataField, NamespacePath,
10};
11use crate::model::{CheckoutMode, GenerationSelector};
12use crate::path::PortablePath;
13use crate::{
14 AsyncAuthorityStore, AsyncObjectStore, AuthoredMutation, Checkout, CheckoutCommitOutcome,
15 ConflictKey, ConflictKind, ConflictSide, ConflictValue, FsError, GenerationDiff, MergeConflict,
16 MergeDriverRegistry, MergePlan, MergePlanResolutionError, MergeResolution,
17 MergeResolutionCache, UnpublishedMergeCandidate, Volume, resolve_merge_plan,
18};
19use bytes::Bytes;
20use serde::{Deserialize, Serialize};
21use std::collections::{BTreeMap, BTreeSet};
22use std::fmt;
23use std::sync::Arc;
24use thiserror::Error;
25use unicode_normalization::UnicodeNormalization;
26
27const WORKSPACE_ID_DOMAIN: &[u8] = b"acyclic-fs-workspace-id-v1\0";
28const MAXIMUM_WORKSPACE_NAME_BYTES: usize = 255;
29
30#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
35pub struct WorkspaceName(Arc<str>);
36
37impl WorkspaceName {
38 pub fn new(name: impl AsRef<str>) -> Result<Self, WorkspaceNameError> {
45 let canonical: String = name.as_ref().nfc().collect();
46 if canonical.is_empty() {
47 return Err(WorkspaceNameError::Empty);
48 }
49 if canonical == "." || canonical == ".." {
50 return Err(WorkspaceNameError::Reserved);
51 }
52 if canonical.len() > MAXIMUM_WORKSPACE_NAME_BYTES {
53 return Err(WorkspaceNameError::TooLong);
54 }
55 if canonical
56 .chars()
57 .any(|character| character.is_control() || matches!(character, '/' | '\\'))
58 {
59 return Err(WorkspaceNameError::InvalidCharacter);
60 }
61 Ok(Self(Arc::from(canonical)))
62 }
63
64 #[must_use]
66 pub fn as_str(&self) -> &str {
67 &self.0
68 }
69}
70
71impl fmt::Debug for WorkspaceName {
72 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
73 formatter
74 .debug_tuple("WorkspaceName")
75 .field(&self.0)
76 .finish()
77 }
78}
79
80impl fmt::Display for WorkspaceName {
81 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
82 formatter.write_str(self.as_str())
83 }
84}
85
86#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
88#[serde(transparent)]
89pub struct WorkspaceId([u8; 16]);
90
91impl WorkspaceId {
92 pub(crate) const fn from_volume_id(volume_id: VolumeId) -> Self {
93 Self(volume_id.into_bytes())
94 }
95
96 #[must_use]
98 pub const fn from_bytes(bytes: [u8; 16]) -> Self {
99 Self(bytes)
100 }
101
102 #[must_use]
106 pub(crate) fn derive(namespace: [u8; 16], name: &WorkspaceName) -> Self {
107 let mut hasher = blake3::Hasher::new();
108 hasher.update(WORKSPACE_ID_DOMAIN);
109 hasher.update(&namespace);
110 hasher.update(
111 &u64::try_from(name.as_str().len())
112 .unwrap_or(u64::MAX)
113 .to_le_bytes(),
114 );
115 hasher.update(name.as_str().as_bytes());
116 let mut bytes = [0_u8; 16];
117 bytes.copy_from_slice(&hasher.finalize().as_bytes()[..16]);
118 Self(bytes)
119 }
120
121 #[must_use]
123 pub const fn into_bytes(self) -> [u8; 16] {
124 self.0
125 }
126
127 pub(crate) const fn volume_id(self) -> VolumeId {
128 VolumeId::from_bytes(self.0)
129 }
130}
131
132#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
134#[serde(transparent)]
135pub struct IdempotencyKey([u8; 16]);
136
137impl IdempotencyKey {
138 #[must_use]
140 pub fn new() -> Self {
141 Self(OperationId::new().into_bytes())
142 }
143
144 #[must_use]
146 pub const fn from_bytes(bytes: [u8; 16]) -> Self {
147 Self(bytes)
148 }
149
150 #[must_use]
152 pub const fn into_bytes(self) -> [u8; 16] {
153 self.0
154 }
155
156 pub(crate) const fn operation_id(self) -> OperationId {
157 OperationId::from_bytes(self.0)
158 }
159}
160
161impl Default for IdempotencyKey {
162 fn default() -> Self {
163 Self::new()
164 }
165}
166
167pub struct Workspace<A, O> {
169 pub(crate) name: WorkspaceName,
170 pub(crate) id: WorkspaceId,
171 pub(crate) volume: Volume<A, O>,
172 #[cfg(all(feature = "native-watch", not(target_arch = "wasm32")))]
173 pub(crate) source: Option<crate::Source<A, O>>,
174}
175
176impl<A, O> Clone for Workspace<A, O> {
177 fn clone(&self) -> Self {
178 Self {
179 name: self.name.clone(),
180 id: self.id,
181 volume: self.volume.clone(),
182 #[cfg(all(feature = "native-watch", not(target_arch = "wasm32")))]
183 source: self.source.clone(),
184 }
185 }
186}
187
188impl<A, O> Workspace<A, O> {
189 #[must_use]
191 pub const fn id(&self) -> WorkspaceId {
192 self.id
193 }
194
195 pub(crate) fn authority(&self) -> &A {
196 self.volume.fs.authority()
197 }
198
199 pub(crate) const fn authority_id(&self) -> crate::AuthorityId {
200 crate::kernel::volume_authority_id(self.id.volume_id())
201 }
202
203 #[must_use]
205 pub fn name(&self) -> &WorkspaceName {
206 &self.name
207 }
208
209 pub fn fork_workspace_id(
214 &self,
215 destination: impl AsRef<str>,
216 ) -> Result<WorkspaceId, WorkspaceNameError> {
217 self.volume.fs.workspace_id(destination)
218 }
219
220 #[must_use]
222 pub const fn profile(&self) -> crate::model::FilesystemProfile {
223 self.volume.config.profile
224 }
225
226 #[must_use]
228 pub const fn limits(&self) -> crate::model::VolumeLimits {
229 self.volume.config.limits
230 }
231}
232
233impl<A: AsyncAuthorityStore, O: AsyncObjectStore> Workspace<A, O> {
234 pub async fn operation_generation(
239 &self,
240 idempotency_key: IdempotencyKey,
241 ) -> Result<Option<Generation<A, O>>, WorkspaceError> {
242 self.volume
243 .fs
244 .workspace_operation_generation(&self.volume, idempotency_key.operation_id())
245 .await
246 .map(|generation| {
247 generation.map(|id| Generation {
248 workspace: self.clone(),
249 id,
250 })
251 })
252 }
253
254 pub async fn checkout(
261 &self,
262 selector: GenerationSelector,
263 mode: CheckoutMode,
264 ) -> Result<Checkout<A, O>, WorkspaceError> {
265 self.engine_checkout(selector, mode).await
266 }
267
268 #[cfg(all(feature = "native-watch", not(target_arch = "wasm32")))]
271 #[must_use]
272 pub fn source(&self) -> Option<&crate::Source<A, O>> {
273 self.source.as_ref()
274 }
275
276 #[cfg(all(feature = "native-watch", not(target_arch = "wasm32")))]
283 pub async fn seal(&self) -> Result<Generation<A, O>, crate::SourceError> {
284 match &self.source {
285 Some(source) => source.seal().await,
286 None => self
287 .head()
288 .await
289 .map_err(|error| crate::SourceError::Engine(error.to_string())),
290 }
291 }
292 pub async fn head(&self) -> Result<Generation<A, O>, WorkspaceError> {
298 self.head_measured(
299 crate::WorkBudget::UNBOUNDED,
300 &crate::CancellationToken::new(),
301 )
302 .await
303 .map(|receipt| receipt.value)
304 }
305
306 pub(crate) async fn head_measured(
307 &self,
308 budget: crate::WorkBudget,
309 cancellation: &crate::CancellationToken,
310 ) -> Result<crate::OperationReceipt<Generation<A, O>>, WorkspaceError> {
311 let receipt = self
312 .volume
313 .checkout(
314 GenerationSelector::Head,
315 CheckoutMode::read_only_pinned(),
316 budget,
317 cancellation,
318 )
319 .await
320 .map_err(|failure| WorkspaceError::from(failure.error))?;
321 Ok(crate::OperationReceipt {
322 value: Generation {
323 workspace: self.clone(),
324 id: receipt.value.generation_id(),
325 },
326 work: receipt.work,
327 })
328 }
329
330 pub async fn generation(
338 &self,
339 generation_id: GenerationId,
340 ) -> Result<Generation<A, O>, WorkspaceError> {
341 self.generation_measured(
342 generation_id,
343 crate::WorkBudget::UNBOUNDED,
344 &crate::CancellationToken::new(),
345 )
346 .await
347 .map(|receipt| receipt.value)
348 }
349
350 pub(crate) async fn generation_measured(
351 &self,
352 generation_id: GenerationId,
353 budget: crate::WorkBudget,
354 cancellation: &crate::CancellationToken,
355 ) -> Result<crate::OperationReceipt<Generation<A, O>>, WorkspaceError> {
356 let checkout = self
357 .engine_checkout_measured(
358 GenerationSelector::Exact(generation_id),
359 CheckoutMode::read_only_pinned(),
360 budget,
361 cancellation,
362 )
363 .await?;
364 Ok(crate::OperationReceipt {
365 value: Generation {
366 workspace: self.clone(),
367 id: checkout.value.generation_id(),
368 },
369 work: checkout.work,
370 })
371 }
372
373 pub async fn restore_generation(
381 &self,
382 generation: &Generation<A, O>,
383 if_current: GenerationId,
384 idempotency_key: IdempotencyKey,
385 ) -> Result<WorkspaceRestore<A, O>, WorkspaceError> {
386 self.restore_generation_with_permit(
387 generation,
388 if_current,
389 idempotency_key,
390 crate::PublicationPermit::Unrestricted,
391 )
392 .await
393 }
394
395 pub async fn restore_generation_with_permit(
397 &self,
398 generation: &Generation<A, O>,
399 if_current: GenerationId,
400 idempotency_key: IdempotencyKey,
401 permit: crate::PublicationPermit,
402 ) -> Result<WorkspaceRestore<A, O>, WorkspaceError> {
403 if generation.workspace.id != self.id {
404 return Err(WorkspaceError::ForeignGeneration);
405 }
406 let outcome = self
407 .volume
408 .fs
409 .restore_workspace_generation(
410 &self.volume,
411 generation.id,
412 if_current,
413 idempotency_key.operation_id(),
414 permit,
415 )
416 .await?;
417 let restored = |id| Generation {
418 workspace: self.clone(),
419 id,
420 };
421 Ok(match outcome {
422 WorkspaceRestoreOutcome::Restored(id) => WorkspaceRestore::Restored(restored(id)),
423 WorkspaceRestoreOutcome::AlreadyRestored(id) => {
424 WorkspaceRestore::AlreadyRestored(restored(id))
425 }
426 WorkspaceRestoreOutcome::Current(id) => WorkspaceRestore::Current(restored(id)),
427 WorkspaceRestoreOutcome::Stale(id) => WorkspaceRestore::Stale(restored(id)),
428 WorkspaceRestoreOutcome::Fenced => WorkspaceRestore::Fenced,
429 WorkspaceRestoreOutcome::IdempotencyConflict => WorkspaceRestore::IdempotencyConflict,
430 })
431 }
432
433 pub async fn restore_paths_from(
441 &self,
442 source: &Generation<A, O>,
443 paths: &[String],
444 if_current: GenerationId,
445 idempotency_key: IdempotencyKey,
446 ) -> Result<TransactionCommit<A, O>, WorkspaceError> {
447 self.restore_paths_from_with_permit(
448 source,
449 paths,
450 if_current,
451 idempotency_key,
452 crate::PublicationPermit::Unrestricted,
453 )
454 .await
455 }
456
457 pub async fn restore_paths_from_with_permit(
459 &self,
460 source: &Generation<A, O>,
461 paths: &[String],
462 if_current: GenerationId,
463 idempotency_key: IdempotencyKey,
464 permit: crate::PublicationPermit,
465 ) -> Result<TransactionCommit<A, O>, WorkspaceError> {
466 if !self.volume.fs.same_deployment(&source.workspace.volume.fs) {
467 return Err(WorkspaceError::ForeignGeneration);
468 }
469 let current = self.head().await?;
470 if current.id != if_current {
471 return Ok(TransactionCommit::Conflict { actual: current });
472 }
473 let mut source_checkout = source
474 .workspace
475 .engine_checkout(
476 GenerationSelector::Exact(source.id),
477 CheckoutMode::read_only_pinned(),
478 )
479 .await?;
480 let mut transaction = self.begin_transaction(idempotency_key).await?;
481 let config = self.volume.config;
482 let cancellation = crate::CancellationToken::new();
483 let parsed = paths
484 .iter()
485 .map(|path| {
486 let relative = path.trim_start_matches('/');
487 let absolute = format!("/{relative}");
488 customer_path(&absolute, config).map(|path| (relative, path))
489 })
490 .collect::<Result<Vec<_>, _>>()?;
491 let lookup_paths = parsed
492 .iter()
493 .map(|(_, path)| path.clone())
494 .collect::<Vec<_>>();
495 let source_records = source_checkout
496 .lookup_batch_no_follow(&lookup_paths, crate::WorkBudget::UNBOUNDED, &cancellation)
497 .await
498 .map_err(WorkspaceError::engine)?
499 .value
500 .entries
501 .into_iter()
502 .map(|entry| entry.record)
503 .collect::<Vec<_>>();
504 let missing_paths = parsed
505 .iter()
506 .zip(&source_records)
507 .filter(|(_, record)| record.is_none())
508 .map(|((_, path), _)| path.clone())
509 .collect::<Vec<_>>();
510 let current_records = if missing_paths.is_empty() {
511 Vec::new()
512 } else {
513 transaction
514 .checkout
515 .lookup_batch_no_follow(&missing_paths, crate::WorkBudget::UNBOUNDED, &cancellation)
516 .await
517 .map_err(WorkspaceError::engine)?
518 .value
519 .entries
520 .into_iter()
521 .map(|entry| entry.record)
522 .collect()
523 };
524 let mut current_records = current_records.into_iter();
525 let mut operations = Vec::with_capacity(paths.len());
526 for ((relative, path), source_record) in parsed.into_iter().zip(source_records) {
527 if let Some(record) = source_record {
528 if record.kind != FileKind::Directory
529 && let Some((parent, _)) = relative.rsplit_once('/')
530 {
531 transaction.create_dir_all(&format!("/{parent}")).await?;
532 }
533 operations.push(crate::kernel::Mutation::Restore { path, record });
534 } else {
535 let current_record = current_records
536 .next()
537 .ok_or_else(|| WorkspaceError::engine("missing batch lookup result"))?;
538 if let Some(record) = current_record {
539 operations.push(crate::kernel::Mutation::Remove {
540 path,
541 expected_file_id: MetadataField::Value(record.file_id),
542 });
543 }
544 }
545 }
546 if !operations.is_empty() {
547 transaction
548 .checkout
549 .mutate(operations, crate::WorkBudget::UNBOUNDED, &cancellation)
550 .await
551 .map_err(WorkspaceError::engine)?;
552 }
553 transaction.commit_with_permit(permit).await
554 }
555
556 #[allow(
561 clippy::too_many_lines,
562 reason = "one atomic path application keeps conflict classification and publication together"
563 )]
564 pub async fn apply_paths_from(
565 &self,
566 base: Option<&Generation<A, O>>,
567 source: Option<&Generation<A, O>>,
568 paths: &[String],
569 if_current: GenerationId,
570 idempotency_key: IdempotencyKey,
571 ) -> Result<WorkspacePathApply<A, O>, WorkspaceError> {
572 self.apply_paths_from_with_permit(
573 base,
574 source,
575 paths,
576 if_current,
577 idempotency_key,
578 crate::PublicationPermit::Unrestricted,
579 )
580 .await
581 }
582
583 #[allow(
585 clippy::too_many_lines,
586 reason = "one transaction keeps path planning, typed conflicts, and fenced publication atomic"
587 )]
588 pub async fn apply_paths_from_with_permit(
589 &self,
590 base: Option<&Generation<A, O>>,
591 source: Option<&Generation<A, O>>,
592 paths: &[String],
593 if_current: GenerationId,
594 idempotency_key: IdempotencyKey,
595 permit: crate::PublicationPermit,
596 ) -> Result<WorkspacePathApply<A, O>, WorkspaceError> {
597 if base.is_some_and(|base| !self.volume.fs.same_deployment(&base.workspace.volume.fs))
598 || source
599 .is_some_and(|source| !self.volume.fs.same_deployment(&source.workspace.volume.fs))
600 {
601 return Err(WorkspaceError::ForeignGeneration);
602 }
603 let current = self.head().await?;
604 if current.id != if_current {
605 return Ok(WorkspacePathApply::Stale(current));
606 }
607 let mut base_checkout = match base {
608 Some(base) => Some(
609 base.workspace
610 .engine_checkout(
611 GenerationSelector::Exact(base.id),
612 CheckoutMode::read_only_pinned(),
613 )
614 .await?,
615 ),
616 None => None,
617 };
618 let mut source_checkout = match source {
619 Some(source) => Some(
620 source
621 .workspace
622 .engine_checkout(
623 GenerationSelector::Exact(source.id),
624 CheckoutMode::read_only_pinned(),
625 )
626 .await?,
627 ),
628 None => None,
629 };
630 let mut transaction = self.begin_transaction(idempotency_key).await?;
631 let config = self.volume.config;
632 let cancellation = crate::CancellationToken::new();
633 let parsed = paths
634 .iter()
635 .map(|path| {
636 let relative = path.trim_start_matches('/');
637 let absolute = format!("/{relative}");
638 customer_path(&absolute, config).map(|path| (relative, path))
639 })
640 .collect::<Result<Vec<_>, _>>()?;
641 let lookup_paths = parsed
642 .iter()
643 .map(|(_, path)| path.clone())
644 .collect::<Vec<_>>();
645 let base_records = match &mut base_checkout {
646 Some(checkout) => checkout
647 .lookup_batch_no_follow(&lookup_paths, crate::WorkBudget::UNBOUNDED, &cancellation)
648 .await
649 .map_err(WorkspaceError::engine)?
650 .value
651 .entries
652 .into_iter()
653 .map(|entry| entry.record)
654 .collect(),
655 None => vec![None; parsed.len()],
656 };
657 let source_records = match &mut source_checkout {
658 Some(checkout) => checkout
659 .lookup_batch_no_follow(&lookup_paths, crate::WorkBudget::UNBOUNDED, &cancellation)
660 .await
661 .map_err(WorkspaceError::engine)?
662 .value
663 .entries
664 .into_iter()
665 .map(|entry| entry.record)
666 .collect(),
667 None => vec![None; parsed.len()],
668 };
669 let changed_paths = parsed
670 .iter()
671 .zip(base_records.iter().zip(&source_records))
672 .filter(|(_, (base, source))| base != source)
673 .map(|((_, path), _)| path.clone())
674 .collect::<Vec<_>>();
675 let current_records = if changed_paths.is_empty() {
676 Vec::new()
677 } else {
678 transaction
679 .checkout
680 .lookup_batch_no_follow(&changed_paths, crate::WorkBudget::UNBOUNDED, &cancellation)
681 .await
682 .map_err(WorkspaceError::engine)?
683 .value
684 .entries
685 .into_iter()
686 .map(|entry| entry.record)
687 .collect()
688 };
689 let mut current_records = current_records.into_iter();
690 let mut operations = Vec::with_capacity(paths.len());
691 let mut conflicts = Vec::new();
692 for (((relative, path), base_record), source_record) in
693 parsed.into_iter().zip(base_records).zip(source_records)
694 {
695 if base_record == source_record {
696 continue;
697 }
698 let current_record = current_records
699 .next()
700 .ok_or_else(|| WorkspaceError::engine("missing batch lookup result"))?;
701 if current_record == source_record {
702 continue;
703 }
704 if current_record != base_record {
705 conflicts.push(WorkspacePathConflict {
706 path: relative.to_owned(),
707 kind: path_conflict_kind(base_record, current_record, source_record),
708 });
709 continue;
710 }
711 match source_record {
712 Some(record) => {
713 if record.kind != FileKind::Directory
714 && let Some((parent, _)) = relative.rsplit_once('/')
715 {
716 transaction.create_dir_all(&format!("/{parent}")).await?;
717 }
718 operations.push(crate::kernel::Mutation::Restore { path, record });
719 }
720 None => {
721 if let Some(record) = current_record {
722 operations.push(crate::kernel::Mutation::Remove {
723 path,
724 expected_file_id: MetadataField::Value(record.file_id),
725 });
726 }
727 }
728 }
729 }
730 if !conflicts.is_empty() {
731 return Ok(WorkspacePathApply::Conflicted(conflicts));
732 }
733 if operations.is_empty() {
734 return Ok(WorkspacePathApply::NoChanges(current));
735 }
736 transaction
737 .checkout
738 .mutate(operations, crate::WorkBudget::UNBOUNDED, &cancellation)
739 .await
740 .map_err(WorkspaceError::engine)?;
741 Ok(match transaction.commit_with_permit(permit).await? {
742 TransactionCommit::Committed(generation) => WorkspacePathApply::Applied(generation),
743 TransactionCommit::AlreadyCommitted(generation) => {
744 WorkspacePathApply::AlreadyApplied(generation)
745 }
746 TransactionCommit::Conflict { actual } => WorkspacePathApply::Stale(actual),
747 TransactionCommit::Fenced => WorkspacePathApply::Fenced,
748 TransactionCommit::IdempotencyConflict => WorkspacePathApply::IdempotencyConflict,
749 })
750 }
751
752 pub async fn sync(&self) -> Result<WorkspaceSync<A, O>, WorkspaceError> {
760 Ok(WorkspaceSync {
761 generation: self.head().await?,
762 })
763 }
764
765 pub async fn fork(
773 &self,
774 destination: impl AsRef<str>,
775 options: ForkOptions<A, O>,
776 ) -> Result<Self, WorkspaceError> {
777 if options.generation.workspace.id != self.id {
778 return Err(WorkspaceError::ForeignGeneration);
779 }
780 self.fork_measured(
781 destination,
782 options,
783 crate::WorkBudget::UNBOUNDED,
784 &crate::CancellationToken::new(),
785 )
786 .await
787 .map(|receipt| receipt.value)
788 }
789
790 pub(crate) async fn fork_measured(
791 &self,
792 destination: impl AsRef<str>,
793 options: ForkOptions<A, O>,
794 budget: crate::WorkBudget,
795 cancellation: &crate::CancellationToken,
796 ) -> Result<crate::OperationReceipt<Self>, WorkspaceError> {
797 if options.generation.workspace.id != self.id {
798 return Err(WorkspaceError::ForeignGeneration);
799 }
800 let destination = WorkspaceName::new(destination)?;
801 Box::pin(self.volume.fs.fork_workspace_measured(
802 destination,
803 &options.generation,
804 options.idempotency_key,
805 budget,
806 cancellation,
807 ))
808 .await
809 }
810
811 pub async fn begin_transaction(
818 &self,
819 idempotency_key: IdempotencyKey,
820 ) -> Result<Transaction<A, O>, WorkspaceError> {
821 self.begin_transaction_measured(
822 idempotency_key,
823 crate::WorkBudget::UNBOUNDED,
824 &crate::CancellationToken::new(),
825 )
826 .await
827 .map(|receipt| receipt.value)
828 }
829
830 pub(crate) async fn begin_transaction_measured(
831 &self,
832 idempotency_key: IdempotencyKey,
833 budget: crate::WorkBudget,
834 cancellation: &crate::CancellationToken,
835 ) -> Result<crate::OperationReceipt<Transaction<A, O>>, WorkspaceError> {
836 let head = self.head_measured(budget, cancellation).await?;
837 let transaction = self
838 .open_transaction_at_measured(
839 &head.value,
840 idempotency_key,
841 false,
842 head.work
843 .remaining(budget)
844 .map_err(WorkspaceError::engine)?,
845 cancellation,
846 )
847 .await?;
848 Ok(crate::OperationReceipt {
849 value: transaction.value,
850 work: head
851 .work
852 .checked_add(transaction.work)
853 .map_err(WorkspaceError::engine)?,
854 })
855 }
856
857 pub async fn begin_transaction_at(
869 &self,
870 generation: &Generation<A, O>,
871 idempotency_key: IdempotencyKey,
872 ) -> Result<Transaction<A, O>, WorkspaceError> {
873 if generation.workspace.id != self.id {
874 return Err(WorkspaceError::ForeignGeneration);
875 }
876 let requires_rebase = self.head().await?.id != generation.id;
877 self.open_transaction_at(generation, idempotency_key, requires_rebase)
878 .await
879 }
880
881 pub(crate) async fn begin_pinned_transaction_measured(
882 &self,
883 generation: &Generation<A, O>,
884 idempotency_key: IdempotencyKey,
885 budget: crate::WorkBudget,
886 cancellation: &crate::CancellationToken,
887 ) -> Result<crate::OperationReceipt<Transaction<A, O>>, WorkspaceError> {
888 if generation.workspace.id != self.id {
889 return Err(WorkspaceError::ForeignGeneration);
890 }
891 self.open_transaction_at_measured(generation, idempotency_key, false, budget, cancellation)
892 .await
893 }
894
895 async fn open_transaction_at(
896 &self,
897 generation: &Generation<A, O>,
898 idempotency_key: IdempotencyKey,
899 requires_rebase: bool,
900 ) -> Result<Transaction<A, O>, WorkspaceError> {
901 self.open_transaction_at_measured(
902 generation,
903 idempotency_key,
904 requires_rebase,
905 crate::WorkBudget::UNBOUNDED,
906 &crate::CancellationToken::new(),
907 )
908 .await
909 .map(|receipt| receipt.value)
910 }
911
912 async fn open_transaction_at_measured(
913 &self,
914 generation: &Generation<A, O>,
915 idempotency_key: IdempotencyKey,
916 requires_rebase: bool,
917 budget: crate::WorkBudget,
918 cancellation: &crate::CancellationToken,
919 ) -> Result<crate::OperationReceipt<Transaction<A, O>>, WorkspaceError> {
920 let receipt = self
921 .engine_checkout_measured(
922 GenerationSelector::Exact(generation.id),
923 CheckoutMode::tracking_transaction(),
924 budget,
925 cancellation,
926 )
927 .await?;
928 let mut checkout = receipt.value;
929 checkout.bind_authored_operation(idempotency_key.operation_id());
930 Ok(crate::OperationReceipt {
931 value: Transaction {
932 checkout,
933 workspace: self.clone(),
934 idempotency_key,
935 requires_rebase,
936 },
937 work: receipt.work,
938 })
939 }
940
941 pub async fn checkpoint(
950 &self,
951 label: impl AsRef<str>,
952 ) -> Result<Checkpoint<A, O>, WorkspaceError> {
953 let label = WorkspaceName::new(label)?;
954 let generation = self.head().await?;
955 self.volume
956 .fs
957 .retain_workspace_generation(
958 &self.volume,
959 generation.id,
960 crate::kernel::RetentionKind::Checkpoint,
961 label.as_str().to_owned(),
962 )
963 .await?;
964 Ok(Checkpoint { label, generation })
965 }
966
967 pub async fn pin(
976 &self,
977 identity: impl AsRef<str>,
978 ) -> Result<GenerationPin<A, O>, WorkspaceError> {
979 self.head().await?.pin(identity).await
980 }
981
982 pub async fn delete(
990 &self,
991 idempotency_key: IdempotencyKey,
992 ) -> Result<WorkspaceDelete, WorkspaceError> {
993 self.volume
994 .fs
995 .delete_workspace_volume(&self.volume, idempotency_key.operation_id())
996 .await
997 }
998
999 pub async fn diff(
1007 &self,
1008 from: &Generation<A, O>,
1009 to: &Generation<A, O>,
1010 maximum_changes: u32,
1011 ) -> Result<ChangeSet<A, O>, WorkspaceError> {
1012 if from.workspace.id != self.id || to.workspace.id != self.id {
1013 return Err(WorkspaceError::ForeignGeneration);
1014 }
1015 let receipt = self
1016 .volume
1017 .diff_generations(
1018 from.id,
1019 to.id,
1020 maximum_changes,
1021 crate::WorkBudget::UNBOUNDED,
1022 &crate::CancellationToken::new(),
1023 )
1024 .await
1025 .map_err(WorkspaceError::engine)?;
1026 if receipt.value.truncated {
1027 return Err(WorkspaceError::JoinLimit);
1028 }
1029 Ok(ChangeSet {
1030 from: from.clone(),
1031 to: to.clone(),
1032 changes: receipt.value,
1033 work: receipt.work,
1034 })
1035 }
1036
1037 #[must_use]
1040 pub fn join_into(&self, target: &Workspace<A, O>) -> JoinBuilder<A, O> {
1041 JoinBuilder {
1042 source: self.clone(),
1043 target: target.clone(),
1044 history: JoinHistory::Merge,
1045 maximum_generations: 4_096,
1046 maximum_changes: self.volume.config().limits.maximum_paths_per_batch,
1047 maximum_conflicts: 1_024,
1048 }
1049 }
1050
1051 pub async fn live_rebase(
1064 &self,
1065 idempotency_key: IdempotencyKey,
1066 maximum_generations: u32,
1067 maximum_changes: u32,
1068 maximum_conflicts: u32,
1069 ) -> Result<WorkspaceRebase<A, O>, WorkspaceError> {
1070 let outcome = self
1071 .volume
1072 .fs
1073 .live_rebase_workspace(crate::facade::WorkspaceRebaseRequest {
1074 target: &self.volume,
1075 operation_id: idempotency_key.operation_id(),
1076 maximum_generations,
1077 maximum_changes,
1078 maximum_conflicts,
1079 })
1080 .await?;
1081 let generation = |id| Generation {
1082 workspace: self.clone(),
1083 id,
1084 };
1085 Ok(match outcome {
1086 crate::facade::WorkspaceJoinOutcome::Applied(id) => {
1087 WorkspaceRebase::Rebased(generation(id))
1088 }
1089 crate::facade::WorkspaceJoinOutcome::AlreadyApplied(id) => {
1090 WorkspaceRebase::AlreadyRebased(generation(id))
1091 }
1092 crate::facade::WorkspaceJoinOutcome::NoChanges(id) => {
1093 WorkspaceRebase::Current(generation(id))
1094 }
1095 crate::facade::WorkspaceJoinOutcome::Stale(id) => {
1096 WorkspaceRebase::Stale(generation(id))
1097 }
1098 crate::facade::WorkspaceJoinOutcome::Conflicted(conflicts, truncated) => {
1099 WorkspaceRebase::Conflicted {
1100 conflicts,
1101 truncated,
1102 }
1103 }
1104 crate::facade::WorkspaceJoinOutcome::Fenced => WorkspaceRebase::Fenced,
1105 crate::facade::WorkspaceJoinOutcome::IdempotencyConflict => {
1106 WorkspaceRebase::IdempotencyConflict
1107 }
1108 })
1109 }
1110
1111 pub async fn read(&self, path: &str, maximum_bytes: u64) -> Result<Bytes, WorkspaceError> {
1119 read_generation(self, GenerationSelector::Head, path, maximum_bytes).await
1120 }
1121
1122 pub async fn read_range(
1129 &self,
1130 path: &str,
1131 offset: u64,
1132 length: u64,
1133 ) -> Result<Bytes, WorkspaceError> {
1134 read_generation_range(self, GenerationSelector::Head, path, offset, length).await
1135 }
1136
1137 pub async fn stat(&self, path: &str) -> Result<WorkspaceStat, WorkspaceError> {
1143 stat_generation(self, GenerationSelector::Head, path).await
1144 }
1145
1146 pub(crate) async fn stat_optional_measured(
1147 &self,
1148 path: &str,
1149 budget: crate::WorkBudget,
1150 cancellation: &crate::CancellationToken,
1151 ) -> Result<crate::OperationReceipt<Option<WorkspaceStat>>, WorkspaceError> {
1152 stat_generation_optional_measured(
1153 self,
1154 GenerationSelector::Head,
1155 path,
1156 budget,
1157 cancellation,
1158 )
1159 .await
1160 }
1161
1162 pub(crate) async fn record_by_id(
1163 &self,
1164 file_id: FileId,
1165 ) -> Result<crate::kernel::FileRecord, WorkspaceError> {
1166 let mut checkout = self
1167 .engine_checkout(GenerationSelector::Head, CheckoutMode::read_only_pinned())
1168 .await?;
1169 checkout
1170 .read_file_record_by_id(
1171 file_id,
1172 crate::WorkBudget::UNBOUNDED,
1173 &crate::CancellationToken::new(),
1174 )
1175 .await
1176 .map(|receipt| receipt.value)
1177 .map_err(WorkspaceError::engine)
1178 }
1179
1180 pub(crate) fn detached_record(
1181 &self,
1182 record: crate::kernel::FileRecord,
1183 ) -> crate::DetachedFile<A, O> {
1184 crate::DetachedFile::from_record(self.volume.clone(), record)
1185 }
1186
1187 pub async fn list_directory(
1194 &self,
1195 path: &str,
1196 after: Option<&LogicalName>,
1197 maximum_entries: u32,
1198 ) -> Result<WorkspaceDirectoryPage, WorkspaceError> {
1199 list_generation_directory(self, GenerationSelector::Head, path, after, maximum_entries)
1200 .await
1201 }
1202
1203 pub(crate) async fn list_directory_measured(
1204 &self,
1205 path: &str,
1206 after: Option<&LogicalName>,
1207 maximum_entries: u32,
1208 budget: crate::WorkBudget,
1209 cancellation: &crate::CancellationToken,
1210 ) -> Result<crate::OperationReceipt<WorkspaceDirectoryPage>, WorkspaceError> {
1211 list_generation_directory_measured(
1212 self,
1213 GenerationSelector::Head,
1214 path,
1215 after,
1216 maximum_entries,
1217 budget,
1218 cancellation,
1219 )
1220 .await
1221 }
1222
1223 pub async fn read_symbolic_link(&self, path: &str) -> Result<Bytes, WorkspaceError> {
1230 read_generation_symbolic_link(self, GenerationSelector::Head, path).await
1231 }
1232
1233 pub async fn plan_extents(
1240 &self,
1241 path: &str,
1242 offset: u64,
1243 length: u64,
1244 maximum_spans: u32,
1245 ) -> Result<WorkspaceExtentPlan, WorkspaceError> {
1246 plan_generation_extents(
1247 self,
1248 GenerationSelector::Head,
1249 path,
1250 offset,
1251 length,
1252 maximum_spans,
1253 )
1254 .await
1255 }
1256
1257 pub async fn write_text(
1265 &self,
1266 path: &str,
1267 text: &str,
1268 ) -> Result<TransactionCommit<A, O>, WorkspaceError> {
1269 self.write(path, Bytes::copy_from_slice(text.as_bytes()))
1270 .await
1271 }
1272
1273 pub async fn write(
1280 &self,
1281 path: &str,
1282 bytes: Bytes,
1283 ) -> Result<TransactionCommit<A, O>, WorkspaceError> {
1284 let mut transaction = self.begin_transaction(IdempotencyKey::new()).await?;
1285 transaction.write(path, bytes).await?;
1286 transaction.commit().await
1287 }
1288
1289 pub async fn remove(&self, path: &str) -> Result<TransactionCommit<A, O>, WorkspaceError> {
1295 let mut transaction = self.begin_transaction(IdempotencyKey::new()).await?;
1296 transaction.remove(path).await?;
1297 transaction.commit().await
1298 }
1299
1300 pub async fn copy(
1308 &self,
1309 source: &str,
1310 destination: &str,
1311 ) -> Result<TransactionCommit<A, O>, WorkspaceError> {
1312 let mut transaction = self.begin_transaction(IdempotencyKey::new()).await?;
1313 transaction.copy(source, destination).await?;
1314 transaction.commit().await
1315 }
1316
1317 async fn read_selected(
1318 &self,
1319 selector: GenerationSelector,
1320 path: &str,
1321 maximum_bytes: u64,
1322 ) -> Result<Bytes, WorkspaceError> {
1323 let mut checkout = self
1324 .engine_checkout(selector, CheckoutMode::read_only_pinned())
1325 .await?;
1326 let path = customer_path(path, checkout.volume_config())?;
1327 let lookup = checkout
1328 .lookup_no_follow(
1329 &path,
1330 crate::WorkBudget::UNBOUNDED,
1331 &crate::CancellationToken::new(),
1332 )
1333 .await
1334 .map_err(WorkspaceError::engine)?
1335 .value
1336 .record
1337 .ok_or(WorkspaceError::NotFound)?;
1338 if lookup.kind != FileKind::Regular {
1339 return Err(WorkspaceError::NotRegularFile);
1340 }
1341 let logical_bytes = regular_file_bytes(lookup)?;
1342 if logical_bytes > maximum_bytes {
1343 return Err(WorkspaceError::ReadLimitExceeded);
1344 }
1345 checkout
1346 .read_file_range(
1347 &path,
1348 crate::ByteRange {
1349 offset: 0,
1350 length: logical_bytes,
1351 },
1352 crate::WorkBudget::UNBOUNDED,
1353 &crate::CancellationToken::new(),
1354 )
1355 .await
1356 .map(|receipt| receipt.value.bytes)
1357 .map_err(WorkspaceError::engine)
1358 }
1359
1360 pub(crate) async fn engine_checkout(
1361 &self,
1362 selector: GenerationSelector,
1363 mode: CheckoutMode,
1364 ) -> Result<Checkout<A, O>, WorkspaceError> {
1365 self.engine_checkout_measured(
1366 selector,
1367 mode,
1368 crate::WorkBudget::UNBOUNDED,
1369 &crate::CancellationToken::new(),
1370 )
1371 .await
1372 .map(|receipt| receipt.value)
1373 }
1374
1375 pub(crate) async fn engine_checkout_measured(
1376 &self,
1377 selector: GenerationSelector,
1378 mode: CheckoutMode,
1379 budget: crate::WorkBudget,
1380 cancellation: &crate::CancellationToken,
1381 ) -> Result<crate::OperationReceipt<Checkout<A, O>>, WorkspaceError> {
1382 self.volume
1383 .checkout(selector, mode, budget, cancellation)
1384 .await
1385 .map_err(|failure| WorkspaceError::from(failure.error))
1386 }
1387}
1388
1389async fn read_generation<A: AsyncAuthorityStore, O: AsyncObjectStore>(
1390 workspace: &Workspace<A, O>,
1391 selector: GenerationSelector,
1392 path: &str,
1393 maximum_bytes: u64,
1394) -> Result<Bytes, WorkspaceError> {
1395 workspace.read_selected(selector, path, maximum_bytes).await
1396}
1397
1398async fn read_generation_range<A: AsyncAuthorityStore, O: AsyncObjectStore>(
1399 workspace: &Workspace<A, O>,
1400 selector: GenerationSelector,
1401 path: &str,
1402 offset: u64,
1403 length: u64,
1404) -> Result<Bytes, WorkspaceError> {
1405 let mut checkout = workspace
1406 .engine_checkout(selector, CheckoutMode::read_only_pinned())
1407 .await?;
1408 let path = customer_path(path, checkout.volume_config())?;
1409 checkout
1410 .read_file_range(
1411 &path,
1412 crate::ByteRange { offset, length },
1413 crate::WorkBudget::UNBOUNDED,
1414 &crate::CancellationToken::new(),
1415 )
1416 .await
1417 .map(|receipt| receipt.value.bytes)
1418 .map_err(|failure| WorkspaceError::from(failure.error))
1419}
1420
1421async fn stat_generation<A: AsyncAuthorityStore, O: AsyncObjectStore>(
1422 workspace: &Workspace<A, O>,
1423 selector: GenerationSelector,
1424 path: &str,
1425) -> Result<WorkspaceStat, WorkspaceError> {
1426 stat_generation_measured(
1427 workspace,
1428 selector,
1429 path,
1430 crate::WorkBudget::UNBOUNDED,
1431 &crate::CancellationToken::new(),
1432 )
1433 .await
1434 .map(|receipt| receipt.value)
1435}
1436
1437async fn stat_generation_measured<A: AsyncAuthorityStore, O: AsyncObjectStore>(
1438 workspace: &Workspace<A, O>,
1439 selector: GenerationSelector,
1440 path: &str,
1441 budget: crate::WorkBudget,
1442 cancellation: &crate::CancellationToken,
1443) -> Result<crate::OperationReceipt<WorkspaceStat>, WorkspaceError> {
1444 let receipt =
1445 stat_generation_optional_measured(workspace, selector, path, budget, cancellation).await?;
1446 Ok(crate::OperationReceipt {
1447 value: receipt.value.ok_or(WorkspaceError::NotFound)?,
1448 work: receipt.work,
1449 })
1450}
1451
1452async fn stat_generation_optional_measured<A: AsyncAuthorityStore, O: AsyncObjectStore>(
1453 workspace: &Workspace<A, O>,
1454 selector: GenerationSelector,
1455 path: &str,
1456 budget: crate::WorkBudget,
1457 cancellation: &crate::CancellationToken,
1458) -> Result<crate::OperationReceipt<Option<WorkspaceStat>>, WorkspaceError> {
1459 let checkout = workspace
1460 .engine_checkout_measured(
1461 selector,
1462 CheckoutMode::read_only_pinned(),
1463 budget,
1464 cancellation,
1465 )
1466 .await?;
1467 let mut work = checkout.work;
1468 let mut checkout = checkout.value;
1469 let path = customer_path(path, checkout.volume_config())?;
1470 let lookup = checkout
1471 .lookup_no_follow_with_metadata(
1472 &path,
1473 work.remaining(budget).map_err(WorkspaceError::from)?,
1474 cancellation,
1475 )
1476 .await
1477 .map_err(|failure| WorkspaceError::from(failure.error))?;
1478 work = work
1479 .checked_add(lookup.work)
1480 .map_err(WorkspaceError::from)?;
1481 let Some(lookup) = lookup.value else {
1482 return Ok(crate::OperationReceipt { value: None, work });
1483 };
1484 let logical_bytes = match &lookup.record.payload {
1485 FilePayload::InlineRegular(bytes) => {
1486 Some(u64::try_from(bytes.as_bytes().len()).map_err(WorkspaceError::engine)?)
1487 }
1488 FilePayload::Regular { logical_bytes, .. }
1489 | FilePayload::SymbolicLink {
1490 target_bytes: logical_bytes,
1491 ..
1492 }
1493 | FilePayload::ReparsePoint {
1494 payload_bytes: logical_bytes,
1495 ..
1496 } => Some(*logical_bytes),
1497 FilePayload::Directory { .. } | FilePayload::Device { .. } | FilePayload::Empty => None,
1498 };
1499 Ok(crate::OperationReceipt {
1500 value: Some(WorkspaceStat {
1501 file_id: lookup.record.file_id,
1502 kind: lookup.record.kind,
1503 link_count: lookup.record.link_count,
1504 logical_bytes,
1505 metadata: WorkspaceMetadata::from_engine(lookup.metadata),
1506 }),
1507 work,
1508 })
1509}
1510
1511async fn list_generation_directory<A: AsyncAuthorityStore, O: AsyncObjectStore>(
1512 workspace: &Workspace<A, O>,
1513 selector: GenerationSelector,
1514 path: &str,
1515 after: Option<&LogicalName>,
1516 maximum_entries: u32,
1517) -> Result<WorkspaceDirectoryPage, WorkspaceError> {
1518 list_generation_directory_measured(
1519 workspace,
1520 selector,
1521 path,
1522 after,
1523 maximum_entries,
1524 crate::WorkBudget::UNBOUNDED,
1525 &crate::CancellationToken::new(),
1526 )
1527 .await
1528 .map(|receipt| receipt.value)
1529}
1530
1531#[allow(clippy::too_many_arguments)]
1532async fn list_generation_directory_measured<A: AsyncAuthorityStore, O: AsyncObjectStore>(
1533 workspace: &Workspace<A, O>,
1534 selector: GenerationSelector,
1535 path: &str,
1536 after: Option<&LogicalName>,
1537 maximum_entries: u32,
1538 budget: crate::WorkBudget,
1539 cancellation: &crate::CancellationToken,
1540) -> Result<crate::OperationReceipt<WorkspaceDirectoryPage>, WorkspaceError> {
1541 let checkout = workspace
1542 .engine_checkout_measured(
1543 selector,
1544 CheckoutMode::read_only_pinned(),
1545 budget,
1546 cancellation,
1547 )
1548 .await?;
1549 let prior = checkout.work;
1550 let mut checkout = checkout.value;
1551 let path = customer_path(path, checkout.volume_config())?;
1552 let page = checkout
1553 .list_directory(
1554 &path,
1555 after,
1556 maximum_entries,
1557 prior.remaining(budget).map_err(WorkspaceError::from)?,
1558 cancellation,
1559 )
1560 .await
1561 .map_err(|failure| WorkspaceError::from(failure.error))?;
1562 let work = prior.checked_add(page.work).map_err(WorkspaceError::from)?;
1563 Ok(crate::OperationReceipt {
1564 value: WorkspaceDirectoryPage {
1565 entries: page
1566 .value
1567 .entries
1568 .into_iter()
1569 .map(|entry| WorkspaceDirectoryEntry {
1570 name: entry.name,
1571 file_id: entry.file_id,
1572 kind: entry.kind,
1573 })
1574 .collect(),
1575 has_more: page.value.has_more,
1576 },
1577 work,
1578 })
1579}
1580
1581async fn read_generation_symbolic_link<A: AsyncAuthorityStore, O: AsyncObjectStore>(
1582 workspace: &Workspace<A, O>,
1583 selector: GenerationSelector,
1584 path: &str,
1585) -> Result<Bytes, WorkspaceError> {
1586 let mut checkout = workspace
1587 .engine_checkout(selector, CheckoutMode::read_only_pinned())
1588 .await?;
1589 let path = customer_path(path, checkout.volume_config())?;
1590 checkout
1591 .read_symbolic_link(
1592 &path,
1593 crate::WorkBudget::UNBOUNDED,
1594 &crate::CancellationToken::new(),
1595 )
1596 .await
1597 .map(|receipt| receipt.value)
1598 .map_err(WorkspaceError::engine)
1599}
1600
1601async fn plan_generation_extents<A: AsyncAuthorityStore, O: AsyncObjectStore>(
1602 workspace: &Workspace<A, O>,
1603 selector: GenerationSelector,
1604 path: &str,
1605 offset: u64,
1606 length: u64,
1607 maximum_spans: u32,
1608) -> Result<WorkspaceExtentPlan, WorkspaceError> {
1609 let mut checkout = workspace
1610 .engine_checkout(selector, CheckoutMode::read_only_pinned())
1611 .await?;
1612 let path = customer_path(path, checkout.volume_config())?;
1613 let plan = checkout
1614 .plan_file_extents(
1615 &path,
1616 crate::ByteRange { offset, length },
1617 maximum_spans,
1618 crate::WorkBudget::UNBOUNDED,
1619 &crate::CancellationToken::new(),
1620 )
1621 .await
1622 .map_err(WorkspaceError::engine)?
1623 .value;
1624 let spans = match plan {
1625 Some(plan) => plan
1626 .spans
1627 .into_iter()
1628 .map(|span| WorkspaceExtentSpan {
1629 offset: span.offset,
1630 length: span.length,
1631 source_end: span.source_end,
1632 kind: match span.kind {
1633 ExtentKind::Hole => WorkspaceExtentKind::Hole,
1634 ExtentKind::AllocatedZero => WorkspaceExtentKind::AllocatedZero,
1635 ExtentKind::Content { .. } => WorkspaceExtentKind::Content,
1636 },
1637 })
1638 .collect(),
1639 None if length == 0 => Vec::new(),
1640 None => vec![WorkspaceExtentSpan {
1641 offset,
1642 length,
1643 source_end: offset
1644 .checked_add(length)
1645 .ok_or_else(|| WorkspaceError::engine("file range overflows"))?,
1646 kind: WorkspaceExtentKind::Content,
1647 }],
1648 };
1649 Ok(WorkspaceExtentPlan { spans })
1650}
1651
1652pub struct Transaction<A, O> {
1654 workspace: Workspace<A, O>,
1655 checkout: Checkout<A, O>,
1656 idempotency_key: IdempotencyKey,
1657 requires_rebase: bool,
1658}
1659
1660impl<A: AsyncAuthorityStore, O: AsyncObjectStore> Transaction<A, O> {
1661 #[cfg(feature = "native-mount")]
1664 pub(crate) fn for_checkout_candidate(
1665 workspace: &Workspace<A, O>,
1666 checkout: Checkout<A, O>,
1667 ) -> Self {
1668 Self {
1669 workspace: workspace.clone(),
1670 checkout,
1671 idempotency_key: IdempotencyKey::new(),
1672 requires_rebase: false,
1673 }
1674 }
1675
1676 #[cfg(feature = "native-mount")]
1677 pub(crate) fn into_checkout_candidate(self) -> Checkout<A, O> {
1678 self.checkout
1679 }
1680
1681 pub(crate) fn workspace_id(&self) -> WorkspaceId {
1682 self.workspace.id()
1683 }
1684
1685 pub(crate) async fn restore_record_measured(
1686 &mut self,
1687 path: &str,
1688 record: crate::kernel::FileRecord,
1689 budget: crate::WorkBudget,
1690 cancellation: &crate::CancellationToken,
1691 ) -> Result<crate::WorkCounters, WorkspaceError> {
1692 let path = customer_path(path, self.checkout.volume_config())?;
1693 self.apply_engine_measured(
1694 vec![crate::kernel::Mutation::Restore { path, record }],
1695 budget,
1696 cancellation,
1697 )
1698 .await
1699 }
1700
1701 pub(crate) async fn create_dir_all_measured(
1702 &mut self,
1703 path: &str,
1704 budget: crate::WorkBudget,
1705 cancellation: &crate::CancellationToken,
1706 ) -> Result<crate::WorkCounters, WorkspaceError> {
1707 let portable = PortablePath::parse(path, self.checkout.volume_config().limits)
1708 .map_err(WorkspaceError::path)?;
1709 let mut current = String::new();
1710 let mut work = crate::WorkCounters::default();
1711 for component in portable.components() {
1712 cancellation.check().map_err(WorkspaceError::from)?;
1713 current.push('/');
1714 current.push_str(component);
1715 let path = customer_path(¤t, self.checkout.volume_config())?;
1716 let lookup = self
1717 .checkout
1718 .lookup_no_follow(
1719 &path,
1720 work.remaining(budget).map_err(WorkspaceError::from)?,
1721 cancellation,
1722 )
1723 .await
1724 .map_err(|failure| WorkspaceError::from(failure.error))?;
1725 work = work
1726 .checked_add(lookup.work)
1727 .map_err(WorkspaceError::from)?;
1728 match lookup.value.record {
1729 Some(value) if value.kind == FileKind::Directory => {}
1730 Some(_) => return Err(WorkspaceError::NotDirectory),
1731 None => {
1732 let applied = self
1733 .apply_measured(
1734 vec![AuthoredMutation::CreateDirectory {
1735 path,
1736 metadata: FileMetadata::default(),
1737 }],
1738 work.remaining(budget).map_err(WorkspaceError::from)?,
1739 cancellation,
1740 )
1741 .await?;
1742 work = work.checked_add(applied).map_err(WorkspaceError::from)?;
1743 }
1744 }
1745 }
1746 Ok(work)
1747 }
1748
1749 pub(crate) async fn apply_authored_measured(
1750 &mut self,
1751 operation: AuthoredMutation,
1752 budget: crate::WorkBudget,
1753 cancellation: &crate::CancellationToken,
1754 ) -> Result<crate::WorkCounters, WorkspaceError> {
1755 self.apply_measured(vec![operation], budget, cancellation)
1756 .await
1757 }
1758
1759 pub(crate) async fn apply_authored_bulk_measured(
1761 &mut self,
1762 operations: Vec<AuthoredMutation>,
1763 budget: crate::WorkBudget,
1764 cancellation: &crate::CancellationToken,
1765 ) -> Result<crate::WorkCounters, WorkspaceError> {
1766 self.checkout
1767 .apply_authored_bulk_transaction(operations, budget, cancellation)
1768 .await
1769 .map(|receipt| receipt.work)
1770 .map_err(|failure| WorkspaceError::from(failure.error))
1771 }
1772
1773 pub(crate) async fn create_directory_measured(
1774 &mut self,
1775 path: &str,
1776 budget: crate::WorkBudget,
1777 cancellation: &crate::CancellationToken,
1778 ) -> Result<crate::WorkCounters, WorkspaceError> {
1779 let path = customer_path(path, self.checkout.volume_config())?;
1780 self.apply_authored_measured(
1781 AuthoredMutation::CreateDirectory {
1782 path,
1783 metadata: FileMetadata::default(),
1784 },
1785 budget,
1786 cancellation,
1787 )
1788 .await
1789 }
1790
1791 pub(crate) async fn create_file_measured(
1792 &mut self,
1793 path: &str,
1794 bytes: Bytes,
1795 budget: crate::WorkBudget,
1796 cancellation: &crate::CancellationToken,
1797 ) -> Result<crate::WorkCounters, WorkspaceError> {
1798 let path = customer_path(path, self.checkout.volume_config())?;
1799 self.apply_authored_measured(
1800 AuthoredMutation::CreateFile {
1801 path,
1802 bytes,
1803 metadata: FileMetadata::default(),
1804 },
1805 budget,
1806 cancellation,
1807 )
1808 .await
1809 }
1810
1811 pub(crate) async fn write_range_measured(
1812 &mut self,
1813 path: &str,
1814 offset: u64,
1815 bytes: Bytes,
1816 budget: crate::WorkBudget,
1817 cancellation: &crate::CancellationToken,
1818 ) -> Result<crate::WorkCounters, WorkspaceError> {
1819 let path = customer_path(path, self.checkout.volume_config())?;
1820 self.apply_authored_measured(
1821 AuthoredMutation::Write {
1822 path,
1823 offset,
1824 bytes,
1825 },
1826 budget,
1827 cancellation,
1828 )
1829 .await
1830 }
1831
1832 pub(crate) async fn create_symbolic_link_measured(
1833 &mut self,
1834 path: &str,
1835 target: Bytes,
1836 budget: crate::WorkBudget,
1837 cancellation: &crate::CancellationToken,
1838 ) -> Result<crate::WorkCounters, WorkspaceError> {
1839 let path = customer_path(path, self.checkout.volume_config())?;
1840 self.apply_authored_measured(
1841 AuthoredMutation::CreateSymbolicLink {
1842 path,
1843 target,
1844 metadata: FileMetadata::default(),
1845 },
1846 budget,
1847 cancellation,
1848 )
1849 .await
1850 }
1851
1852 pub(crate) async fn create_special_measured(
1853 &mut self,
1854 path: &str,
1855 kind: FileKind,
1856 device: Option<(u32, u32)>,
1857 budget: crate::WorkBudget,
1858 cancellation: &crate::CancellationToken,
1859 ) -> Result<crate::WorkCounters, WorkspaceError> {
1860 let path = customer_path(path, self.checkout.volume_config())?;
1861 let operation = match (kind, device) {
1862 (FileKind::Fifo | FileKind::Socket, None) => AuthoredMutation::CreateEmptySpecial {
1863 path,
1864 kind,
1865 metadata: FileMetadata::default(),
1866 },
1867 (FileKind::CharacterDevice | FileKind::BlockDevice, Some((major, minor))) => {
1868 AuthoredMutation::CreateDevice {
1869 path,
1870 kind,
1871 major,
1872 minor,
1873 metadata: FileMetadata::default(),
1874 }
1875 }
1876 _ => return Err(WorkspaceError::IncompatibleWorkspace),
1877 };
1878 self.apply_authored_measured(operation, budget, cancellation)
1879 .await
1880 }
1881
1882 pub(crate) async fn set_metadata_measured(
1883 &mut self,
1884 path: &str,
1885 metadata: FileMetadata,
1886 budget: crate::WorkBudget,
1887 cancellation: &crate::CancellationToken,
1888 ) -> Result<crate::WorkCounters, WorkspaceError> {
1889 let path = customer_path(path, self.checkout.volume_config())?;
1890 self.apply_authored_measured(
1891 AuthoredMutation::SetMetadata { path, metadata },
1892 budget,
1893 cancellation,
1894 )
1895 .await
1896 }
1897
1898 pub(crate) async fn preserve_file_identity_measured(
1899 &mut self,
1900 path: &str,
1901 file_id: FileId,
1902 budget: crate::WorkBudget,
1903 cancellation: &crate::CancellationToken,
1904 ) -> Result<crate::WorkCounters, WorkspaceError> {
1905 let path = customer_path(path, self.checkout.volume_config())?;
1906 self.apply_authored_measured(
1907 AuthoredMutation::Reidentify { path, file_id },
1908 budget,
1909 cancellation,
1910 )
1911 .await
1912 }
1913
1914 pub(crate) async fn hard_link_measured(
1915 &mut self,
1916 source: &str,
1917 destination: &str,
1918 budget: crate::WorkBudget,
1919 cancellation: &crate::CancellationToken,
1920 ) -> Result<crate::WorkCounters, WorkspaceError> {
1921 let config = self.checkout.volume_config();
1922 self.apply_authored_measured(
1923 AuthoredMutation::HardLink {
1924 source: customer_path(source, config)?,
1925 destination: customer_path(destination, config)?,
1926 },
1927 budget,
1928 cancellation,
1929 )
1930 .await
1931 }
1932
1933 async fn apply_engine_measured(
1934 &mut self,
1935 operations: Vec<crate::kernel::Mutation>,
1936 budget: crate::WorkBudget,
1937 cancellation: &crate::CancellationToken,
1938 ) -> Result<crate::WorkCounters, WorkspaceError> {
1939 self.checkout
1940 .mutate(operations, budget, cancellation)
1941 .await
1942 .map(|receipt| receipt.work)
1943 .map_err(|failure| WorkspaceError::from(failure.error))
1944 }
1945
1946 pub async fn preserve_file_identity(
1951 &mut self,
1952 path: &str,
1953 file_id: FileId,
1954 ) -> Result<(), WorkspaceError> {
1955 let path = customer_path(path, self.checkout.volume_config())?;
1956 self.apply(vec![AuthoredMutation::Reidentify { path, file_id }])
1957 .await
1958 }
1959
1960 pub async fn create_file(
1967 &mut self,
1968 path: &str,
1969 bytes: Bytes,
1970 metadata: FileMetadata,
1971 ) -> Result<(), WorkspaceError> {
1972 let path = customer_path(path, self.checkout.volume_config())?;
1973 self.apply(vec![AuthoredMutation::CreateFile {
1974 path,
1975 bytes,
1976 metadata,
1977 }])
1978 .await
1979 }
1980
1981 pub async fn stage_content<R: crate::kernel::AsyncBlobSource>(
1988 &self,
1989 source: &mut R,
1990 maximum_source_bytes: u64,
1991 ) -> Result<crate::StagedContent, WorkspaceError> {
1992 self.stage_content_measured(
1993 source,
1994 maximum_source_bytes,
1995 crate::WorkBudget::UNBOUNDED,
1996 &crate::CancellationToken::new(),
1997 )
1998 .await
1999 .map(|receipt| receipt.value)
2000 }
2001
2002 pub(crate) async fn stage_content_measured<R: crate::kernel::AsyncBlobSource>(
2003 &self,
2004 source: &mut R,
2005 maximum_source_bytes: u64,
2006 budget: crate::WorkBudget,
2007 cancellation: &crate::CancellationToken,
2008 ) -> Result<crate::OperationReceipt<crate::StagedContent>, WorkspaceError> {
2009 self.checkout
2010 .stage_content(source, maximum_source_bytes, budget, cancellation)
2011 .await
2012 .map(|receipt| crate::OperationReceipt {
2013 value: receipt.value,
2014 work: receipt.work,
2015 })
2016 .map_err(WorkspaceError::engine)
2017 }
2018
2019 pub async fn create_dir_all(&mut self, path: &str) -> Result<(), WorkspaceError> {
2025 let portable = PortablePath::parse(path, self.checkout.volume_config().limits)
2026 .map_err(WorkspaceError::path)?;
2027 let mut current = String::new();
2028 for component in portable.components() {
2029 current.push('/');
2030 current.push_str(component);
2031 let path = customer_path(¤t, self.checkout.volume_config())?;
2032 let existing = self
2033 .checkout
2034 .lookup_no_follow(
2035 &path,
2036 crate::WorkBudget::UNBOUNDED,
2037 &crate::CancellationToken::new(),
2038 )
2039 .await
2040 .map_err(WorkspaceError::engine)?
2041 .value
2042 .record;
2043 match existing {
2044 Some(value) if value.kind == FileKind::Directory => {}
2045 Some(_) => return Err(WorkspaceError::NotDirectory),
2046 None => {
2047 self.apply(vec![AuthoredMutation::CreateDirectory {
2048 path,
2049 metadata: FileMetadata::default(),
2050 }])
2051 .await?;
2052 }
2053 }
2054 }
2055 Ok(())
2056 }
2057
2058 pub async fn create_directory(&mut self, path: &str) -> Result<(), WorkspaceError> {
2064 let path = customer_path(path, self.checkout.volume_config())?;
2065 self.apply(vec![AuthoredMutation::CreateDirectory {
2066 path,
2067 metadata: FileMetadata::default(),
2068 }])
2069 .await
2070 }
2071
2072 pub async fn create_symbolic_link(
2078 &mut self,
2079 path: &str,
2080 target: Bytes,
2081 ) -> Result<(), WorkspaceError> {
2082 let path = customer_path(path, self.checkout.volume_config())?;
2083 self.apply(vec![AuthoredMutation::CreateSymbolicLink {
2084 path,
2085 target,
2086 metadata: FileMetadata::default(),
2087 }])
2088 .await
2089 }
2090
2091 pub async fn create_special(
2093 &mut self,
2094 path: &str,
2095 kind: FileKind,
2096 device: Option<(u32, u32)>,
2097 ) -> Result<(), WorkspaceError> {
2098 let path = customer_path(path, self.checkout.volume_config())?;
2099 let mutation = match (kind, device) {
2100 (FileKind::Fifo | FileKind::Socket, None) => AuthoredMutation::CreateEmptySpecial {
2101 path,
2102 kind,
2103 metadata: FileMetadata::default(),
2104 },
2105 (FileKind::CharacterDevice | FileKind::BlockDevice, Some((major, minor))) => {
2106 AuthoredMutation::CreateDevice {
2107 path,
2108 kind,
2109 major,
2110 minor,
2111 metadata: FileMetadata::default(),
2112 }
2113 }
2114 _ => return Err(WorkspaceError::IncompatibleWorkspace),
2115 };
2116 self.apply(vec![mutation]).await
2117 }
2118
2119 pub async fn write_text(&mut self, path: &str, text: &str) -> Result<(), WorkspaceError> {
2126 self.write(path, Bytes::copy_from_slice(text.as_bytes()))
2127 .await
2128 }
2129
2130 pub async fn write(&mut self, path: &str, bytes: Bytes) -> Result<(), WorkspaceError> {
2137 let path = customer_path(path, self.checkout.volume_config())?;
2138 let existing = self
2139 .checkout
2140 .lookup_no_follow(
2141 &path,
2142 crate::WorkBudget::UNBOUNDED,
2143 &crate::CancellationToken::new(),
2144 )
2145 .await
2146 .map_err(WorkspaceError::engine)?
2147 .value
2148 .record;
2149 let operations = match existing {
2150 None => vec![AuthoredMutation::CreateFile {
2151 path,
2152 bytes,
2153 metadata: FileMetadata::default(),
2154 }],
2155 Some(value) if value.kind == FileKind::Regular => vec![
2156 AuthoredMutation::Resize {
2157 path: path.clone(),
2158 logical_bytes: 0,
2159 },
2160 AuthoredMutation::Write {
2161 path,
2162 offset: 0,
2163 bytes,
2164 },
2165 ],
2166 Some(_) => return Err(WorkspaceError::NotRegularFile),
2167 };
2168 self.apply(operations).await
2169 }
2170
2171 pub async fn write_staged(
2178 &mut self,
2179 path: &str,
2180 parts: &[crate::StagedContent],
2181 ) -> Result<(), WorkspaceError> {
2182 if parts.is_empty() {
2183 return Err(WorkspaceError::EmptyContentSet);
2184 }
2185 let path = customer_path(path, self.checkout.volume_config())?;
2186 let existing = self
2187 .checkout
2188 .lookup_no_follow(
2189 &path,
2190 crate::WorkBudget::UNBOUNDED,
2191 &crate::CancellationToken::new(),
2192 )
2193 .await
2194 .map_err(WorkspaceError::engine)?
2195 .value
2196 .record;
2197 let mut operations = match existing {
2198 None => vec![AuthoredMutation::CreateFile {
2199 path: path.clone(),
2200 bytes: Bytes::new(),
2201 metadata: FileMetadata::default(),
2202 }],
2203 Some(record) if record.kind == FileKind::Regular => vec![AuthoredMutation::Resize {
2204 path: path.clone(),
2205 logical_bytes: 0,
2206 }],
2207 Some(_) => return Err(WorkspaceError::NotRegularFile),
2208 };
2209 let mut offset = 0_u64;
2210 for part in parts {
2211 if part.logical_bytes() != 0 {
2212 operations.push(AuthoredMutation::WriteFromContent {
2213 path: path.clone(),
2214 offset,
2215 content: *part,
2216 });
2217 }
2218 offset = offset
2219 .checked_add(part.logical_bytes())
2220 .ok_or(WorkspaceError::ContentLengthOverflow)?;
2221 }
2222 self.apply(operations).await
2223 }
2224
2225 pub async fn remove(&mut self, path: &str) -> Result<(), WorkspaceError> {
2231 let path = customer_path(path, self.checkout.volume_config())?;
2232 let existing = self
2233 .checkout
2234 .lookup_no_follow(
2235 &path,
2236 crate::WorkBudget::UNBOUNDED,
2237 &crate::CancellationToken::new(),
2238 )
2239 .await
2240 .map_err(WorkspaceError::engine)?
2241 .value
2242 .record
2243 .ok_or(WorkspaceError::NotFound)?;
2244 self.apply(vec![AuthoredMutation::Remove {
2245 path,
2246 expected_file_id: Some(existing.file_id),
2247 }])
2248 .await
2249 }
2250
2251 pub async fn remove_if(
2256 &mut self,
2257 path: &str,
2258 expected_file_id: crate::FileId,
2259 ) -> Result<(), WorkspaceError> {
2260 let path = customer_path(path, self.checkout.volume_config())?;
2261 self.apply(vec![AuthoredMutation::Remove {
2262 path,
2263 expected_file_id: Some(expected_file_id),
2264 }])
2265 .await
2266 }
2267
2268 pub async fn copy(&mut self, source: &str, destination: &str) -> Result<(), WorkspaceError> {
2274 let config = self.checkout.volume_config();
2275 let source = customer_path(source, config)?;
2276 let destination = customer_path(destination, config)?;
2277 let source_record = self
2278 .checkout
2279 .lookup_no_follow(
2280 &source,
2281 crate::WorkBudget::UNBOUNDED,
2282 &crate::CancellationToken::new(),
2283 )
2284 .await
2285 .map_err(WorkspaceError::engine)?
2286 .value
2287 .record
2288 .ok_or(WorkspaceError::NotFound)?;
2289 let logical_bytes = regular_file_bytes(source_record)?;
2290 let destination_record = self
2291 .checkout
2292 .lookup_no_follow(
2293 &destination,
2294 crate::WorkBudget::UNBOUNDED,
2295 &crate::CancellationToken::new(),
2296 )
2297 .await
2298 .map_err(WorkspaceError::engine)?
2299 .value
2300 .record;
2301 let mut operations = match destination_record {
2302 None => vec![AuthoredMutation::CreateFile {
2303 path: destination.clone(),
2304 bytes: Bytes::new(),
2305 metadata: FileMetadata::default(),
2306 }],
2307 Some(record) if record.kind == FileKind::Regular => vec![AuthoredMutation::Resize {
2308 path: destination.clone(),
2309 logical_bytes: 0,
2310 }],
2311 Some(_) => return Err(WorkspaceError::NotRegularFile),
2312 };
2313 if logical_bytes != 0 {
2314 operations.push(AuthoredMutation::CloneRange(crate::FileCloneRequest {
2315 source,
2316 source_offset: 0,
2317 destination,
2318 destination_offset: 0,
2319 length: logical_bytes,
2320 }));
2321 }
2322 self.apply(operations).await
2323 }
2324
2325 pub async fn rename(&mut self, source: &str, destination: &str) -> Result<(), WorkspaceError> {
2331 self.rename_with_replace(source, destination, true).await
2332 }
2333
2334 pub async fn rename_with_replace(
2341 &mut self,
2342 source: &str,
2343 destination: &str,
2344 replace: bool,
2345 ) -> Result<(), WorkspaceError> {
2346 let config = self.checkout.volume_config();
2347 self.apply(vec![AuthoredMutation::Rename {
2348 source: customer_path(source, config)?,
2349 destination: customer_path(destination, config)?,
2350 replace,
2351 }])
2352 .await
2353 }
2354
2355 pub async fn hard_link(
2361 &mut self,
2362 source: &str,
2363 destination: &str,
2364 ) -> Result<(), WorkspaceError> {
2365 let config = self.checkout.volume_config();
2366 self.apply(vec![AuthoredMutation::HardLink {
2367 source: customer_path(source, config)?,
2368 destination: customer_path(destination, config)?,
2369 }])
2370 .await
2371 }
2372
2373 pub async fn write_range(
2379 &mut self,
2380 path: &str,
2381 offset: u64,
2382 bytes: Bytes,
2383 ) -> Result<(), WorkspaceError> {
2384 let path = customer_path(path, self.checkout.volume_config())?;
2385 self.apply(vec![AuthoredMutation::Write {
2386 path,
2387 offset,
2388 bytes,
2389 }])
2390 .await
2391 }
2392
2393 pub async fn resize(&mut self, path: &str, logical_bytes: u64) -> Result<(), WorkspaceError> {
2399 let path = customer_path(path, self.checkout.volume_config())?;
2400 self.apply(vec![AuthoredMutation::Resize {
2401 path,
2402 logical_bytes,
2403 }])
2404 .await
2405 }
2406
2407 pub async fn zero_range(
2413 &mut self,
2414 path: &str,
2415 range: crate::ByteRange,
2416 allocated: bool,
2417 extend: bool,
2418 ) -> Result<(), WorkspaceError> {
2419 let path = customer_path(path, self.checkout.volume_config())?;
2420 self.apply(vec![AuthoredMutation::ZeroRange {
2421 path,
2422 range,
2423 allocated,
2424 extend,
2425 }])
2426 .await
2427 }
2428
2429 pub async fn preallocate(
2435 &mut self,
2436 path: &str,
2437 range: crate::ByteRange,
2438 keep_size: bool,
2439 ) -> Result<(), WorkspaceError> {
2440 let path = customer_path(path, self.checkout.volume_config())?;
2441 self.apply(vec![AuthoredMutation::Preallocate {
2442 path,
2443 range,
2444 keep_size,
2445 }])
2446 .await
2447 }
2448
2449 pub async fn clone_range(
2455 &mut self,
2456 source: &str,
2457 source_offset: u64,
2458 destination: &str,
2459 destination_offset: u64,
2460 length: u64,
2461 ) -> Result<(), WorkspaceError> {
2462 let config = self.checkout.volume_config();
2463 self.apply(vec![AuthoredMutation::CloneRange(
2464 crate::FileCloneRequest {
2465 source: customer_path(source, config)?,
2466 source_offset,
2467 destination: customer_path(destination, config)?,
2468 destination_offset,
2469 length,
2470 },
2471 )])
2472 .await
2473 }
2474
2475 pub async fn set_metadata(
2481 &mut self,
2482 path: &str,
2483 metadata: FileMetadata,
2484 ) -> Result<(), WorkspaceError> {
2485 let path = customer_path(path, self.checkout.volume_config())?;
2486 self.apply(vec![AuthoredMutation::SetMetadata { path, metadata }])
2487 .await
2488 }
2489
2490 pub async fn commit(&mut self) -> Result<TransactionCommit<A, O>, WorkspaceError> {
2498 self.commit_with_permit(crate::PublicationPermit::Unrestricted)
2499 .await
2500 }
2501
2502 pub async fn commit_with_permit(
2505 &mut self,
2506 permit: crate::PublicationPermit,
2507 ) -> Result<TransactionCommit<A, O>, WorkspaceError> {
2508 self.commit_with_permit_measured(
2509 permit,
2510 crate::WorkBudget::UNBOUNDED,
2511 &crate::CancellationToken::new(),
2512 )
2513 .await
2514 .map(|receipt| receipt.value)
2515 }
2516
2517 pub(crate) async fn commit_with_permit_measured(
2519 &mut self,
2520 permit: crate::PublicationPermit,
2521 budget: crate::WorkBudget,
2522 cancellation: &crate::CancellationToken,
2523 ) -> Result<crate::OperationReceipt<TransactionCommit<A, O>>, WorkspaceError> {
2524 cancellation.check().map_err(WorkspaceError::from)?;
2525 if self.requires_rebase {
2526 let receipt = Box::pin(self.checkout.retry_stale_commit(
2527 self.idempotency_key.operation_id(),
2528 permit,
2529 budget,
2530 cancellation,
2531 ))
2532 .await
2533 .map_err(|failure| WorkspaceError::from(failure.error))?;
2534 let work = receipt.work;
2535 let Some(outcome) = receipt.value else {
2536 let actual = Box::pin(self.workspace.head_measured(
2537 work.remaining(budget).map_err(WorkspaceError::from)?,
2538 cancellation,
2539 ))
2540 .await?;
2541 return Ok(crate::OperationReceipt {
2542 value: TransactionCommit::Conflict {
2543 actual: actual.value,
2544 },
2545 work: work
2546 .checked_add(actual.work)
2547 .map_err(WorkspaceError::from)?,
2548 });
2549 };
2550 return Box::pin(self.commit_outcome_measured(outcome, work, budget, cancellation))
2551 .await;
2552 }
2553 let receipt = Box::pin(self.checkout.commit_with_permit(
2554 self.idempotency_key.operation_id(),
2555 permit,
2556 budget,
2557 cancellation,
2558 ))
2559 .await
2560 .map_err(|failure| WorkspaceError::from(failure.error))?;
2561 Box::pin(self.commit_outcome_measured(receipt.value, receipt.work, budget, cancellation))
2562 .await
2563 }
2564
2565 async fn commit_outcome_measured(
2566 &self,
2567 outcome: CheckoutCommitOutcome,
2568 work: crate::WorkCounters,
2569 budget: crate::WorkBudget,
2570 cancellation: &crate::CancellationToken,
2571 ) -> Result<crate::OperationReceipt<TransactionCommit<A, O>>, WorkspaceError> {
2572 let value = match outcome {
2573 CheckoutCommitOutcome::Committed { generation_id, .. } => {
2574 TransactionCommit::Committed(Generation {
2575 workspace: self.workspace.clone(),
2576 id: generation_id,
2577 })
2578 }
2579 CheckoutCommitOutcome::AlreadyCommitted { generation_id, .. } => {
2580 TransactionCommit::AlreadyCommitted(Generation {
2581 workspace: self.workspace.clone(),
2582 id: generation_id,
2583 })
2584 }
2585 CheckoutCommitOutcome::Conflict { .. } => {
2586 let actual = Box::pin(self.workspace.head_measured(
2587 work.remaining(budget).map_err(WorkspaceError::from)?,
2588 cancellation,
2589 ))
2590 .await?;
2591 return Ok(crate::OperationReceipt {
2592 value: TransactionCommit::Conflict {
2593 actual: actual.value,
2594 },
2595 work: work
2596 .checked_add(actual.work)
2597 .map_err(WorkspaceError::from)?,
2598 });
2599 }
2600 CheckoutCommitOutcome::Fenced { .. } => TransactionCommit::Fenced,
2601 CheckoutCommitOutcome::IdempotencyConflict { .. } => {
2602 TransactionCommit::IdempotencyConflict
2603 }
2604 };
2605 Ok(crate::OperationReceipt { value, work })
2606 }
2607
2608 pub async fn rebase(
2617 &mut self,
2618 maximum_conflicts: u32,
2619 ) -> Result<TransactionRebase<A, O>, WorkspaceError> {
2620 let decision = self
2621 .checkout
2622 .rebase_head(
2623 maximum_conflicts,
2624 crate::WorkBudget::UNBOUNDED,
2625 &crate::CancellationToken::new(),
2626 )
2627 .await
2628 .map_err(WorkspaceError::engine)?
2629 .value;
2630 Ok(match decision {
2631 crate::kernel::RebaseDecision::Safe { generation } => {
2632 self.requires_rebase = false;
2633 TransactionRebase::Rebased(Generation {
2634 workspace: self.workspace.clone(),
2635 id: generation,
2636 })
2637 }
2638 crate::kernel::RebaseDecision::Conflicted {
2639 conflicts,
2640 truncated,
2641 } => TransactionRebase::Conflicted {
2642 conflicts: conflicts
2643 .into_iter()
2644 .map(TransactionConflict::from_engine)
2645 .collect(),
2646 truncated,
2647 },
2648 })
2649 }
2650
2651 async fn apply(&mut self, operations: Vec<AuthoredMutation>) -> Result<(), WorkspaceError> {
2652 self.apply_measured(
2653 operations,
2654 crate::WorkBudget::UNBOUNDED,
2655 &crate::CancellationToken::new(),
2656 )
2657 .await
2658 .map(|_| ())
2659 }
2660
2661 async fn apply_measured(
2662 &mut self,
2663 operations: Vec<AuthoredMutation>,
2664 budget: crate::WorkBudget,
2665 cancellation: &crate::CancellationToken,
2666 ) -> Result<crate::WorkCounters, WorkspaceError> {
2667 self.checkout
2668 .apply_authored_transaction(operations, budget, cancellation)
2669 .await
2670 .map(|receipt| receipt.work)
2671 .map_err(|failure| WorkspaceError::from(failure.error))
2672 }
2673}
2674
2675pub enum TransactionCommit<A, O> {
2677 Committed(Generation<A, O>),
2679 AlreadyCommitted(Generation<A, O>),
2681 Conflict {
2683 actual: Generation<A, O>,
2685 },
2686 Fenced,
2688 IdempotencyConflict,
2690}
2691
2692pub enum TransactionRebase<A, O> {
2694 Rebased(Generation<A, O>),
2697 Conflicted {
2699 conflicts: Vec<TransactionConflict>,
2701 truncated: bool,
2703 },
2704}
2705
2706pub enum WorkspaceRebase<A, O> {
2708 Rebased(Generation<A, O>),
2710 AlreadyRebased(Generation<A, O>),
2712 Current(Generation<A, O>),
2714 Stale(Generation<A, O>),
2716 Conflicted {
2718 conflicts: Vec<MergeConflict>,
2720 truncated: bool,
2722 },
2723 Fenced,
2725 IdempotencyConflict,
2727}
2728
2729#[derive(Clone, Debug, Eq, PartialEq)]
2731pub struct TransactionConflict {
2732 pub region: TransactionConflictRegion,
2734 pub usage: TransactionDependencyUse,
2736 pub expected: Option<crate::Digest>,
2738 pub actual: Option<crate::Digest>,
2740}
2741
2742#[derive(Clone, Debug, Eq, PartialEq)]
2744pub enum TransactionConflictRegion {
2745 FileRecord(crate::FileId),
2747 Metadata(crate::FileId),
2749 FileLength(crate::FileId),
2751 ContentRange {
2753 file_id: crate::FileId,
2755 offset: u64,
2757 length: u64,
2759 },
2760 SparseSeek {
2762 file_id: crate::FileId,
2764 offset: u64,
2766 target: TransactionSparseSeek,
2768 },
2769 DirectoryName {
2771 directory_id: crate::FileId,
2773 name: LogicalName,
2775 },
2776 DirectoryRange {
2778 directory_id: crate::FileId,
2780 after: Option<LogicalName>,
2782 maximum_entries: u32,
2784 },
2785}
2786
2787#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2789pub enum TransactionDependencyUse {
2790 Observation,
2792 Mutation,
2794 ObservationAndMutation,
2796}
2797
2798#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2800pub enum TransactionSparseSeek {
2801 Data,
2803 Hole,
2805}
2806
2807impl TransactionConflict {
2808 fn from_engine(value: crate::kernel::RebaseConflict) -> Self {
2809 use crate::kernel::{DependencyRegion, DependencyState, DependencyUse, ExtentSeekTarget};
2810 let state = |value| match value {
2811 DependencyState::Absent => None,
2812 DependencyState::Present(digest) => Some(digest),
2813 };
2814 Self {
2815 region: match value.region {
2816 DependencyRegion::FileRecord(id) => TransactionConflictRegion::FileRecord(id),
2817 DependencyRegion::Metadata(id) => TransactionConflictRegion::Metadata(id),
2818 DependencyRegion::FileLength(id) => TransactionConflictRegion::FileLength(id),
2819 DependencyRegion::ContentRange {
2820 file_id,
2821 offset,
2822 length,
2823 } => TransactionConflictRegion::ContentRange {
2824 file_id,
2825 offset,
2826 length,
2827 },
2828 DependencyRegion::SparseSeek {
2829 file_id,
2830 offset,
2831 target,
2832 } => TransactionConflictRegion::SparseSeek {
2833 file_id,
2834 offset,
2835 target: match target {
2836 ExtentSeekTarget::Data => TransactionSparseSeek::Data,
2837 ExtentSeekTarget::Hole => TransactionSparseSeek::Hole,
2838 },
2839 },
2840 DependencyRegion::DirectoryName { directory_id, name } => {
2841 TransactionConflictRegion::DirectoryName { directory_id, name }
2842 }
2843 DependencyRegion::DirectoryRange {
2844 directory_id,
2845 after,
2846 maximum_entries,
2847 } => TransactionConflictRegion::DirectoryRange {
2848 directory_id,
2849 after,
2850 maximum_entries,
2851 },
2852 },
2853 usage: match value.usage {
2854 DependencyUse::Observation => TransactionDependencyUse::Observation,
2855 DependencyUse::Mutation => TransactionDependencyUse::Mutation,
2856 DependencyUse::ObservationAndMutation => {
2857 TransactionDependencyUse::ObservationAndMutation
2858 }
2859 },
2860 expected: state(value.expected),
2861 actual: state(value.actual),
2862 }
2863 }
2864}
2865
2866pub struct Generation<A, O> {
2868 pub(crate) workspace: Workspace<A, O>,
2869 pub(crate) id: GenerationId,
2870}
2871
2872pub struct WorkspaceSync<A, O> {
2874 generation: Generation<A, O>,
2875}
2876
2877impl<A, O> WorkspaceSync<A, O> {
2878 #[must_use]
2880 pub const fn generation(&self) -> &Generation<A, O> {
2881 &self.generation
2882 }
2883
2884 #[must_use]
2886 pub fn into_generation(self) -> Generation<A, O> {
2887 self.generation
2888 }
2889}
2890
2891pub struct Checkpoint<A, O> {
2893 label: WorkspaceName,
2894 generation: Generation<A, O>,
2895}
2896
2897pub struct GenerationPin<A, O> {
2899 identity: WorkspaceName,
2900 generation: Generation<A, O>,
2901}
2902
2903#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2905pub enum WorkspaceDelete {
2906 Deleted,
2908 AlreadyDeleted,
2910 Conflict,
2912 IdempotencyConflict,
2914}
2915
2916#[derive(Clone, Debug, Eq, PartialEq)]
2918pub struct WorkspaceStat {
2919 pub file_id: crate::FileId,
2921 pub kind: FileKind,
2923 pub link_count: u64,
2925 pub logical_bytes: Option<u64>,
2927 pub metadata: WorkspaceMetadata,
2929}
2930
2931#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
2937pub struct WorkspaceMetadata {
2938 pub posix_mode: Option<u32>,
2940 pub posix_uid: Option<u32>,
2942 pub posix_gid: Option<u32>,
2944 pub posix_flags: Option<u64>,
2946 pub windows_attributes: Option<u32>,
2948 pub created_ns: Option<i64>,
2950 pub modified_ns: Option<i64>,
2952 pub accessed_ns: Option<i64>,
2954 pub changed_ns: Option<i64>,
2956 pub has_named_attributes: bool,
2958 pub has_acl: bool,
2960 pub has_security_descriptor: bool,
2962}
2963
2964impl WorkspaceMetadata {
2965 pub(crate) fn from_engine(metadata: FileMetadata) -> Self {
2966 Self {
2967 posix_mode: metadata_value(metadata.posix_mode),
2968 posix_uid: metadata_value(metadata.posix_uid),
2969 posix_gid: metadata_value(metadata.posix_gid),
2970 posix_flags: metadata_value(metadata.posix_flags),
2971 windows_attributes: metadata_value(metadata.windows_attributes),
2972 created_ns: metadata_value(metadata.created_ns),
2973 modified_ns: metadata_value(metadata.modified_ns),
2974 accessed_ns: metadata_value(metadata.accessed_ns),
2975 changed_ns: metadata_value(metadata.changed_ns),
2976 has_named_attributes: matches!(metadata.named_attributes, MetadataField::Value(_)),
2977 has_acl: matches!(metadata.acl, MetadataField::Value(_)),
2978 has_security_descriptor: matches!(
2979 metadata.security_descriptor,
2980 MetadataField::Value(_)
2981 ),
2982 }
2983 }
2984}
2985
2986const fn metadata_value<T: Copy>(field: MetadataField<T>) -> Option<T> {
2987 match field {
2988 MetadataField::Unavailable => None,
2989 MetadataField::Value(value) => Some(value),
2990 }
2991}
2992
2993#[derive(Clone, Debug, Eq, PartialEq)]
2995pub struct WorkspaceDirectoryEntry {
2996 pub name: LogicalName,
2998 pub file_id: crate::FileId,
3000 pub kind: FileKind,
3002}
3003
3004#[derive(Clone, Debug, Eq, PartialEq)]
3006pub struct WorkspaceDirectoryPage {
3007 pub entries: Vec<WorkspaceDirectoryEntry>,
3009 pub has_more: bool,
3011}
3012
3013#[derive(Clone, Copy, Debug, Eq, PartialEq)]
3015pub enum WorkspaceExtentKind {
3016 Hole,
3018 AllocatedZero,
3020 Content,
3022}
3023
3024#[derive(Clone, Copy, Debug, Eq, PartialEq)]
3026pub struct WorkspaceExtentSpan {
3027 pub offset: u64,
3029 pub length: u64,
3031 pub source_end: u64,
3033 pub kind: WorkspaceExtentKind,
3035}
3036
3037#[derive(Clone, Debug, Eq, PartialEq)]
3039pub struct WorkspaceExtentPlan {
3040 pub spans: Vec<WorkspaceExtentSpan>,
3042}
3043
3044pub struct ChangeSet<A, O> {
3046 from: Generation<A, O>,
3047 to: Generation<A, O>,
3048 changes: GenerationDiff,
3049 work: crate::WorkCounters,
3050}
3051
3052#[derive(Clone, Debug, Eq, PartialEq)]
3054pub struct ChangedPath {
3055 pub path: NamespacePath,
3057 pub before: Option<crate::kernel::FileRecord>,
3059 pub after: Option<crate::kernel::FileRecord>,
3061}
3062
3063impl<A, O> ChangeSet<A, O> {
3064 #[must_use]
3066 pub const fn from(&self) -> &Generation<A, O> {
3067 &self.from
3068 }
3069
3070 #[must_use]
3072 pub const fn to(&self) -> &Generation<A, O> {
3073 &self.to
3074 }
3075
3076 #[must_use]
3078 pub const fn changes(&self) -> &GenerationDiff {
3079 &self.changes
3080 }
3081
3082 #[must_use]
3084 pub const fn work(&self) -> crate::WorkCounters {
3085 self.work
3086 }
3087}
3088
3089fn merge_binding_endpoint(
3090 paths: &mut BTreeMap<
3091 NamespacePath,
3092 (
3093 Option<crate::kernel::FileRecord>,
3094 Option<crate::kernel::FileRecord>,
3095 ),
3096 >,
3097 directories: &BTreeMap<FileId, Vec<(NamespacePath, crate::kernel::FileRecord)>>,
3098 records: &BTreeMap<FileId, crate::kernel::FileRecord>,
3099 change: &crate::DirectoryBindingChange,
3100 limits: crate::model::VolumeLimits,
3101 before: bool,
3102) -> Result<(), WorkspaceError> {
3103 let Some(directories) = directories.get(&change.directory_id) else {
3104 return Ok(());
3105 };
3106 let binding = if before {
3107 change.before.as_ref()
3108 } else {
3109 change.after.as_ref()
3110 };
3111 let record = binding.and_then(|entry| records.get(&entry.file_id).copied());
3112 for (directory, _) in directories {
3113 let path = child_namespace_path(directory, change.name.clone(), limits)?;
3114 let endpoints = paths.entry(path).or_default();
3115 if before {
3116 endpoints.0 = record;
3117 } else {
3118 endpoints.1 = record;
3119 }
3120 }
3121 Ok(())
3122}
3123
3124struct NamespaceRecordSearch {
3125 records: BTreeMap<FileId, Vec<(NamespacePath, crate::kernel::FileRecord)>>,
3126 complete: bool,
3127}
3128
3129impl<A: AsyncAuthorityStore, O: AsyncObjectStore> ChangeSet<A, O> {
3130 pub async fn changed_paths(
3138 &self,
3139 maximum_entries: u32,
3140 ) -> Result<Vec<ChangedPath>, WorkspaceError> {
3141 self.changed_paths_bounded(
3142 maximum_entries,
3143 crate::WorkBudget::UNBOUNDED,
3144 &crate::CancellationToken::new(),
3145 )
3146 .await
3147 .map(|receipt| receipt.value)
3148 }
3149
3150 #[allow(clippy::too_many_lines)]
3153 pub async fn changed_paths_bounded(
3154 &self,
3155 maximum_entries: u32,
3156 budget: crate::WorkBudget,
3157 cancellation: &crate::CancellationToken,
3158 ) -> Result<crate::OperationReceipt<Vec<ChangedPath>>, WorkspaceError> {
3159 if self.changes.truncated {
3160 return Err(WorkspaceError::ChangedPathLimit);
3161 }
3162 let file_ids: BTreeSet<_> = self
3163 .changes
3164 .files
3165 .iter()
3166 .map(|change| change.file_id)
3167 .chain(
3168 self.changes
3169 .bindings
3170 .iter()
3171 .map(|change| change.directory_id),
3172 )
3173 .chain(self.changes.bindings.iter().flat_map(|change| {
3174 change
3175 .before
3176 .iter()
3177 .chain(change.after.iter())
3178 .map(|entry| entry.file_id)
3179 }))
3180 .collect();
3181 self.work.verify(budget).map_err(WorkspaceError::engine)?;
3182 let before_receipt = self
3183 .from
3184 .namespace_records_for_file_ids_bounded(
3185 file_ids.iter().copied(),
3186 maximum_entries,
3187 self.work
3188 .remaining(budget)
3189 .map_err(WorkspaceError::engine)?,
3190 cancellation,
3191 )
3192 .await?;
3193 let mut work = self
3194 .work
3195 .checked_add(before_receipt.work)
3196 .map_err(WorkspaceError::engine)?;
3197 let after_receipt = self
3198 .to
3199 .namespace_records_for_file_ids_bounded(
3200 file_ids.iter().copied(),
3201 maximum_entries,
3202 work.remaining(budget).map_err(WorkspaceError::engine)?,
3203 cancellation,
3204 )
3205 .await?;
3206 work = work
3207 .checked_add(after_receipt.work)
3208 .map_err(WorkspaceError::engine)?;
3209 let before = before_receipt.value;
3210 let after = after_receipt.value;
3211 if !before.complete || !after.complete {
3212 return Err(WorkspaceError::ChangedPathLimit);
3213 }
3214 let before = before.records;
3215 let after = after.records;
3216 let mut paths = BTreeMap::<NamespacePath, (Option<_>, Option<_>)>::new();
3217 let before_records: BTreeMap<_, _> = before
3218 .iter()
3219 .filter_map(|(&file_id, records)| records.first().map(|(_, record)| (file_id, *record)))
3220 .collect();
3221 let after_records: BTreeMap<_, _> = after
3222 .iter()
3223 .filter_map(|(&file_id, records)| records.first().map(|(_, record)| (file_id, *record)))
3224 .collect();
3225 for records in before.values() {
3226 for (path, record) in records {
3227 paths.entry(path.clone()).or_default().0 = Some(*record);
3228 }
3229 }
3230 for records in after.values() {
3231 for (path, record) in records {
3232 paths.entry(path.clone()).or_default().1 = Some(*record);
3233 }
3234 }
3235 for change in &self.changes.bindings {
3236 merge_binding_endpoint(
3237 &mut paths,
3238 &before,
3239 &before_records,
3240 change,
3241 self.from.workspace.volume.config.limits,
3242 true,
3243 )?;
3244 merge_binding_endpoint(
3245 &mut paths,
3246 &after,
3247 &after_records,
3248 change,
3249 self.to.workspace.volume.config.limits,
3250 false,
3251 )?;
3252 }
3253 let paths = paths
3254 .into_iter()
3255 .filter(|(_, (before, after))| match (before, after) {
3256 (Some(before), Some(after)) => {
3257 before.kind != after.kind
3258 || before.metadata != after.metadata
3259 || (before.kind != FileKind::Directory && before.payload != after.payload)
3260 }
3261 (None, None) => false,
3262 _ => true,
3263 })
3264 .map(|(path, (before, after))| ChangedPath {
3265 path,
3266 before,
3267 after,
3268 })
3269 .collect();
3270 Ok(crate::OperationReceipt { value: paths, work })
3271 }
3272
3273 pub async fn compose(
3284 &self,
3285 next: &ChangeSet<A, O>,
3286 maximum_changes: u32,
3287 ) -> Result<ChangeSet<A, O>, WorkspaceError> {
3288 if self.to.id != next.from.id
3289 || self.to.workspace.id != next.from.workspace.id
3290 || !self
3291 .to
3292 .workspace
3293 .volume
3294 .fs
3295 .same_deployment(&next.from.workspace.volume.fs)
3296 {
3297 return Err(WorkspaceError::ChangeSetContinuity);
3298 }
3299 self.from
3300 .workspace
3301 .diff(&self.from, &next.to, maximum_changes)
3302 .await
3303 }
3304}
3305
3306#[derive(Clone, Copy, Debug, Eq, PartialEq)]
3308pub enum JoinHistory {
3309 Merge,
3311 Rebase,
3313 Squash,
3315 CherryPick,
3317}
3318
3319pub struct JoinBuilder<A, O> {
3321 source: Workspace<A, O>,
3322 target: Workspace<A, O>,
3323 history: JoinHistory,
3324 maximum_generations: u32,
3325 maximum_changes: u32,
3326 maximum_conflicts: u32,
3327}
3328
3329impl<A, O> JoinBuilder<A, O> {
3330 fn validate_bounds(&self) -> Result<(), WorkspaceError> {
3331 if self.maximum_generations == 0 || self.maximum_changes == 0 || self.maximum_conflicts == 0
3332 {
3333 Err(WorkspaceError::JoinLimit)
3334 } else {
3335 Ok(())
3336 }
3337 }
3338
3339 #[must_use]
3341 pub const fn history(mut self, history: JoinHistory) -> Self {
3342 self.history = history;
3343 self
3344 }
3345
3346 #[must_use]
3348 pub const fn bounds(
3349 mut self,
3350 maximum_generations: u32,
3351 maximum_changes: u32,
3352 maximum_conflicts: u32,
3353 ) -> Self {
3354 self.maximum_generations = maximum_generations;
3355 self.maximum_changes = maximum_changes;
3356 self.maximum_conflicts = maximum_conflicts;
3357 self
3358 }
3359}
3360
3361impl<A: AsyncAuthorityStore, O: AsyncObjectStore> JoinBuilder<A, O> {
3362 pub async fn plan(self) -> Result<JoinPlan<A, O>, WorkspaceError> {
3370 self.validate_bounds()?;
3371 let source_head = self.source.head().await?;
3372 let (target_head_id, target_authority_head) = self
3373 .target
3374 .volume
3375 .fs
3376 .workspace_head_state(&self.target.volume)
3377 .await?;
3378 let target_head = Generation {
3379 workspace: self.target.clone(),
3380 id: target_head_id,
3381 };
3382 self.plan_generations(source_head, target_head, target_authority_head)
3383 .await
3384 }
3385
3386 pub async fn plan_pinned(
3392 self,
3393 source_head: Generation<A, O>,
3394 target_head: Generation<A, O>,
3395 ) -> Result<JoinPlan<A, O>, WorkspaceError> {
3396 self.validate_bounds()?;
3397 if source_head.workspace.id != self.source.id || target_head.workspace.id != self.target.id
3398 {
3399 return Err(WorkspaceError::ForeignGeneration);
3400 }
3401 let (current_target, target_authority_head) = self
3402 .target
3403 .volume
3404 .fs
3405 .workspace_head_state(&self.target.volume)
3406 .await?;
3407 if current_target != target_head.id {
3408 return Err(WorkspaceError::StaleGeneration);
3409 }
3410 self.plan_generations(source_head, target_head, target_authority_head)
3411 .await
3412 }
3413
3414 pub(crate) async fn plan_generations(
3415 self,
3416 source_head: Generation<A, O>,
3417 target_head: Generation<A, O>,
3418 target_authority_head: crate::Head,
3419 ) -> Result<JoinPlan<A, O>, WorkspaceError> {
3420 self.validate_bounds()?;
3421 if source_head.workspace.id != self.source.id || target_head.workspace.id != self.target.id
3422 {
3423 return Err(WorkspaceError::ForeignGeneration);
3424 }
3425 let base = self
3426 .source
3427 .volume
3428 .fs
3429 .workspace_common_ancestor(&source_head, &target_head, self.maximum_generations)
3430 .await?;
3431 Ok(JoinPlan {
3432 source: self.source,
3433 target: self.target,
3434 source_head,
3435 target_head,
3436 target_authority_head,
3437 base,
3438 history: self.history,
3439 maximum_generations: self.maximum_generations,
3440 maximum_changes: self.maximum_changes,
3441 maximum_conflicts: self.maximum_conflicts,
3442 })
3443 }
3444}
3445
3446pub struct JoinPlan<A, O> {
3448 source: Workspace<A, O>,
3449 target: Workspace<A, O>,
3450 source_head: Generation<A, O>,
3451 target_head: Generation<A, O>,
3452 target_authority_head: crate::Head,
3453 base: crate::facade::WorkspaceCommonAncestor,
3454 history: JoinHistory,
3455 maximum_generations: u32,
3456 maximum_changes: u32,
3457 maximum_conflicts: u32,
3458}
3459
3460impl<A, O> JoinPlan<A, O> {
3461 #[must_use]
3463 pub const fn source_head(&self) -> GenerationId {
3464 self.source_head.id
3465 }
3466
3467 #[must_use]
3469 pub const fn target_head(&self) -> GenerationId {
3470 self.target_head.id
3471 }
3472
3473 #[must_use]
3475 pub const fn common_ancestor(&self) -> GenerationId {
3476 self.base.id
3477 }
3478
3479 #[must_use]
3481 #[cfg(not(target_arch = "wasm32"))]
3482 pub(crate) const fn common_ancestor_volume(&self) -> VolumeId {
3483 self.base.volume_id
3484 }
3485
3486 #[must_use]
3488 pub const fn source(&self) -> &Workspace<A, O> {
3489 &self.source
3490 }
3491}
3492
3493#[derive(Clone, Copy, Debug, Eq, PartialEq)]
3495pub struct ApplyOptions {
3496 pub if_target: GenerationId,
3498 pub idempotency_key: IdempotencyKey,
3500}
3501
3502pub enum JoinOutcome<A, O> {
3504 Applied(Generation<A, O>),
3506 AlreadyApplied(Generation<A, O>),
3508 NoChanges(Generation<A, O>),
3510 StaleTarget(Generation<A, O>),
3512 Conflicted {
3514 conflicts: Vec<MergeConflict>,
3516 truncated: bool,
3518 },
3519 Fenced,
3521 IdempotencyConflict,
3523}
3524
3525pub(crate) enum WorkspaceRestoreOutcome {
3526 Restored(GenerationId),
3527 AlreadyRestored(GenerationId),
3528 Current(GenerationId),
3529 Stale(GenerationId),
3530 Fenced,
3531 IdempotencyConflict,
3532}
3533
3534pub enum WorkspaceRestore<A, O> {
3536 Restored(Generation<A, O>),
3538 AlreadyRestored(Generation<A, O>),
3540 Current(Generation<A, O>),
3542 Stale(Generation<A, O>),
3544 Fenced,
3546 IdempotencyConflict,
3548}
3549
3550#[derive(Clone, Debug, Eq, PartialEq)]
3552pub struct WorkspacePathConflict {
3553 pub path: String,
3555 pub kind: ConflictKind,
3557}
3558
3559pub enum WorkspacePathApply<A, O> {
3561 Applied(Generation<A, O>),
3563 AlreadyApplied(Generation<A, O>),
3565 NoChanges(Generation<A, O>),
3567 Conflicted(Vec<WorkspacePathConflict>),
3569 Stale(Generation<A, O>),
3571 Fenced,
3573 IdempotencyConflict,
3575}
3576
3577#[derive(Debug, Error)]
3579pub enum DrivenJoinError {
3580 #[error(transparent)]
3582 Workspace(#[from] WorkspaceError),
3583 #[error(transparent)]
3585 Resolution(#[from] MergePlanResolutionError),
3586 #[error("merge candidate does not match the immutable workspace join plan")]
3588 StaleCandidate,
3589 #[error("merge resolution for {0:?} is not representable by this join")]
3591 UnsupportedResolution(ConflictKey),
3592}
3593
3594impl<A: AsyncAuthorityStore, O: AsyncObjectStore> JoinPlan<A, O> {
3595 #[cfg(not(target_arch = "wasm32"))]
3596 pub(crate) async fn source_changes(
3597 &self,
3598 ) -> Result<crate::FsReceipt<GenerationDiff>, WorkspaceError> {
3599 self.source
3600 .volume
3601 .fs
3602 .workspace_join_changes(self.base.id, &self.source_head, self.maximum_changes)
3603 .await
3604 }
3605
3606 pub async fn apply(&self, options: ApplyOptions) -> Result<JoinOutcome<A, O>, WorkspaceError> {
3613 self.apply_with_permit(options, crate::PublicationPermit::Unrestricted)
3614 .await
3615 }
3616
3617 pub async fn apply_with_permit(
3619 &self,
3620 options: ApplyOptions,
3621 permit: crate::PublicationPermit,
3622 ) -> Result<JoinOutcome<A, O>, WorkspaceError> {
3623 self.apply_resolutions(options, BTreeMap::new(), permit)
3624 .await
3625 }
3626
3627 pub async fn describe_conflicts(
3632 &self,
3633 conflicts: &[MergeConflict],
3634 truncated: bool,
3635 ) -> Result<MergePlan, WorkspaceError> {
3636 self.typed_merge_plan(conflicts.to_vec(), truncated).await
3637 }
3638
3639 pub async fn apply_with_drivers<C: MergeResolutionCache>(
3642 &self,
3643 options: ApplyOptions,
3644 registry: &MergeDriverRegistry,
3645 cache: &mut C,
3646 replanning: bool,
3647 ) -> Result<JoinOutcome<A, O>, DrivenJoinError> {
3648 self.apply_with_drivers_and_permit(
3649 options,
3650 registry,
3651 cache,
3652 replanning,
3653 crate::PublicationPermit::Unrestricted,
3654 )
3655 .await
3656 }
3657
3658 pub async fn apply_with_drivers_and_permit<C: MergeResolutionCache>(
3660 &self,
3661 options: ApplyOptions,
3662 registry: &MergeDriverRegistry,
3663 cache: &mut C,
3664 replanning: bool,
3665 permit: crate::PublicationPermit,
3666 ) -> Result<JoinOutcome<A, O>, DrivenJoinError> {
3667 let initial = self.apply_with_permit(options, permit).await?;
3668 let JoinOutcome::Conflicted {
3669 conflicts,
3670 truncated,
3671 } = initial
3672 else {
3673 return Ok(initial);
3674 };
3675 let plan = self.typed_merge_plan(conflicts, truncated).await?;
3676 let candidate = resolve_merge_plan(plan, registry, cache, replanning)?;
3677 self.apply_candidate_with_permit(options, &candidate, permit)
3678 .await
3679 }
3680
3681 pub async fn apply_sides(
3684 &self,
3685 options: ApplyOptions,
3686 selections: BTreeMap<MergeConflict, ConflictSide>,
3687 ) -> Result<JoinOutcome<A, O>, DrivenJoinError> {
3688 let initial = self.apply(options).await?;
3689 let JoinOutcome::Conflicted {
3690 conflicts,
3691 truncated,
3692 } = initial
3693 else {
3694 return Ok(initial);
3695 };
3696 if truncated
3697 || conflicts.len() != selections.len()
3698 || conflicts
3699 .iter()
3700 .any(|conflict| !selections.contains_key(conflict))
3701 {
3702 return Err(DrivenJoinError::StaleCandidate);
3703 }
3704 let resolutions = selections
3705 .into_iter()
3706 .map(|(conflict, side)| {
3707 let side = match side {
3708 ConflictSide::Base => crate::kernel::MergeConflictSide::Base,
3709 ConflictSide::Ours => crate::kernel::MergeConflictSide::Ours,
3710 ConflictSide::Theirs => crate::kernel::MergeConflictSide::Theirs,
3711 };
3712 (
3713 conflict,
3714 crate::kernel::MergeConflictResolution::Select(side),
3715 )
3716 })
3717 .collect();
3718 self.apply_resolutions(options, resolutions, crate::PublicationPermit::Unrestricted)
3719 .await
3720 .map_err(Into::into)
3721 }
3722
3723 pub async fn apply_candidate(
3726 &self,
3727 options: ApplyOptions,
3728 candidate: &UnpublishedMergeCandidate,
3729 ) -> Result<JoinOutcome<A, O>, DrivenJoinError> {
3730 self.apply_candidate_with_permit(options, candidate, crate::PublicationPermit::Unrestricted)
3731 .await
3732 }
3733
3734 pub async fn apply_candidate_with_permit(
3736 &self,
3737 options: ApplyOptions,
3738 candidate: &UnpublishedMergeCandidate,
3739 permit: crate::PublicationPermit,
3740 ) -> Result<JoinOutcome<A, O>, DrivenJoinError> {
3741 let conflict_keys = candidate
3742 .plan
3743 .conflicts
3744 .iter()
3745 .map(|conflict| &conflict.key)
3746 .collect::<BTreeSet<_>>();
3747 if candidate.plan.base != self.base.id
3748 || candidate.plan.ours != self.target_head.id
3749 || candidate.plan.theirs != self.source_head.id
3750 || candidate.plan.truncated
3751 || candidate.plan.conflicts.len() != candidate.resolutions.len()
3752 || conflict_keys.len() != candidate.plan.conflicts.len()
3753 || !candidate.resolutions.keys().eq(conflict_keys.into_iter())
3754 {
3755 return Err(DrivenJoinError::StaleCandidate);
3756 }
3757 let mut resolutions = BTreeMap::new();
3758 for conflict in &candidate.plan.conflicts {
3759 let resolution = candidate
3760 .resolutions
3761 .get(&conflict.key)
3762 .ok_or(DrivenJoinError::StaleCandidate)?;
3763 if !resolution_matches_conflict(&conflict.key, resolution) {
3764 return Err(DrivenJoinError::UnsupportedResolution(conflict.key.clone()));
3765 }
3766 let resolution = match resolution {
3767 MergeResolution::Select(ConflictSide::Base) => {
3768 crate::kernel::MergeConflictResolution::Select(
3769 crate::kernel::MergeConflictSide::Base,
3770 )
3771 }
3772 MergeResolution::Select(ConflictSide::Ours) => {
3773 crate::kernel::MergeConflictResolution::Select(
3774 crate::kernel::MergeConflictSide::Ours,
3775 )
3776 }
3777 MergeResolution::Select(ConflictSide::Theirs) => {
3778 crate::kernel::MergeConflictResolution::Select(
3779 crate::kernel::MergeConflictSide::Theirs,
3780 )
3781 }
3782 MergeResolution::Text(text) => crate::kernel::MergeConflictResolution::File(Some(
3783 self.stage_regular_resolution(
3784 &conflict.key,
3785 Bytes::copy_from_slice(text.as_bytes()),
3786 )
3787 .await?,
3788 )),
3789 MergeResolution::Binary(bytes) => {
3790 crate::kernel::MergeConflictResolution::File(Some(
3791 self.stage_regular_resolution(&conflict.key, Bytes::copy_from_slice(bytes))
3792 .await?,
3793 ))
3794 }
3795 MergeResolution::Metadata(bytes) => {
3796 let file_id = conflict_file_id(&conflict.key).ok_or_else(|| {
3797 DrivenJoinError::UnsupportedResolution(conflict.key.clone())
3798 })?;
3799 let mut record = self
3800 .target
3801 .volume
3802 .fs
3803 .workspace_conflict_record(
3804 &self.target.volume,
3805 self.target_head.id,
3806 file_id,
3807 )
3808 .await?
3809 .ok_or(WorkspaceError::InvalidMergeResolution)?;
3810 record.metadata = self
3811 .target
3812 .volume
3813 .fs
3814 .stage_merge_metadata(&self.target.volume, bytes)
3815 .await?;
3816 crate::kernel::MergeConflictResolution::File(Some(record))
3817 }
3818 MergeResolution::Binding(file_id) => {
3819 crate::kernel::MergeConflictResolution::Binding(*file_id)
3820 }
3821 MergeResolution::Unresolved => {
3822 return Err(DrivenJoinError::UnsupportedResolution(conflict.key.clone()));
3823 }
3824 };
3825 resolutions.insert(
3826 kernel_conflict(
3827 &conflict.key,
3828 self.target.volume.config.limits.maximum_component_bytes,
3829 )?,
3830 resolution,
3831 );
3832 }
3833 self.apply_resolutions(options, resolutions, permit)
3834 .await
3835 .map_err(Into::into)
3836 }
3837
3838 async fn stage_regular_resolution(
3839 &self,
3840 key: &ConflictKey,
3841 bytes: Bytes,
3842 ) -> Result<crate::kernel::FileRecord, DrivenJoinError> {
3843 let file_id = conflict_file_id(key)
3844 .ok_or_else(|| DrivenJoinError::UnsupportedResolution(key.clone()))?;
3845 let record = self
3846 .target
3847 .volume
3848 .fs
3849 .workspace_conflict_record(&self.target.volume, self.target_head.id, file_id)
3850 .await?
3851 .ok_or(WorkspaceError::InvalidMergeResolution)?;
3852 self.target
3853 .volume
3854 .fs
3855 .stage_merge_regular_record(&self.target.volume, record, bytes)
3856 .await
3857 .map_err(Into::into)
3858 }
3859
3860 async fn typed_merge_plan(
3861 &self,
3862 conflicts: Vec<MergeConflict>,
3863 truncated: bool,
3864 ) -> Result<MergePlan, WorkspaceError> {
3865 let mut typed = Vec::with_capacity(conflicts.len());
3866 for conflict in conflicts {
3867 typed.push(self.conflict_view(conflict).await?);
3868 }
3869 Ok(MergePlan {
3870 base: self.base.id,
3871 ours: self.target_head.id,
3872 theirs: self.source_head.id,
3873 conflicts: typed,
3874 truncated,
3875 })
3876 }
3877
3878 async fn conflict_view(
3879 &self,
3880 conflict: MergeConflict,
3881 ) -> Result<crate::ConflictView, WorkspaceError> {
3882 let MergeConflict::File(file_id) = conflict else {
3883 let mut view = binding_conflict_view(self, conflict.clone());
3884 if let MergeConflict::Binding { directory_id, name } = conflict
3885 && let Some(directory) = self.conflict_path(directory_id).await?
3886 && let Ok(name) = logical_name_text(&name)
3887 {
3888 view.path = Some(if directory == "/" {
3889 format!("/{name}")
3890 } else {
3891 format!("{directory}/{name}")
3892 });
3893 }
3894 return Ok(view);
3895 };
3896 let path = self.conflict_path(file_id).await?;
3897 let fs = &self.target.volume.fs;
3898 let base = fs
3899 .workspace_conflict_record(&self.target.volume, self.base.id, file_id)
3900 .await?;
3901 let ours = fs
3902 .workspace_conflict_record(&self.target.volume, self.target_head.id, file_id)
3903 .await?;
3904 let theirs = fs
3905 .workspace_conflict_record(&self.target.volume, self.source_head.id, file_id)
3906 .await?;
3907 let fallback = |generation, record: Option<crate::kernel::FileRecord>| match record {
3908 Some(_) => ConflictValue::Record {
3909 generation,
3910 file_id,
3911 },
3912 None => ConflictValue::Absent,
3913 };
3914 let Some((base_record, ours_record, theirs_record)) = base
3915 .zip(ours)
3916 .zip(theirs)
3917 .map(|((base, ours), theirs)| (base, ours, theirs))
3918 else {
3919 return Ok(crate::ConflictView {
3920 key: ConflictKey::File(file_id),
3921 path,
3922 kind: ConflictKind::Record,
3923 base: fallback(self.base.id, base),
3924 ours: fallback(self.target_head.id, ours),
3925 theirs: fallback(self.source_head.id, theirs),
3926 });
3927 };
3928 if base_record.link_count != ours_record.link_count
3929 || base_record.link_count != theirs_record.link_count
3930 {
3931 return Ok(record_conflict_view(
3932 self,
3933 file_id,
3934 path,
3935 ConflictKind::HardLink,
3936 ));
3937 }
3938 if base_record.metadata != ours_record.metadata
3939 || base_record.metadata != theirs_record.metadata
3940 {
3941 let values = [base_record, ours_record, theirs_record];
3942 let mut metadata = Vec::with_capacity(3);
3943 for record in values {
3944 metadata.push(ConflictValue::Metadata(
3945 fs.workspace_conflict_metadata(&self.target.volume, record)
3946 .await?,
3947 ));
3948 }
3949 return Ok(crate::ConflictView {
3950 key: ConflictKey::Metadata(file_id),
3951 path,
3952 kind: ConflictKind::Metadata,
3953 base: metadata.remove(0),
3954 ours: metadata.remove(0),
3955 theirs: metadata.remove(0),
3956 });
3957 }
3958 if let Some(view) = self
3959 .regular_conflict_view(
3960 file_id,
3961 path.clone(),
3962 [base_record, ours_record, theirs_record],
3963 )
3964 .await?
3965 {
3966 return Ok(view);
3967 }
3968 let kind = match base_record.kind {
3969 FileKind::Directory => ConflictKind::Directory,
3970 FileKind::SymbolicLink => ConflictKind::SymbolicLink,
3971 _ => ConflictKind::Special,
3972 };
3973 Ok(record_conflict_view(self, file_id, path, kind))
3974 }
3975
3976 async fn conflict_path(&self, file_id: FileId) -> Result<Option<String>, WorkspaceError> {
3977 let maximum = self.maximum_changes.max(1);
3978 let mut paths = self.target_head.paths_for_file_id(file_id, maximum).await?;
3979 if paths.is_empty() {
3980 paths = self.source_head.paths_for_file_id(file_id, maximum).await?;
3981 }
3982 Ok(paths.into_iter().next())
3983 }
3984
3985 async fn regular_conflict_view(
3986 &self,
3987 file_id: FileId,
3988 path: Option<String>,
3989 records: [crate::kernel::FileRecord; 3],
3990 ) -> Result<Option<crate::ConflictView>, WorkspaceError> {
3991 if records
3992 .iter()
3993 .any(|record| record.kind != FileKind::Regular)
3994 {
3995 return Ok(None);
3996 }
3997 let mut bodies = Vec::with_capacity(3);
3998 for record in records {
3999 let Some(bytes) = self
4000 .target
4001 .volume
4002 .fs
4003 .workspace_conflict_bytes(&self.target.volume, record)
4004 .await?
4005 else {
4006 return Ok(Some(record_conflict_view(
4007 self,
4008 file_id,
4009 path,
4010 ConflictKind::Binary,
4011 )));
4012 };
4013 bodies.push(bytes);
4014 }
4015 let text = bodies
4016 .iter()
4017 .all(|bytes| std::str::from_utf8(bytes).is_ok());
4018 let mut values = bodies.into_iter().map(|bytes| {
4019 if text {
4020 ConflictValue::Text(String::from_utf8(bytes.to_vec()).unwrap_or_default())
4021 } else {
4022 ConflictValue::Binary(bytes.to_vec())
4023 }
4024 });
4025 Ok(Some(crate::ConflictView {
4026 key: ConflictKey::File(file_id),
4027 path,
4028 kind: if text {
4029 ConflictKind::Text
4030 } else {
4031 ConflictKind::Binary
4032 },
4033 base: values
4034 .next()
4035 .ok_or(WorkspaceError::InvalidMergeResolution)?,
4036 ours: values
4037 .next()
4038 .ok_or(WorkspaceError::InvalidMergeResolution)?,
4039 theirs: values
4040 .next()
4041 .ok_or(WorkspaceError::InvalidMergeResolution)?,
4042 }))
4043 }
4044
4045 async fn apply_resolutions(
4046 &self,
4047 options: ApplyOptions,
4048 resolutions: BTreeMap<MergeConflict, crate::kernel::MergeConflictResolution>,
4049 permit: crate::PublicationPermit,
4050 ) -> Result<JoinOutcome<A, O>, WorkspaceError> {
4051 if options.if_target != self.target_head.id {
4052 return Ok(JoinOutcome::StaleTarget(self.target.head().await?));
4053 }
4054 let outcome = self
4055 .target
4056 .volume
4057 .fs
4058 .apply_workspace_join(crate::facade::WorkspaceJoinRequest {
4059 target: &self.target.volume,
4060 base: self.base,
4061 source: &self.source_head,
4062 expected_target: options.if_target,
4063 expected_head: self.target_authority_head,
4064 history: self.history,
4065 operation_id: options.idempotency_key.operation_id(),
4066 permit,
4067 maximum_generations: self.maximum_generations,
4068 maximum_changes: self.maximum_changes,
4069 maximum_conflicts: self.maximum_conflicts,
4070 resolutions,
4071 })
4072 .await?;
4073 let generation = |id| Generation {
4074 workspace: self.target.clone(),
4075 id,
4076 };
4077 Ok(match outcome {
4078 crate::facade::WorkspaceJoinOutcome::Applied(id) => {
4079 JoinOutcome::Applied(generation(id))
4080 }
4081 crate::facade::WorkspaceJoinOutcome::AlreadyApplied(id) => {
4082 JoinOutcome::AlreadyApplied(generation(id))
4083 }
4084 crate::facade::WorkspaceJoinOutcome::NoChanges(id) => {
4085 JoinOutcome::NoChanges(generation(id))
4086 }
4087 crate::facade::WorkspaceJoinOutcome::Stale(id) => {
4088 JoinOutcome::StaleTarget(generation(id))
4089 }
4090 crate::facade::WorkspaceJoinOutcome::Conflicted(conflicts, truncated) => {
4091 JoinOutcome::Conflicted {
4092 conflicts,
4093 truncated,
4094 }
4095 }
4096 crate::facade::WorkspaceJoinOutcome::Fenced => JoinOutcome::Fenced,
4097 crate::facade::WorkspaceJoinOutcome::IdempotencyConflict => {
4098 JoinOutcome::IdempotencyConflict
4099 }
4100 })
4101 }
4102}
4103
4104fn binding_conflict_view<A, O>(
4105 plan: &JoinPlan<A, O>,
4106 conflict: MergeConflict,
4107) -> crate::ConflictView {
4108 match conflict {
4109 MergeConflict::File(file_id) => crate::ConflictView {
4110 key: ConflictKey::File(file_id),
4111 path: None,
4112 kind: ConflictKind::Record,
4113 base: ConflictValue::RecordReference {
4114 generation: plan.base.id,
4115 file_id,
4116 },
4117 ours: ConflictValue::RecordReference {
4118 generation: plan.target_head.id,
4119 file_id,
4120 },
4121 theirs: ConflictValue::RecordReference {
4122 generation: plan.source_head.id,
4123 file_id,
4124 },
4125 },
4126 MergeConflict::Binding { directory_id, name } => {
4127 let name = canonical_name_key(&name);
4128 let value = |generation| ConflictValue::BindingReference {
4129 generation,
4130 directory_id,
4131 name: name.clone(),
4132 };
4133 crate::ConflictView {
4134 key: ConflictKey::Binding {
4135 directory_id,
4136 name: name.clone(),
4137 },
4138 path: None,
4139 kind: ConflictKind::Binding,
4140 base: value(plan.base.id),
4141 ours: value(plan.target_head.id),
4142 theirs: value(plan.source_head.id),
4143 }
4144 }
4145 }
4146}
4147
4148fn record_conflict_view<A, O>(
4149 plan: &JoinPlan<A, O>,
4150 file_id: FileId,
4151 path: Option<String>,
4152 kind: ConflictKind,
4153) -> crate::ConflictView {
4154 let value = |generation| ConflictValue::Record {
4155 generation,
4156 file_id,
4157 };
4158 crate::ConflictView {
4159 key: ConflictKey::File(file_id),
4160 path,
4161 kind,
4162 base: value(plan.base.id),
4163 ours: value(plan.target_head.id),
4164 theirs: value(plan.source_head.id),
4165 }
4166}
4167
4168fn canonical_name_key(name: &LogicalName) -> Vec<u8> {
4169 let mut key = Vec::with_capacity(name.as_bytes().len().saturating_add(1));
4170 key.push(match name.encoding() {
4171 crate::kernel::NameEncoding::Utf8 => 1,
4172 crate::kernel::NameEncoding::PosixBytes => 2,
4173 crate::kernel::NameEncoding::WindowsUtf16Le => 3,
4174 });
4175 key.extend_from_slice(name.as_bytes());
4176 key
4177}
4178
4179fn logical_name_text(name: &LogicalName) -> Result<std::borrow::Cow<'_, str>, WorkspaceError> {
4180 name.unicode_text().ok_or_else(|| {
4181 WorkspaceError::path("non-Unicode path cannot be projected as a portable string")
4182 })
4183}
4184
4185fn namespace_path_text(path: &NamespacePath) -> Result<String, WorkspaceError> {
4186 if path.is_root() {
4187 return Ok("/".to_owned());
4188 }
4189 let components = path
4190 .components()
4191 .iter()
4192 .map(logical_name_text)
4193 .collect::<Result<Vec<_>, _>>()?;
4194 Ok(format!("/{}", components.join("/")))
4195}
4196
4197fn child_namespace_path(
4198 directory: &NamespacePath,
4199 name: LogicalName,
4200 limits: crate::model::VolumeLimits,
4201) -> Result<NamespacePath, WorkspaceError> {
4202 let mut components = directory.components().to_vec();
4203 components.push(name);
4204 NamespacePath::new(components, limits).map_err(WorkspaceError::path)
4205}
4206
4207fn kernel_conflict(
4208 key: &ConflictKey,
4209 maximum_component_bytes: u32,
4210) -> Result<MergeConflict, DrivenJoinError> {
4211 match key {
4212 ConflictKey::File(file_id)
4213 | ConflictKey::Metadata(file_id)
4214 | ConflictKey::HardLinks(file_id)
4215 | ConflictKey::Directory(file_id)
4216 | ConflictKey::SpecialPayload(file_id)
4217 | ConflictKey::ContentRange { file_id, .. }
4218 | ConflictKey::Rename { file_id, .. } => Ok(MergeConflict::File(*file_id)),
4219 ConflictKey::Binding { directory_id, name } => {
4220 let (&tag, bytes) = name.split_first().ok_or(DrivenJoinError::StaleCandidate)?;
4221 let encoding = match tag {
4222 1 => crate::kernel::NameEncoding::Utf8,
4223 2 => crate::kernel::NameEncoding::PosixBytes,
4224 3 => crate::kernel::NameEncoding::WindowsUtf16Le,
4225 _ => return Err(DrivenJoinError::StaleCandidate),
4226 };
4227 let name = LogicalName::new(encoding, bytes.to_vec(), maximum_component_bytes)
4228 .map_err(|_| DrivenJoinError::StaleCandidate)?;
4229 Ok(MergeConflict::Binding {
4230 directory_id: *directory_id,
4231 name,
4232 })
4233 }
4234 }
4235}
4236
4237fn conflict_file_id(key: &ConflictKey) -> Option<FileId> {
4238 match key {
4239 ConflictKey::File(file_id)
4240 | ConflictKey::Metadata(file_id)
4241 | ConflictKey::HardLinks(file_id)
4242 | ConflictKey::Directory(file_id)
4243 | ConflictKey::SpecialPayload(file_id)
4244 | ConflictKey::ContentRange { file_id, .. }
4245 | ConflictKey::Rename { file_id, .. } => Some(*file_id),
4246 ConflictKey::Binding { .. } => None,
4247 }
4248}
4249
4250fn resolution_matches_conflict(key: &ConflictKey, resolution: &MergeResolution) -> bool {
4251 match resolution {
4252 MergeResolution::Select(_) | MergeResolution::Unresolved => true,
4253 MergeResolution::Text(_) | MergeResolution::Binary(_) => {
4254 matches!(key, ConflictKey::File(_) | ConflictKey::ContentRange { .. })
4255 }
4256 MergeResolution::Metadata(_) => matches!(key, ConflictKey::Metadata(_)),
4257 MergeResolution::Binding(_) => matches!(key, ConflictKey::Binding { .. }),
4258 }
4259}
4260
4261#[cfg(test)]
4262mod merge_resolution_tests {
4263 use super::*;
4264
4265 #[test]
4266 fn synthesized_resolutions_match_only_their_exact_conflict_shape() {
4267 let file_id = FileId::new();
4268 let file = ConflictKey::File(file_id);
4269 let metadata = ConflictKey::Metadata(file_id);
4270 let binding = ConflictKey::Binding {
4271 directory_id: file_id,
4272 name: vec![1, b'x'],
4273 };
4274
4275 for resolution in [
4276 MergeResolution::Text("merged".into()),
4277 MergeResolution::Binary(vec![1]),
4278 ] {
4279 assert!(resolution_matches_conflict(&file, &resolution));
4280 assert!(!resolution_matches_conflict(&metadata, &resolution));
4281 assert!(!resolution_matches_conflict(&binding, &resolution));
4282 }
4283 let metadata_resolution = MergeResolution::Metadata(vec![1]);
4284 assert!(resolution_matches_conflict(&metadata, &metadata_resolution));
4285 assert!(!resolution_matches_conflict(&file, &metadata_resolution));
4286 let binding_resolution = MergeResolution::Binding(Some(file_id));
4287 assert!(resolution_matches_conflict(&binding, &binding_resolution));
4288 assert!(!resolution_matches_conflict(&file, &binding_resolution));
4289 assert!(resolution_matches_conflict(
4290 &file,
4291 &MergeResolution::Select(ConflictSide::Ours)
4292 ));
4293 }
4294}
4295
4296impl<A, O> GenerationPin<A, O> {
4297 #[must_use]
4299 pub fn identity(&self) -> &WorkspaceName {
4300 &self.identity
4301 }
4302
4303 #[must_use]
4305 pub const fn generation(&self) -> &Generation<A, O> {
4306 &self.generation
4307 }
4308}
4309
4310impl<A, O> Checkpoint<A, O> {
4311 #[must_use]
4313 pub fn label(&self) -> &WorkspaceName {
4314 &self.label
4315 }
4316
4317 #[must_use]
4319 pub const fn generation(&self) -> &Generation<A, O> {
4320 &self.generation
4321 }
4322}
4323
4324impl<A, O> Clone for Generation<A, O> {
4325 fn clone(&self) -> Self {
4326 Self {
4327 workspace: self.workspace.clone(),
4328 id: self.id,
4329 }
4330 }
4331}
4332
4333impl<A, O> Generation<A, O> {
4334 #[must_use]
4336 pub const fn id(&self) -> GenerationId {
4337 self.id
4338 }
4339
4340 #[must_use]
4342 pub const fn workspace_id(&self) -> WorkspaceId {
4343 self.workspace.id
4344 }
4345
4346 #[must_use]
4352 pub fn workspace(&self) -> Workspace<A, O> {
4353 self.workspace.clone()
4354 }
4355}
4356
4357impl<A: AsyncAuthorityStore, O: AsyncObjectStore> Generation<A, O> {
4358 pub async fn reader(&self) -> Result<crate::PinnedReader<A, O>, WorkspaceError> {
4370 self.workspace
4371 .engine_checkout(
4372 GenerationSelector::Exact(self.id),
4373 CheckoutMode::read_only_pinned(),
4374 )
4375 .await?
4376 .pinned_reader()
4377 .map_err(WorkspaceError::engine)
4378 }
4379
4380 pub async fn diff_to(
4383 &self,
4384 to: &Generation<A, O>,
4385 maximum_changes: u32,
4386 ) -> Result<ChangeSet<A, O>, WorkspaceError> {
4387 self.diff_to_bounded(
4388 to,
4389 maximum_changes,
4390 crate::WorkBudget::UNBOUNDED,
4391 &crate::CancellationToken::new(),
4392 )
4393 .await
4394 }
4395
4396 pub async fn diff_to_bounded(
4399 &self,
4400 to: &Generation<A, O>,
4401 maximum_changes: u32,
4402 budget: crate::WorkBudget,
4403 cancellation: &crate::CancellationToken,
4404 ) -> Result<ChangeSet<A, O>, WorkspaceError> {
4405 let receipt = self
4406 .workspace
4407 .volume
4408 .fs
4409 .workspace_join_changes_bounded(self.id, to, maximum_changes, budget, cancellation)
4410 .await?;
4411 Ok(ChangeSet {
4412 from: self.clone(),
4413 to: to.clone(),
4414 changes: receipt.value,
4415 work: receipt.work,
4416 })
4417 }
4418
4419 #[cfg(all(feature = "native-mount", not(target_arch = "wasm32")))]
4424 pub async fn materialize(
4425 &self,
4426 options: &crate::MaterializeOptions,
4427 budget: crate::WorkBudget,
4428 cancellation: &crate::CancellationToken,
4429 ) -> Result<crate::OperationReceipt<crate::MaterializationReceipt>, WorkspaceError> {
4430 self.materialize_with_mode(
4431 options,
4432 budget,
4433 cancellation,
4434 crate::native_mount::MaterializeMode::DurableOutput,
4435 )
4436 .await
4437 }
4438
4439 #[cfg(all(feature = "native-mount", not(target_arch = "wasm32")))]
4440 pub(crate) async fn materialize_with_mode(
4441 &self,
4442 options: &crate::MaterializeOptions,
4443 budget: crate::WorkBudget,
4444 cancellation: &crate::CancellationToken,
4445 mode: crate::native_mount::MaterializeMode,
4446 ) -> Result<crate::OperationReceipt<crate::MaterializationReceipt>, WorkspaceError> {
4447 let checkout = self
4448 .workspace
4449 .engine_checkout_measured(
4450 GenerationSelector::Exact(self.id),
4451 CheckoutMode::read_only_pinned(),
4452 budget,
4453 cancellation,
4454 )
4455 .await?;
4456 let mut value = checkout.value;
4457 let receipt = crate::native_mount::materialize_checkout_with_mode(
4458 &mut value,
4459 options,
4460 checkout
4461 .work
4462 .remaining(budget)
4463 .map_err(WorkspaceError::from)?,
4464 cancellation,
4465 mode,
4466 )
4467 .await
4468 .map_err(|failure| WorkspaceError::engine(failure.error))?;
4469 merge_workspace_work(checkout.work, receipt, budget)
4470 }
4471
4472 #[cfg(all(feature = "native-mount", not(target_arch = "wasm32")))]
4476 pub async fn materialize_path(
4477 &self,
4478 path: &str,
4479 options: &crate::MaterializeOptions,
4480 budget: crate::WorkBudget,
4481 cancellation: &crate::CancellationToken,
4482 ) -> Result<crate::OperationReceipt<crate::MaterializationReceipt>, WorkspaceError> {
4483 self.materialize_path_with_mode(
4484 path,
4485 options,
4486 budget,
4487 cancellation,
4488 crate::native_mount::MaterializeMode::DurableOutput,
4489 )
4490 .await
4491 }
4492
4493 #[cfg(all(feature = "native-mount", not(target_arch = "wasm32")))]
4494 pub(crate) async fn materialize_path_with_mode(
4495 &self,
4496 path: &str,
4497 options: &crate::MaterializeOptions,
4498 budget: crate::WorkBudget,
4499 cancellation: &crate::CancellationToken,
4500 mode: crate::native_mount::MaterializeMode,
4501 ) -> Result<crate::OperationReceipt<crate::MaterializationReceipt>, WorkspaceError> {
4502 let checkout = self
4503 .workspace
4504 .engine_checkout_measured(
4505 GenerationSelector::Exact(self.id),
4506 CheckoutMode::read_only_pinned(),
4507 budget,
4508 cancellation,
4509 )
4510 .await?;
4511 let mut value = checkout.value;
4512 let path = customer_path(path, value.volume_config())?;
4513 let receipt = crate::native_mount::materialize_checkout_paths_with_mode(
4514 &mut value,
4515 &[path],
4516 options,
4517 checkout
4518 .work
4519 .remaining(budget)
4520 .map_err(WorkspaceError::from)?,
4521 cancellation,
4522 mode,
4523 )
4524 .await
4525 .map_err(|failure| WorkspaceError::engine(failure.error))?;
4526 merge_workspace_work(checkout.work, receipt, budget)
4527 }
4528
4529 #[cfg(all(feature = "native-mount", not(target_arch = "wasm32")))]
4532 pub async fn materialize_paths(
4533 &self,
4534 paths: &[String],
4535 options: &crate::MaterializeOptions,
4536 budget: crate::WorkBudget,
4537 cancellation: &crate::CancellationToken,
4538 ) -> Result<crate::OperationReceipt<crate::MaterializationReceipt>, WorkspaceError> {
4539 let checkout = self
4540 .workspace
4541 .engine_checkout_measured(
4542 GenerationSelector::Exact(self.id),
4543 CheckoutMode::read_only_pinned(),
4544 budget,
4545 cancellation,
4546 )
4547 .await?;
4548 let mut value = checkout.value;
4549 let paths = paths
4550 .iter()
4551 .map(|path| customer_path(path, value.volume_config()))
4552 .collect::<Result<Vec<_>, _>>()?;
4553 let receipt = crate::materialize_checkout_paths(
4554 &mut value,
4555 &paths,
4556 options,
4557 checkout
4558 .work
4559 .remaining(budget)
4560 .map_err(WorkspaceError::from)?,
4561 cancellation,
4562 )
4563 .await
4564 .map_err(|failure| WorkspaceError::engine(failure.error))?;
4565 merge_workspace_work(checkout.work, receipt, budget)
4566 }
4567
4568 #[cfg(all(feature = "native-mount", not(target_arch = "wasm32")))]
4573 pub async fn restore_host_path(
4574 &self,
4575 relative: &std::path::Path,
4576 replacement: crate::HostPathReplacement,
4577 options: &crate::MaterializeOptions,
4578 budget: crate::WorkBudget,
4579 cancellation: &crate::CancellationToken,
4580 ) -> Result<crate::OperationReceipt<crate::HostPathRestore>, WorkspaceError> {
4581 let checkout = self
4582 .workspace
4583 .engine_checkout_measured(
4584 GenerationSelector::Exact(self.id),
4585 CheckoutMode::read_only_pinned(),
4586 budget,
4587 cancellation,
4588 )
4589 .await?;
4590 let mut value = checkout.value;
4591 let receipt = crate::restore_checkout_host_path(
4592 &mut value,
4593 relative,
4594 replacement,
4595 options,
4596 checkout
4597 .work
4598 .remaining(budget)
4599 .map_err(WorkspaceError::from)?,
4600 cancellation,
4601 )
4602 .await
4603 .map_err(|failure| WorkspaceError::engine(failure.error))?;
4604 merge_workspace_work(checkout.work, receipt, budget)
4605 }
4606
4607 #[cfg(all(feature = "native-mount", not(target_arch = "wasm32")))]
4609 pub async fn restore_host_paths(
4610 &self,
4611 paths: &[std::path::PathBuf],
4612 replacement: crate::HostPathReplacement,
4613 options: &crate::MaterializeOptions,
4614 cancellation: &crate::CancellationToken,
4615 ) -> Result<Vec<crate::HostPathRestore>, WorkspaceError> {
4616 let mut checkout = self
4617 .workspace
4618 .engine_checkout_measured(
4619 GenerationSelector::Exact(self.id),
4620 CheckoutMode::read_only_pinned(),
4621 crate::WorkBudget::UNBOUNDED,
4622 cancellation,
4623 )
4624 .await?
4625 .value;
4626 let mut restored = Vec::new();
4627 restored
4628 .try_reserve_exact(paths.len())
4629 .map_err(|_| WorkspaceError::engine("restore path result allocation failed"))?;
4630 for path in paths {
4631 restored.push(
4632 crate::restore_checkout_host_path(
4633 &mut checkout,
4634 path,
4635 replacement,
4636 options,
4637 crate::WorkBudget::UNBOUNDED,
4638 cancellation,
4639 )
4640 .await
4641 .map_err(|failure| WorkspaceError::engine(failure.error))?
4642 .value,
4643 );
4644 }
4645 Ok(restored)
4646 }
4647
4648 pub async fn parents(&self) -> Result<Vec<GenerationId>, WorkspaceError> {
4654 self.workspace
4655 .volume
4656 .fs
4657 .generation_parents(&self.workspace.volume, self.id)
4658 .await
4659 }
4660
4661 pub async fn read(&self, path: &str, maximum_bytes: u64) -> Result<Bytes, WorkspaceError> {
4668 read_generation(
4669 &self.workspace,
4670 GenerationSelector::Exact(self.id),
4671 path,
4672 maximum_bytes,
4673 )
4674 .await
4675 }
4676
4677 pub async fn read_range(
4684 &self,
4685 path: &str,
4686 offset: u64,
4687 length: u64,
4688 ) -> Result<Bytes, WorkspaceError> {
4689 read_generation_range(
4690 &self.workspace,
4691 GenerationSelector::Exact(self.id),
4692 path,
4693 offset,
4694 length,
4695 )
4696 .await
4697 }
4698
4699 pub async fn stat(&self, path: &str) -> Result<WorkspaceStat, WorkspaceError> {
4705 stat_generation(&self.workspace, GenerationSelector::Exact(self.id), path).await
4706 }
4707
4708 pub async fn lookup_paths(
4717 &self,
4718 paths: &[NamespacePath],
4719 budget: crate::WorkBudget,
4720 cancellation: &crate::CancellationToken,
4721 ) -> crate::FsResult<Vec<Option<crate::kernel::FileRecord>>> {
4722 let checkout = self
4723 .workspace
4724 .volume
4725 .checkout(
4726 GenerationSelector::Exact(self.id),
4727 CheckoutMode::read_only_pinned(),
4728 budget,
4729 cancellation,
4730 )
4731 .await?;
4732 let checkout_work = checkout.work;
4733 let remaining = checkout_work
4734 .remaining(budget)
4735 .map_err(|error| crate::OperationFailure::new(error.into(), checkout_work))?;
4736 let mut checkout = checkout.value;
4737 let lookup = checkout
4738 .lookup_batch_no_follow(paths, remaining, cancellation)
4739 .await
4740 .map_err(|failure| failure.map_with_prior_work(checkout_work, |error| error))?;
4741 let work = checkout_work
4742 .checked_add(lookup.work)
4743 .map_err(|error| crate::OperationFailure::new(error.into(), checkout_work))?;
4744 Ok(crate::FsReceipt {
4745 value: lookup
4746 .value
4747 .entries
4748 .into_iter()
4749 .map(|entry| entry.record)
4750 .collect(),
4751 work,
4752 })
4753 }
4754
4755 pub async fn list_directory(
4762 &self,
4763 path: &str,
4764 after: Option<&LogicalName>,
4765 maximum_entries: u32,
4766 ) -> Result<WorkspaceDirectoryPage, WorkspaceError> {
4767 list_generation_directory(
4768 &self.workspace,
4769 GenerationSelector::Exact(self.id),
4770 path,
4771 after,
4772 maximum_entries,
4773 )
4774 .await
4775 }
4776
4777 pub async fn paths_for_file_id(
4782 &self,
4783 file_id: FileId,
4784 maximum_entries: u32,
4785 ) -> Result<Vec<String>, WorkspaceError> {
4786 Ok(self
4787 .paths_for_file_ids([file_id], maximum_entries)
4788 .await?
4789 .remove(&file_id)
4790 .unwrap_or_default())
4791 }
4792
4793 pub(crate) async fn paths_for_file_id_measured(
4794 &self,
4795 file_id: FileId,
4796 maximum_entries: u32,
4797 budget: crate::WorkBudget,
4798 cancellation: &crate::CancellationToken,
4799 ) -> Result<crate::OperationReceipt<Vec<String>>, WorkspaceError> {
4800 let receipt = self
4801 .namespace_records_for_file_ids_bounded(
4802 [file_id],
4803 maximum_entries,
4804 budget,
4805 cancellation,
4806 )
4807 .await?;
4808 let paths = receipt
4809 .value
4810 .records
4811 .get(&file_id)
4812 .into_iter()
4813 .flatten()
4814 .map(|(path, _)| namespace_path_text(path))
4815 .collect::<Result<Vec<_>, _>>()?;
4816 Ok(crate::OperationReceipt {
4817 value: paths,
4818 work: receipt.work,
4819 })
4820 }
4821
4822 pub async fn paths_for_file_ids(
4828 &self,
4829 file_ids: impl IntoIterator<Item = FileId>,
4830 maximum_entries: u32,
4831 ) -> Result<BTreeMap<FileId, Vec<String>>, WorkspaceError> {
4832 let paths = self
4833 .namespace_paths_for_file_ids(file_ids, maximum_entries)
4834 .await?;
4835 paths
4836 .into_iter()
4837 .map(|(file_id, paths)| {
4838 paths
4839 .into_iter()
4840 .map(|path| namespace_path_text(&path))
4841 .collect::<Result<Vec<_>, _>>()
4842 .map(|paths| (file_id, paths))
4843 })
4844 .collect()
4845 }
4846
4847 async fn namespace_paths_for_file_ids(
4848 &self,
4849 file_ids: impl IntoIterator<Item = FileId>,
4850 maximum_entries: u32,
4851 ) -> Result<BTreeMap<FileId, Vec<NamespacePath>>, WorkspaceError> {
4852 Ok(self
4853 .namespace_records_for_file_ids(file_ids, maximum_entries)
4854 .await?
4855 .records
4856 .into_iter()
4857 .map(|(file_id, records)| {
4858 (file_id, records.into_iter().map(|(path, _)| path).collect())
4859 })
4860 .collect())
4861 }
4862
4863 async fn namespace_records_for_file_ids(
4864 &self,
4865 file_ids: impl IntoIterator<Item = FileId>,
4866 maximum_entries: u32,
4867 ) -> Result<NamespaceRecordSearch, WorkspaceError> {
4868 self.namespace_records_for_file_ids_bounded(
4869 file_ids,
4870 maximum_entries,
4871 crate::WorkBudget::UNBOUNDED,
4872 &crate::CancellationToken::new(),
4873 )
4874 .await
4875 .map(|receipt| receipt.value)
4876 }
4877
4878 #[allow(clippy::too_many_lines)]
4879 async fn namespace_records_for_file_ids_bounded(
4880 &self,
4881 file_ids: impl IntoIterator<Item = FileId>,
4882 maximum_entries: u32,
4883 budget: crate::WorkBudget,
4884 cancellation: &crate::CancellationToken,
4885 ) -> Result<crate::OperationReceipt<NamespaceRecordSearch>, WorkspaceError> {
4886 let file_ids: BTreeSet<_> = file_ids.into_iter().collect();
4887 if file_ids.is_empty() {
4888 return Ok(crate::OperationReceipt {
4889 value: NamespaceRecordSearch {
4890 records: BTreeMap::new(),
4891 complete: true,
4892 },
4893 work: crate::WorkCounters::default(),
4894 });
4895 }
4896 if maximum_entries == 0 {
4897 return Ok(crate::OperationReceipt {
4898 value: NamespaceRecordSearch {
4899 records: BTreeMap::new(),
4900 complete: false,
4901 },
4902 work: crate::WorkCounters::default(),
4903 });
4904 }
4905 let maximum = usize::try_from(maximum_entries).unwrap_or(usize::MAX);
4906 let limits = self.workspace.volume.config.limits;
4907 let cache_key = crate::path_index::cache_key(self.id, file_ids.iter().copied());
4908 if let Some(bytes) = self.workspace.volume.fs.path_index().load(cache_key)
4909 && let Some(cached) = crate::path_index::GenerationPathQuery::decode(
4910 &bytes,
4911 self.id,
4912 file_ids.iter().copied(),
4913 )
4914 {
4915 let cached_paths = cached
4916 .into_iter()
4917 .flat_map(|(file_id, paths)| paths.into_iter().map(move |path| (file_id, path)))
4918 .map(|(file_id, path)| path.into_namespace(limits).map(|path| (file_id, path)))
4919 .collect::<Option<Vec<_>>>();
4920 if let Some(cached_paths) = cached_paths {
4921 if cached_paths.is_empty() {
4922 return Ok(crate::OperationReceipt {
4923 value: NamespaceRecordSearch {
4924 records: BTreeMap::new(),
4925 complete: true,
4926 },
4927 work: crate::WorkCounters::default(),
4928 });
4929 }
4930 if cached_paths.len() <= maximum {
4935 let paths = cached_paths
4936 .iter()
4937 .map(|(_, path)| path.clone())
4938 .collect::<Vec<_>>();
4939 let lookup = self
4940 .lookup_paths(&paths, budget, cancellation)
4941 .await
4942 .map_err(WorkspaceError::engine)?;
4943 let mut records = BTreeMap::<_, Vec<_>>::new();
4944 let mut valid = true;
4945 for ((expected, path), record) in cached_paths.into_iter().zip(lookup.value) {
4946 let Some(record) = record else {
4947 valid = false;
4948 break;
4949 };
4950 if record.file_id != expected {
4951 valid = false;
4952 break;
4953 }
4954 records.entry(expected).or_default().push((path, record));
4955 }
4956 if valid {
4957 return Ok(crate::OperationReceipt {
4958 value: NamespaceRecordSearch {
4959 records,
4960 complete: true,
4961 },
4962 work: lookup.work,
4963 });
4964 }
4965 }
4966 }
4967 }
4968 let mut matches: BTreeMap<_, Vec<(NamespacePath, crate::kernel::FileRecord)>> =
4969 BTreeMap::new();
4970 let checkout = self
4971 .workspace
4972 .engine_checkout_measured(
4973 GenerationSelector::Exact(self.id),
4974 CheckoutMode::read_only_pinned(),
4975 budget,
4976 cancellation,
4977 )
4978 .await?;
4979 let mut work = checkout.work;
4980 let mut checkout = checkout.value;
4981 let root = NamespacePath::new(Vec::new(), limits).map_err(WorkspaceError::path)?;
4982 let root_lookup = checkout
4983 .lookup_no_follow(
4984 &root,
4985 work.remaining(budget).map_err(WorkspaceError::engine)?,
4986 cancellation,
4987 )
4988 .await
4989 .map_err(WorkspaceError::engine)?;
4990 work = work
4991 .checked_add(root_lookup.work)
4992 .map_err(WorkspaceError::engine)?;
4993 if let Some(record) = root_lookup.value.record
4994 && file_ids.contains(&record.file_id)
4995 {
4996 matches
4997 .entry(record.file_id)
4998 .or_default()
4999 .push((root.clone(), record));
5000 }
5001 let mut pending = vec![root];
5002 let mut examined = 0_usize;
5003 while let Some(directory) = pending.pop() {
5004 let mut after = None;
5005 loop {
5006 let page_receipt = checkout
5007 .list_directory_records(
5008 &directory,
5009 after.as_ref(),
5010 1_024,
5011 work.remaining(budget).map_err(WorkspaceError::engine)?,
5012 cancellation,
5013 )
5014 .await
5015 .map_err(WorkspaceError::engine)?;
5016 work = work
5017 .checked_add(page_receipt.work)
5018 .map_err(WorkspaceError::engine)?;
5019 let page = page_receipt.value;
5020 for entry in &page.entries {
5021 examined = examined.saturating_add(1);
5022 if examined > maximum {
5023 for records in matches.values_mut() {
5024 records.sort_by(|left, right| left.0.cmp(&right.0));
5025 }
5026 return Ok(crate::OperationReceipt {
5027 value: NamespaceRecordSearch {
5028 records: matches,
5029 complete: false,
5030 },
5031 work,
5032 });
5033 }
5034 let mut components = directory.components().to_vec();
5035 components.push(entry.name.clone());
5036 let path =
5037 NamespacePath::new(components, limits).map_err(WorkspaceError::path)?;
5038 if file_ids.contains(&entry.record.file_id) {
5039 matches
5040 .entry(entry.record.file_id)
5041 .or_default()
5042 .push((path.clone(), entry.record));
5043 }
5044 if entry.record.kind == FileKind::Directory {
5045 pending.push(path);
5046 }
5047 }
5048 if !page.has_more {
5049 break;
5050 }
5051 after = page.entries.last().map(|entry| entry.name.clone());
5052 }
5053 }
5054 for records in matches.values_mut() {
5055 records.sort_by(|left, right| left.0.cmp(&right.0));
5056 }
5057 let cached_paths = matches
5058 .iter()
5059 .map(|(&file_id, records)| {
5060 (
5061 file_id,
5062 records
5063 .iter()
5064 .map(|(path, _)| crate::path_index::CachedPath::from_namespace(path))
5065 .collect(),
5066 )
5067 })
5068 .collect();
5069 let cached = crate::path_index::GenerationPathQuery::new(
5070 self.id,
5071 file_ids.iter().copied(),
5072 &cached_paths,
5073 );
5074 if let Some(bytes) = cached.encode() {
5075 self.workspace
5076 .volume
5077 .fs
5078 .path_index()
5079 .store(cache_key, &bytes);
5080 }
5081 Ok(crate::OperationReceipt {
5082 value: NamespaceRecordSearch {
5083 records: matches,
5084 complete: true,
5085 },
5086 work,
5087 })
5088 }
5089
5090 pub async fn read_symbolic_link(&self, path: &str) -> Result<Bytes, WorkspaceError> {
5097 read_generation_symbolic_link(&self.workspace, GenerationSelector::Exact(self.id), path)
5098 .await
5099 }
5100
5101 pub async fn plan_extents(
5108 &self,
5109 path: &str,
5110 offset: u64,
5111 length: u64,
5112 maximum_spans: u32,
5113 ) -> Result<WorkspaceExtentPlan, WorkspaceError> {
5114 plan_generation_extents(
5115 &self.workspace,
5116 GenerationSelector::Exact(self.id),
5117 path,
5118 offset,
5119 length,
5120 maximum_spans,
5121 )
5122 .await
5123 }
5124
5125 pub async fn pin(
5132 &self,
5133 identity: impl AsRef<str>,
5134 ) -> Result<GenerationPin<A, O>, WorkspaceError> {
5135 let identity = WorkspaceName::new(identity)?;
5136 self.workspace
5137 .volume
5138 .fs
5139 .retain_workspace_generation(
5140 &self.workspace.volume,
5141 self.id,
5142 crate::kernel::RetentionKind::Pin,
5143 identity.as_str().to_owned(),
5144 )
5145 .await?;
5146 Ok(GenerationPin {
5147 identity,
5148 generation: self.clone(),
5149 })
5150 }
5151}
5152
5153#[derive(Clone)]
5155pub struct ForkOptions<A, O> {
5156 pub(crate) generation: Generation<A, O>,
5157 pub(crate) idempotency_key: IdempotencyKey,
5158}
5159
5160impl<A, O> ForkOptions<A, O> {
5161 #[must_use]
5163 pub fn from_generation(generation: Generation<A, O>, idempotency_key: IdempotencyKey) -> Self {
5164 Self {
5165 generation,
5166 idempotency_key,
5167 }
5168 }
5169}
5170
5171#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
5173pub enum WorkspaceNameError {
5174 #[error("workspace name is empty")]
5176 Empty,
5177 #[error("workspace name is reserved")]
5179 Reserved,
5180 #[error("workspace name exceeds 255 UTF-8 bytes")]
5182 TooLong,
5183 #[error("workspace name contains a path separator or control character")]
5185 InvalidCharacter,
5186}
5187
5188#[derive(Debug, Error)]
5190pub enum WorkspaceError {
5191 #[error(transparent)]
5193 Name(#[from] WorkspaceNameError),
5194 #[error("workspace engine failure: {0}")]
5196 Engine(String),
5197 #[error(transparent)]
5199 Cancelled(#[from] crate::CancellationError),
5200 #[error(transparent)]
5202 Work(#[from] crate::WorkError),
5203 #[error("fork generation belongs to another filesystem deployment")]
5205 ForeignGeneration,
5206 #[error("workspace generation is stale")]
5208 StaleGeneration,
5209 #[error("workspace file identity is stale")]
5211 StaleIdentity,
5212 #[error("invalid workspace path: {0}")]
5214 Path(String),
5215 #[error("workspace path does not exist")]
5217 NotFound,
5218 #[error("workspace path is not a regular file")]
5220 NotRegularFile,
5221 #[error("workspace path is not a directory")]
5223 NotDirectory,
5224 #[error("workspace file exceeds the caller's read bound")]
5226 ReadLimitExceeded,
5227 #[error("retention label is already bound to another generation")]
5229 RetentionConflict,
5230 #[error("workspaces are incompatible")]
5232 IncompatibleWorkspace,
5233 #[error("workspaces have no common ancestor")]
5235 NoCommonAncestor,
5236 #[error("workspace lineage exceeds the configured bound")]
5238 LineageLimit,
5239 #[error("workspace join exceeds its configured bound")]
5241 JoinLimit,
5242 #[error("merge resolution is incompatible with its conflict")]
5244 InvalidMergeResolution,
5245 #[error("change sets are not contiguous")]
5247 ChangeSetContinuity,
5248 #[error("changed paths exceed the caller's traversal bound")]
5250 ChangedPathLimit,
5251 #[error("staged content set is empty")]
5253 EmptyContentSet,
5254 #[error("workspace is not a fork")]
5256 NotFork,
5257 #[error("staged content length overflow")]
5259 ContentLengthOverflow,
5260}
5261
5262impl WorkspaceError {
5263 pub(crate) fn engine(error: impl fmt::Display) -> Self {
5264 Self::Engine(error.to_string())
5265 }
5266
5267 pub(crate) fn path(error: impl fmt::Display) -> Self {
5268 Self::Path(error.to_string())
5269 }
5270}
5271
5272impl From<FsError> for WorkspaceError {
5273 fn from(error: FsError) -> Self {
5274 match error {
5275 FsError::Cancelled(error) => Self::Cancelled(error),
5276 FsError::Work(error) => Self::Work(error),
5277 FsError::NotFound => Self::NotFound,
5278 FsError::NotDirectory => Self::NotDirectory,
5279 FsError::FileRead(crate::kernel::FileRangeReadError::NotRegular) => {
5280 Self::NotRegularFile
5281 }
5282 FsError::Mutation(crate::kernel::GenerationMutationError::FileIdentityConflict) => {
5283 Self::StaleIdentity
5284 }
5285 error => Self::Engine(error.to_string()),
5286 }
5287 }
5288}
5289
5290pub(crate) fn customer_path(
5291 path: &str,
5292 config: crate::model::VolumeConfig,
5293) -> Result<NamespacePath, WorkspaceError> {
5294 let portable = PortablePath::parse(path, config.limits).map_err(WorkspaceError::path)?;
5295 NamespacePath::from_portable_in_profile(&portable, config.profile, config.limits)
5296 .map_err(WorkspaceError::path)
5297}
5298
5299#[cfg(all(feature = "native-mount", not(target_arch = "wasm32")))]
5300fn merge_workspace_work<T>(
5301 prior: crate::WorkCounters,
5302 receipt: crate::OperationReceipt<T>,
5303 budget: crate::WorkBudget,
5304) -> Result<crate::OperationReceipt<T>, WorkspaceError> {
5305 let work = prior
5306 .checked_add(receipt.work)
5307 .map_err(WorkspaceError::from)?;
5308 work.verify(budget).map_err(WorkspaceError::from)?;
5309 Ok(crate::OperationReceipt {
5310 value: receipt.value,
5311 work,
5312 })
5313}
5314
5315fn path_conflict_kind(
5316 base: Option<crate::kernel::FileRecord>,
5317 current: Option<crate::kernel::FileRecord>,
5318 source: Option<crate::kernel::FileRecord>,
5319) -> ConflictKind {
5320 let (Some(base), Some(current), Some(source)) = (base, current, source) else {
5321 return ConflictKind::Binding;
5322 };
5323 if base.kind != current.kind
5324 || base.kind != source.kind
5325 || base.file_id != current.file_id
5326 || base.file_id != source.file_id
5327 {
5328 return ConflictKind::Binding;
5329 }
5330 if base.link_count != current.link_count || base.link_count != source.link_count {
5331 return ConflictKind::HardLink;
5332 }
5333 if base.metadata != current.metadata || base.metadata != source.metadata {
5334 return ConflictKind::Metadata;
5335 }
5336 match base.kind {
5337 FileKind::Regular => ConflictKind::Binary,
5338 FileKind::Directory => ConflictKind::Directory,
5339 FileKind::SymbolicLink => ConflictKind::SymbolicLink,
5340 FileKind::Fifo
5341 | FileKind::Socket
5342 | FileKind::CharacterDevice
5343 | FileKind::BlockDevice
5344 | FileKind::ReparsePoint
5345 | FileKind::MountBoundary => ConflictKind::Special,
5346 }
5347}
5348
5349fn regular_file_bytes(record: crate::kernel::FileRecord) -> Result<u64, WorkspaceError> {
5350 match record.payload {
5351 FilePayload::InlineRegular(bytes) => {
5352 Ok(u64::try_from(bytes.as_bytes().len()).unwrap_or(u64::MAX))
5353 }
5354 FilePayload::Regular { logical_bytes, .. } => Ok(logical_bytes),
5355 _ => Err(WorkspaceError::NotRegularFile),
5356 }
5357}
5358
5359#[cfg(test)]
5360#[path = "tests/workspace.rs"]
5361mod tests;