1#![expect(missing_docs)]
16
17use std::borrow::Cow;
18use std::cmp::Ordering;
19use std::collections::HashMap;
20use std::collections::HashSet;
21use std::error::Error;
22use std::fs;
23use std::fs::DirEntry;
24use std::fs::File;
25use std::fs::Metadata;
26use std::fs::OpenOptions;
27use std::io;
28use std::io::Read as _;
29use std::io::Write as _;
30use std::iter;
31use std::mem;
32use std::ops::Range;
33#[cfg(unix)]
34use std::os::unix::fs::PermissionsExt as _;
35use std::path::Path;
36use std::path::PathBuf;
37use std::slice;
38use std::sync::Arc;
39use std::sync::OnceLock;
40use std::sync::mpsc::Sender;
41use std::sync::mpsc::channel;
42use std::time::SystemTime;
43
44use async_trait::async_trait;
45use either::Either;
46use futures::AsyncRead;
47use futures::AsyncReadExt as _;
48use futures::StreamExt as _;
49use futures::io::AllowStdIo;
50use itertools::EitherOrBoth;
51use itertools::Itertools as _;
52use once_cell::unsync::OnceCell;
53use pollster::FutureExt as _;
54use prost::Message as _;
55use rayon::iter::IntoParallelIterator as _;
56use rayon::prelude::IndexedParallelIterator as _;
57use rayon::prelude::ParallelIterator as _;
58use tempfile::NamedTempFile;
59use thiserror::Error;
60use tracing::instrument;
61use tracing::trace_span;
62
63use crate::backend::BackendError;
64use crate::backend::CopyId;
65use crate::backend::FileId;
66use crate::backend::MillisSinceEpoch;
67use crate::backend::SymlinkId;
68use crate::backend::TreeId;
69use crate::backend::TreeValue;
70use crate::commit::Commit;
71use crate::config::ConfigGetError;
72use crate::conflict_labels::ConflictLabels;
73use crate::conflicts;
74use crate::conflicts::ConflictMarkerStyle;
75use crate::conflicts::ConflictMaterializeOptions;
76use crate::conflicts::MIN_CONFLICT_MARKER_LEN;
77use crate::conflicts::MaterializedTreeValue;
78use crate::conflicts::choose_materialized_conflict_marker_len;
79use crate::conflicts::materialize_merge_result_to_bytes;
80use crate::conflicts::materialize_tree_value;
81pub use crate::eol::EolConversionMode;
82use crate::eol::TargetEolStrategy;
83use crate::file_util::FileIdentity;
84use crate::file_util::check_symlink_support;
85use crate::file_util::copy_async_to_sync;
86use crate::file_util::persist_temp_file;
87use crate::file_util::symlink_file;
88use crate::fsmonitor::FsmonitorSettings;
89#[cfg(feature = "watchman")]
90use crate::fsmonitor::WatchmanConfig;
91#[cfg(feature = "watchman")]
92use crate::fsmonitor::watchman;
93use crate::gitignore::GitIgnoreFile;
94use crate::lock::FileLock;
95use crate::matchers::DifferenceMatcher;
96use crate::matchers::EverythingMatcher;
97use crate::matchers::FilesMatcher;
98use crate::matchers::IntersectionMatcher;
99use crate::matchers::Matcher;
100use crate::matchers::PrefixMatcher;
101use crate::matchers::UnionMatcher;
102use crate::merge::Merge;
103use crate::merge::MergeBuilder;
104use crate::merge::MergedTreeValue;
105use crate::merge::SameChange;
106use crate::merged_tree::MergedTree;
107use crate::merged_tree::TreeDiffEntry;
108use crate::merged_tree_builder::MergedTreeBuilder;
109use crate::object_id::ObjectId as _;
110use crate::op_store::OperationId;
111use crate::ref_name::WorkspaceName;
112use crate::ref_name::WorkspaceNameBuf;
113use crate::repo_path::RepoPath;
114use crate::repo_path::RepoPathBuf;
115use crate::repo_path::RepoPathComponent;
116use crate::settings::UserSettings;
117use crate::store::Store;
118use crate::working_copy::CheckoutError;
119use crate::working_copy::CheckoutStats;
120use crate::working_copy::LockedWorkingCopy;
121use crate::working_copy::ResetError;
122use crate::working_copy::SnapshotError;
123use crate::working_copy::SnapshotOptions;
124use crate::working_copy::SnapshotProgress;
125use crate::working_copy::SnapshotStats;
126use crate::working_copy::UntrackedReason;
127use crate::working_copy::WorkingCopy;
128use crate::working_copy::WorkingCopyFactory;
129use crate::working_copy::WorkingCopyStateError;
130
131fn symlink_target_convert_to_store(path: &Path) -> Option<Cow<'_, str>> {
132 let path = path.to_str()?;
133 if std::path::MAIN_SEPARATOR == '/' {
134 Some(Cow::Borrowed(path))
135 } else {
136 Some(Cow::Owned(path.replace(std::path::MAIN_SEPARATOR_STR, "/")))
142 }
143}
144
145fn symlink_target_convert_to_disk(path: &str) -> PathBuf {
146 let path = if std::path::MAIN_SEPARATOR == '/' {
147 Cow::Borrowed(path)
148 } else {
149 Cow::Owned(path.replace('/', std::path::MAIN_SEPARATOR_STR))
154 };
155 PathBuf::from(path.as_ref())
156}
157
158#[derive(Clone, Copy, Debug)]
164enum ExecChangePolicy {
165 Ignore,
166 #[cfg_attr(windows, expect(dead_code))]
167 Respect,
168}
169
170#[derive(Clone, Copy, Debug, Default, serde::Deserialize)]
172#[serde(rename_all = "kebab-case")]
173pub enum ExecChangeSetting {
174 Ignore,
175 Respect,
176 #[default]
177 Auto,
178}
179
180impl ExecChangePolicy {
181 #[cfg_attr(windows, expect(unused_variables))]
187 fn new(exec_change_setting: ExecChangeSetting, state_path: &Path) -> Self {
188 #[cfg(windows)]
189 return Self::Ignore;
190 #[cfg(unix)]
191 return match exec_change_setting {
192 ExecChangeSetting::Ignore => Self::Ignore,
193 ExecChangeSetting::Respect => Self::Respect,
194 ExecChangeSetting::Auto => {
195 match crate::file_util::check_executable_bit_support(state_path) {
196 Ok(false) => Self::Ignore,
197 Ok(true) => Self::Respect,
198 Err(err) => {
199 tracing::warn!(?err, "Error when checking for executable bit support");
200 Self::Respect
201 }
202 }
203 }
204 };
205 }
206}
207
208#[derive(Clone, Copy, Debug, Eq, PartialEq)]
216pub struct ExecBit(bool);
217
218impl ExecBit {
219 fn for_tree_value(
224 self,
225 exec_policy: ExecChangePolicy,
226 prev_in_repo: impl FnOnce() -> Option<bool>,
227 ) -> bool {
228 match exec_policy {
229 ExecChangePolicy::Ignore => prev_in_repo().unwrap_or(false),
230 ExecChangePolicy::Respect => self.0,
231 }
232 }
233
234 fn new_from_repo(
244 in_repo: bool,
245 exec_policy: ExecChangePolicy,
246 prev_on_disk: impl FnOnce() -> Option<Self>,
247 ) -> Self {
248 match exec_policy {
249 _ if cfg!(windows) => Self(false),
250 ExecChangePolicy::Ignore => prev_on_disk().unwrap_or(Self(false)),
251 ExecChangePolicy::Respect => Self(in_repo),
252 }
253 }
254
255 #[cfg_attr(windows, expect(unused_variables))]
257 fn new_from_disk(metadata: &Metadata) -> Self {
258 #[cfg(unix)]
259 return Self(metadata.permissions().mode() & 0o111 != 0);
260 #[cfg(windows)]
261 return Self(false);
262 }
263}
264
265#[cfg_attr(windows, expect(unused_variables))]
271fn set_executable(exec_bit: ExecBit, disk_path: &Path) -> Result<(), io::Error> {
272 #[cfg(unix)]
273 {
274 let mode = if exec_bit.0 { 0o755 } else { 0o644 };
275 fs::set_permissions(disk_path, fs::Permissions::from_mode(mode))?;
276 }
277 Ok(())
278}
279
280#[derive(Debug, PartialEq, Eq, Clone)]
281pub enum FileType {
282 Normal { exec_bit: ExecBit },
283 Symlink,
284 GitSubmodule,
285}
286
287#[derive(Debug, PartialEq, Eq, Clone, Copy)]
288pub struct MaterializedConflictData {
289 pub conflict_marker_len: u32,
290}
291
292#[derive(Debug, PartialEq, Eq, Clone)]
293pub struct FileState {
294 pub file_type: FileType,
295 pub mtime: MillisSinceEpoch,
296 pub size: u64,
297 pub materialized_conflict_data: Option<MaterializedConflictData>,
298 }
302
303impl FileState {
304 pub fn is_clean(&self, old_file_state: &Self) -> bool {
307 self.file_type == old_file_state.file_type
308 && self.mtime == old_file_state.mtime
309 && self.size == old_file_state.size
310 }
311
312 fn placeholder() -> Self {
315 Self {
316 file_type: FileType::Normal {
317 exec_bit: ExecBit(false),
318 },
319 mtime: MillisSinceEpoch(0),
320 size: 0,
321 materialized_conflict_data: None,
322 }
323 }
324
325 fn for_file(
326 exec_bit: ExecBit,
327 size: u64,
328 metadata: &Metadata,
329 ) -> Result<Self, MtimeOutOfRange> {
330 Ok(Self {
331 file_type: FileType::Normal { exec_bit },
332 mtime: mtime_from_metadata(metadata)?,
333 size,
334 materialized_conflict_data: None,
335 })
336 }
337
338 fn for_symlink(metadata: &Metadata) -> Result<Self, MtimeOutOfRange> {
339 Ok(Self {
343 file_type: FileType::Symlink,
344 mtime: mtime_from_metadata(metadata)?,
345 size: metadata.len(),
346 materialized_conflict_data: None,
347 })
348 }
349
350 fn for_gitsubmodule() -> Self {
351 Self {
352 file_type: FileType::GitSubmodule,
353 mtime: MillisSinceEpoch(0),
354 size: 0,
355 materialized_conflict_data: None,
356 }
357 }
358}
359
360#[derive(Clone, Debug)]
362struct FileStatesMap {
363 data: Vec<crate::protos::local_working_copy::FileStateEntry>,
364}
365
366impl FileStatesMap {
367 fn new() -> Self {
368 Self { data: Vec::new() }
369 }
370
371 fn from_proto(
372 mut data: Vec<crate::protos::local_working_copy::FileStateEntry>,
373 is_sorted: bool,
374 ) -> Self {
375 if !is_sorted {
376 data.sort_unstable_by(|entry1, entry2| {
377 let path1 = RepoPath::from_internal_string(&entry1.path).unwrap();
378 let path2 = RepoPath::from_internal_string(&entry2.path).unwrap();
379 path1.cmp(path2)
380 });
381 }
382 debug_assert!(is_file_state_entries_proto_unique_and_sorted(&data));
383 Self { data }
384 }
385
386 fn merge_in(
389 &mut self,
390 changed_file_states: Vec<(RepoPathBuf, FileState)>,
391 deleted_files: &HashSet<RepoPathBuf>,
392 ) {
393 if changed_file_states.is_empty() && deleted_files.is_empty() {
394 return;
395 }
396 debug_assert!(
397 changed_file_states.is_sorted_by(|(path1, _), (path2, _)| path1 < path2),
398 "changed_file_states must be sorted and have no duplicates"
399 );
400 self.data = itertools::merge_join_by(
401 mem::take(&mut self.data),
402 changed_file_states,
403 |old_entry, (changed_path, _)| {
404 RepoPath::from_internal_string(&old_entry.path)
405 .unwrap()
406 .cmp(changed_path)
407 },
408 )
409 .filter_map(|diff| match diff {
410 EitherOrBoth::Both(_, (path, state)) | EitherOrBoth::Right((path, state)) => {
411 debug_assert!(!deleted_files.contains(&path));
412 Some(file_state_entry_to_proto(path, &state))
413 }
414 EitherOrBoth::Left(entry) => {
415 let present =
416 !deleted_files.contains(RepoPath::from_internal_string(&entry.path).unwrap());
417 present.then_some(entry)
418 }
419 })
420 .collect();
421 }
422
423 fn clear(&mut self) {
424 self.data.clear();
425 }
426
427 fn all(&self) -> FileStates<'_> {
429 FileStates::from_sorted(&self.data)
430 }
431}
432
433#[derive(Clone, Copy, Debug)]
435pub struct FileStates<'a> {
436 data: &'a [crate::protos::local_working_copy::FileStateEntry],
437}
438
439impl<'a> FileStates<'a> {
440 fn from_sorted(data: &'a [crate::protos::local_working_copy::FileStateEntry]) -> Self {
441 debug_assert!(is_file_state_entries_proto_unique_and_sorted(data));
442 Self { data }
443 }
444
445 pub fn prefixed(&self, base: &RepoPath) -> Self {
447 let range = self.prefixed_range(base);
448 Self::from_sorted(&self.data[range])
449 }
450
451 fn prefixed_at(&self, dir: &RepoPath, base: &RepoPathComponent) -> Self {
454 let range = self.prefixed_range_at(dir, base);
455 Self::from_sorted(&self.data[range])
456 }
457
458 pub fn is_empty(&self) -> bool {
460 self.data.is_empty()
461 }
462
463 pub fn contains_path(&self, path: &RepoPath) -> bool {
465 self.exact_position(path).is_some()
466 }
467
468 pub fn get(&self, path: &RepoPath) -> Option<FileState> {
470 let pos = self.exact_position(path)?;
471 let (_, state) = file_state_entry_from_proto(&self.data[pos]);
472 Some(state)
473 }
474
475 pub fn get_exec_bit(&self, path: &RepoPath) -> Option<ExecBit> {
477 match self.get(path)?.file_type {
478 FileType::Normal { exec_bit } => Some(exec_bit),
479 FileType::Symlink | FileType::GitSubmodule => None,
480 }
481 }
482
483 fn get_at(&self, dir: &RepoPath, name: &RepoPathComponent) -> Option<FileState> {
486 let pos = self.exact_position_at(dir, name)?;
487 let (_, state) = file_state_entry_from_proto(&self.data[pos]);
488 Some(state)
489 }
490
491 fn exact_position(&self, path: &RepoPath) -> Option<usize> {
492 self.data
493 .binary_search_by(|entry| {
494 RepoPath::from_internal_string(&entry.path)
495 .unwrap()
496 .cmp(path)
497 })
498 .ok()
499 }
500
501 fn exact_position_at(&self, dir: &RepoPath, name: &RepoPathComponent) -> Option<usize> {
502 debug_assert!(self.paths().all(|path| path.starts_with(dir)));
503 let slash_len = usize::from(!dir.is_root());
504 let prefix_len = dir.as_internal_file_string().len() + slash_len;
505 self.data
506 .binary_search_by(|entry| {
507 let tail = entry.path.get(prefix_len..).unwrap_or("");
508 match tail.split_once('/') {
509 Some((pre, _)) => pre.cmp(name.as_internal_str()).then(Ordering::Greater),
511 None => tail.cmp(name.as_internal_str()),
512 }
513 })
514 .ok()
515 }
516
517 fn prefixed_range(&self, base: &RepoPath) -> Range<usize> {
518 let start = self
519 .data
520 .partition_point(|entry| RepoPath::from_internal_string(&entry.path).unwrap() < base);
521 let len = self.data[start..].partition_point(|entry| {
522 RepoPath::from_internal_string(&entry.path)
523 .unwrap()
524 .starts_with(base)
525 });
526 start..(start + len)
527 }
528
529 fn prefixed_range_at(&self, dir: &RepoPath, base: &RepoPathComponent) -> Range<usize> {
530 debug_assert!(self.paths().all(|path| path.starts_with(dir)));
531 let slash_len = usize::from(!dir.is_root());
532 let prefix_len = dir.as_internal_file_string().len() + slash_len;
533 let start = self.data.partition_point(|entry| {
534 let tail = entry.path.get(prefix_len..).unwrap_or("");
535 let entry_name = tail.split_once('/').map_or(tail, |(name, _)| name);
536 entry_name < base.as_internal_str()
537 });
538 let len = self.data[start..].partition_point(|entry| {
539 let tail = entry.path.get(prefix_len..).unwrap_or("");
540 let entry_name = tail.split_once('/').map_or(tail, |(name, _)| name);
541 entry_name == base.as_internal_str()
542 });
543 start..(start + len)
544 }
545
546 pub fn iter(&self) -> FileStatesIter<'a> {
548 self.data.iter().map(file_state_entry_from_proto)
549 }
550
551 pub fn paths(&self) -> impl ExactSizeIterator<Item = &'a RepoPath> + use<'a> {
553 self.data
554 .iter()
555 .map(|entry| RepoPath::from_internal_string(&entry.path).unwrap())
556 }
557}
558
559type FileStatesIter<'a> = iter::Map<
560 slice::Iter<'a, crate::protos::local_working_copy::FileStateEntry>,
561 fn(&crate::protos::local_working_copy::FileStateEntry) -> (&RepoPath, FileState),
562>;
563
564impl<'a> IntoIterator for FileStates<'a> {
565 type Item = (&'a RepoPath, FileState);
566 type IntoIter = FileStatesIter<'a>;
567
568 fn into_iter(self) -> Self::IntoIter {
569 self.iter()
570 }
571}
572
573fn file_state_from_proto(proto: &crate::protos::local_working_copy::FileState) -> FileState {
574 let file_type = match proto.file_type() {
575 crate::protos::local_working_copy::FileType::Normal => FileType::Normal {
576 exec_bit: ExecBit(false),
577 },
578 crate::protos::local_working_copy::FileType::Executable => FileType::Normal {
581 exec_bit: ExecBit(true),
582 },
583 crate::protos::local_working_copy::FileType::Symlink => FileType::Symlink,
584 #[expect(deprecated)]
585 crate::protos::local_working_copy::FileType::Conflict => FileType::Normal {
586 exec_bit: ExecBit(false),
587 },
588 crate::protos::local_working_copy::FileType::GitSubmodule => FileType::GitSubmodule,
589 };
590 FileState {
591 file_type,
592 mtime: MillisSinceEpoch(proto.mtime_millis_since_epoch),
593 size: proto.size,
594 materialized_conflict_data: proto.materialized_conflict_data.as_ref().map(|data| {
595 MaterializedConflictData {
596 conflict_marker_len: data.conflict_marker_len,
597 }
598 }),
599 }
600}
601
602fn file_state_to_proto(file_state: &FileState) -> crate::protos::local_working_copy::FileState {
603 let mut proto = crate::protos::local_working_copy::FileState::default();
604 let file_type = match &file_state.file_type {
605 FileType::Normal { exec_bit } => {
606 if exec_bit.0 {
607 crate::protos::local_working_copy::FileType::Executable
608 } else {
609 crate::protos::local_working_copy::FileType::Normal
610 }
611 }
612 FileType::Symlink => crate::protos::local_working_copy::FileType::Symlink,
613 FileType::GitSubmodule => crate::protos::local_working_copy::FileType::GitSubmodule,
614 };
615 proto.file_type = file_type as i32;
616 proto.mtime_millis_since_epoch = file_state.mtime.0;
617 proto.size = file_state.size;
618 proto.materialized_conflict_data = file_state.materialized_conflict_data.map(|data| {
619 crate::protos::local_working_copy::MaterializedConflictData {
620 conflict_marker_len: data.conflict_marker_len,
621 }
622 });
623 proto
624}
625
626fn file_state_entry_from_proto(
627 proto: &crate::protos::local_working_copy::FileStateEntry,
628) -> (&RepoPath, FileState) {
629 let path = RepoPath::from_internal_string(&proto.path).unwrap();
630 (path, file_state_from_proto(proto.state.as_ref().unwrap()))
631}
632
633fn file_state_entry_to_proto(
634 path: RepoPathBuf,
635 state: &FileState,
636) -> crate::protos::local_working_copy::FileStateEntry {
637 crate::protos::local_working_copy::FileStateEntry {
638 path: path.into_internal_string(),
639 state: Some(file_state_to_proto(state)),
640 }
641}
642
643fn is_file_state_entries_proto_unique_and_sorted(
644 data: &[crate::protos::local_working_copy::FileStateEntry],
645) -> bool {
646 data.iter()
647 .map(|entry| RepoPath::from_internal_string(&entry.path).unwrap())
648 .is_sorted_by(|path1, path2| path1 < path2)
649}
650
651fn sparse_patterns_from_proto(
652 proto: Option<&crate::protos::local_working_copy::SparsePatterns>,
653) -> Vec<RepoPathBuf> {
654 let mut sparse_patterns = vec![];
655 if let Some(proto_sparse_patterns) = proto {
656 for prefix in &proto_sparse_patterns.prefixes {
657 sparse_patterns.push(RepoPathBuf::from_internal_string(prefix).unwrap());
658 }
659 } else {
660 sparse_patterns.push(RepoPathBuf::root());
663 }
664 sparse_patterns
665}
666
667fn create_parent_dirs(
681 working_copy_path: &Path,
682 repo_path: &RepoPath,
683) -> Result<Option<PathBuf>, CheckoutError> {
684 let (parent_path, basename) = repo_path.split().expect("repo path shouldn't be root");
685 let mut dir_path = working_copy_path.to_owned();
686 for c in parent_path.components() {
687 dir_path.push(c.to_fs_name().map_err(|err| err.with_path(repo_path))?);
689 let (new_dir_created, is_dir) = match fs::create_dir(&dir_path) {
693 Ok(()) => (true, true), Err(err) => match dir_path.symlink_metadata() {
695 Ok(m) => (false, m.is_dir()), Err(_) => {
697 return Err(CheckoutError::Other {
698 message: format!(
699 "Failed to create parent directories for {}",
700 repo_path.to_fs_path_unchecked(working_copy_path).display(),
701 ),
702 err: err.into(),
703 });
704 }
705 },
706 };
707 reject_reserved_existing_path(&dir_path).inspect_err(|_| {
710 if new_dir_created {
711 fs::remove_dir(&dir_path).ok();
712 }
713 })?;
714 if !is_dir {
715 return Ok(None); }
717 }
718
719 let mut file_path = dir_path;
720 file_path.push(
721 basename
722 .to_fs_name()
723 .map_err(|err| err.with_path(repo_path))?,
724 );
725 Ok(Some(file_path))
726}
727
728fn remove_old_file(disk_path: &Path) -> Result<bool, CheckoutError> {
734 reject_reserved_existing_path(disk_path)?;
735 match fs::remove_file(disk_path) {
736 Ok(()) => Ok(true),
737 Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(false),
738 Err(_) if disk_path.symlink_metadata().is_ok_and(|m| m.is_dir()) => Ok(false),
740 Err(err) => Err(CheckoutError::Other {
741 message: format!("Failed to remove file {}", disk_path.display()),
742 err: err.into(),
743 }),
744 }
745}
746
747fn remove_old_submodule_dir(disk_path: &Path) -> Result<bool, CheckoutError> {
754 match fs::remove_dir(disk_path) {
755 Ok(()) => Ok(true),
756 Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(false),
757 Err(err) if err.kind() == io::ErrorKind::DirectoryNotEmpty => Ok(false),
758 Err(err) => Err(CheckoutError::Other {
759 message: format!(
760 "Failed to remove submodule directory {}",
761 disk_path.display()
762 ),
763 err: err.into(),
764 }),
765 }
766}
767
768fn can_create_new_file(disk_path: &Path) -> Result<bool, CheckoutError> {
778 let new_file = match OpenOptions::new()
783 .write(true)
784 .create_new(true) .open(disk_path)
786 {
787 Ok(file) => Some(file),
788 Err(err) if err.kind() == io::ErrorKind::AlreadyExists => None,
789 Err(_) => match disk_path.symlink_metadata() {
791 Ok(_) => None,
792 Err(err) => {
793 return Err(CheckoutError::Other {
794 message: format!("Failed to stat {}", disk_path.display()),
795 err: err.into(),
796 });
797 }
798 },
799 };
800
801 let new_file_created = new_file.is_some();
802
803 if let Some(new_file) = new_file {
804 reject_reserved_existing_file(new_file, disk_path).inspect_err(|_| {
805 fs::remove_file(disk_path).ok();
807 })?;
808
809 fs::remove_file(disk_path).map_err(|err| CheckoutError::Other {
810 message: format!("Failed to remove temporary file {}", disk_path.display()),
811 err: err.into(),
812 })?;
813 } else {
814 reject_reserved_existing_path(disk_path)?;
815 }
816 Ok(new_file_created)
817}
818
819const RESERVED_DIR_NAMES: &[&str] = &[".git", ".jj"];
820
821fn file_identity_from_symlink_path(disk_path: &Path) -> io::Result<Option<FileIdentity>> {
822 match FileIdentity::from_symlink_path(disk_path) {
823 Ok(identity) => Ok(Some(identity)),
824 Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(None),
825 Err(err) => Err(err),
826 }
827}
828
829fn reject_reserved_existing_file(file: File, disk_path: &Path) -> Result<(), CheckoutError> {
835 let file_identity = FileIdentity::from_file(file).map_err(|err| CheckoutError::Other {
838 message: format!("Failed to validate path {}", disk_path.display()),
839 err: err.into(),
840 })?;
841
842 reject_reserved_existing_file_identity(file_identity, disk_path)
843}
844
845fn reject_reserved_existing_path(disk_path: &Path) -> Result<(), CheckoutError> {
855 let Some(disk_identity) =
856 file_identity_from_symlink_path(disk_path).map_err(|err| CheckoutError::Other {
857 message: format!("Failed to validate path {}", disk_path.display()),
858 err: err.into(),
859 })?
860 else {
861 return Ok(());
865 };
866
867 reject_reserved_existing_file_identity(disk_identity, disk_path)
868}
869
870fn reject_reserved_existing_file_identity(
881 disk_identity: FileIdentity,
882 disk_path: &Path,
883) -> Result<(), CheckoutError> {
884 let parent_dir_path = disk_path.parent().expect("content path shouldn't be root");
885 for name in RESERVED_DIR_NAMES {
886 let reserved_path = parent_dir_path.join(name);
887
888 let Some(reserved_identity) =
889 file_identity_from_symlink_path(&reserved_path).map_err(|err| {
890 CheckoutError::Other {
891 message: format!("Failed to validate path {}", disk_path.display()),
892 err: err.into(),
893 }
894 })?
895 else {
896 continue;
900 };
901
902 if disk_identity == reserved_identity {
903 return Err(CheckoutError::ReservedPathComponent {
904 path: disk_path.to_owned(),
905 name,
906 });
907 }
908 }
909
910 Ok(())
911}
912
913#[derive(Debug, Error)]
914#[error("Out-of-range file modification time")]
915struct MtimeOutOfRange;
916
917fn mtime_from_metadata(metadata: &Metadata) -> Result<MillisSinceEpoch, MtimeOutOfRange> {
918 let time = metadata
919 .modified()
920 .expect("File mtime not supported on this platform?");
921 system_time_to_millis(time).ok_or(MtimeOutOfRange)
922}
923
924fn system_time_to_millis(time: SystemTime) -> Option<MillisSinceEpoch> {
925 let millis = match time.duration_since(SystemTime::UNIX_EPOCH) {
926 Ok(duration) => i64::try_from(duration.as_millis()).ok()?,
927 Err(err) => -i64::try_from(err.duration().as_millis()).ok()?,
928 };
929 Some(MillisSinceEpoch(millis))
930}
931
932fn file_state(metadata: &Metadata) -> Result<Option<FileState>, MtimeOutOfRange> {
934 let metadata_file_type = metadata.file_type();
935 let file_type = if metadata_file_type.is_dir() {
936 None
937 } else if metadata_file_type.is_symlink() {
938 Some(FileType::Symlink)
939 } else if metadata_file_type.is_file() {
940 let exec_bit = ExecBit::new_from_disk(metadata);
941 Some(FileType::Normal { exec_bit })
942 } else {
943 None
944 };
945 if let Some(file_type) = file_type {
946 Ok(Some(FileState {
947 file_type,
948 mtime: mtime_from_metadata(metadata)?,
949 size: metadata.len(),
950 materialized_conflict_data: None,
951 }))
952 } else {
953 Ok(None)
954 }
955}
956
957struct FsmonitorMatcher {
958 matcher: Option<Box<dyn Matcher>>,
959 watchman_clock: Option<crate::protos::local_working_copy::WatchmanClock>,
960}
961
962#[derive(Clone, Debug)]
964pub struct TreeStateSettings {
965 pub conflict_marker_style: ConflictMarkerStyle,
968 pub eol_conversion_mode: EolConversionMode,
972 pub exec_change_setting: ExecChangeSetting,
974 pub fsmonitor_settings: FsmonitorSettings,
976}
977
978impl TreeStateSettings {
979 pub fn try_from_user_settings(user_settings: &UserSettings) -> Result<Self, ConfigGetError> {
981 Ok(Self {
982 conflict_marker_style: user_settings.get("ui.conflict-marker-style")?,
983 eol_conversion_mode: EolConversionMode::try_from_settings(user_settings)?,
984 exec_change_setting: user_settings.get("working-copy.exec-bit-change")?,
985 fsmonitor_settings: FsmonitorSettings::from_settings(user_settings)?,
986 })
987 }
988}
989
990pub struct TreeState {
991 store: Arc<Store>,
992 working_copy_path: PathBuf,
993 state_path: PathBuf,
994 tree: MergedTree,
995 file_states: FileStatesMap,
996 sparse_patterns: Vec<RepoPathBuf>,
998 own_mtime: MillisSinceEpoch,
999 symlink_support: bool,
1000
1001 watchman_clock: Option<crate::protos::local_working_copy::WatchmanClock>,
1005
1006 conflict_marker_style: ConflictMarkerStyle,
1007 exec_policy: ExecChangePolicy,
1008 fsmonitor_settings: FsmonitorSettings,
1009 target_eol_strategy: TargetEolStrategy,
1010}
1011
1012#[derive(Debug, Error)]
1013pub enum TreeStateError {
1014 #[error("Reading tree state from {path}")]
1015 ReadTreeState { path: PathBuf, source: io::Error },
1016 #[error("Decoding tree state from {path}")]
1017 DecodeTreeState {
1018 path: PathBuf,
1019 source: prost::DecodeError,
1020 },
1021 #[error("Writing tree state to temporary file {path}")]
1022 WriteTreeState { path: PathBuf, source: io::Error },
1023 #[error("Persisting tree state to file {path}")]
1024 PersistTreeState { path: PathBuf, source: io::Error },
1025 #[error("Filesystem monitor error")]
1026 Fsmonitor(#[source] Box<dyn Error + Send + Sync>),
1027}
1028
1029impl TreeState {
1030 pub fn working_copy_path(&self) -> &Path {
1031 &self.working_copy_path
1032 }
1033
1034 pub fn current_tree(&self) -> &MergedTree {
1035 &self.tree
1036 }
1037
1038 pub fn file_states(&self) -> FileStates<'_> {
1039 self.file_states.all()
1040 }
1041
1042 pub fn sparse_patterns(&self) -> &Vec<RepoPathBuf> {
1043 &self.sparse_patterns
1044 }
1045
1046 fn sparse_matcher(&self) -> Box<dyn Matcher> {
1047 Box::new(PrefixMatcher::new(&self.sparse_patterns))
1048 }
1049
1050 pub fn init(
1051 store: Arc<Store>,
1052 working_copy_path: PathBuf,
1053 state_path: PathBuf,
1054 tree_state_settings: &TreeStateSettings,
1055 ) -> Result<Self, TreeStateError> {
1056 let mut wc = Self::empty(store, working_copy_path, state_path, tree_state_settings);
1057 wc.save()?;
1058 Ok(wc)
1059 }
1060
1061 pub fn init_without_saving(
1066 store: Arc<Store>,
1067 working_copy_path: PathBuf,
1068 state_path: PathBuf,
1069 tree_state_settings: &TreeStateSettings,
1070 ) -> Self {
1071 Self::empty(store, working_copy_path, state_path, tree_state_settings)
1072 }
1073
1074 fn empty(
1075 store: Arc<Store>,
1076 working_copy_path: PathBuf,
1077 state_path: PathBuf,
1078 TreeStateSettings {
1079 conflict_marker_style,
1080 eol_conversion_mode,
1081 exec_change_setting,
1082 fsmonitor_settings,
1083 }: &TreeStateSettings,
1084 ) -> Self {
1085 let exec_policy = ExecChangePolicy::new(*exec_change_setting, &state_path);
1086 Self {
1087 store: store.clone(),
1088 working_copy_path,
1089 state_path,
1090 tree: store.empty_merged_tree(),
1091 file_states: FileStatesMap::new(),
1092 sparse_patterns: vec![RepoPathBuf::root()],
1093 own_mtime: MillisSinceEpoch(0),
1094 symlink_support: check_symlink_support().unwrap_or(false),
1095 watchman_clock: None,
1096 conflict_marker_style: *conflict_marker_style,
1097 exec_policy,
1098 fsmonitor_settings: fsmonitor_settings.clone(),
1099 target_eol_strategy: TargetEolStrategy::new(*eol_conversion_mode),
1100 }
1101 }
1102
1103 pub fn load(
1104 store: Arc<Store>,
1105 working_copy_path: PathBuf,
1106 state_path: PathBuf,
1107 tree_state_settings: &TreeStateSettings,
1108 ) -> Result<Self, TreeStateError> {
1109 let tree_state_path = state_path.join("tree_state");
1110 let file = match File::open(&tree_state_path) {
1111 Err(err) if err.kind() == io::ErrorKind::NotFound => {
1112 return Self::init(store, working_copy_path, state_path, tree_state_settings);
1113 }
1114 Err(err) => {
1115 return Err(TreeStateError::ReadTreeState {
1116 path: tree_state_path,
1117 source: err,
1118 });
1119 }
1120 Ok(file) => file,
1121 };
1122
1123 let mut wc = Self::empty(store, working_copy_path, state_path, tree_state_settings);
1124 wc.read(&tree_state_path, file)?;
1125 Ok(wc)
1126 }
1127
1128 fn update_own_mtime(&mut self) {
1129 if let Ok(metadata) = self.state_path.join("tree_state").symlink_metadata()
1130 && let Ok(mtime) = mtime_from_metadata(&metadata)
1131 {
1132 self.own_mtime = mtime;
1133 } else {
1134 self.own_mtime = MillisSinceEpoch(0);
1135 }
1136 }
1137
1138 fn read(&mut self, tree_state_path: &Path, mut file: File) -> Result<(), TreeStateError> {
1139 self.update_own_mtime();
1140 let mut buf = Vec::new();
1141 file.read_to_end(&mut buf)
1142 .map_err(|err| TreeStateError::ReadTreeState {
1143 path: tree_state_path.to_owned(),
1144 source: err,
1145 })?;
1146 let proto = crate::protos::local_working_copy::TreeState::decode(&*buf).map_err(|err| {
1147 TreeStateError::DecodeTreeState {
1148 path: tree_state_path.to_owned(),
1149 source: err,
1150 }
1151 })?;
1152 #[expect(deprecated)]
1153 if proto.tree_ids.is_empty() {
1154 self.tree = MergedTree::resolved(
1155 self.store.clone(),
1156 TreeId::new(proto.legacy_tree_id.clone()),
1157 );
1158 } else {
1159 let tree_ids_builder: MergeBuilder<TreeId> = proto
1160 .tree_ids
1161 .iter()
1162 .map(|id| TreeId::new(id.clone()))
1163 .collect();
1164 self.tree = MergedTree::new(
1165 self.store.clone(),
1166 tree_ids_builder.build(),
1167 ConflictLabels::from_vec(proto.conflict_labels),
1168 );
1169 }
1170 self.file_states =
1171 FileStatesMap::from_proto(proto.file_states, proto.is_file_states_sorted);
1172 self.sparse_patterns = sparse_patterns_from_proto(proto.sparse_patterns.as_ref());
1173 self.watchman_clock = proto.watchman_clock;
1174 Ok(())
1175 }
1176
1177 #[expect(clippy::assigning_clones, clippy::field_reassign_with_default)]
1178 pub fn save(&mut self) -> Result<(), TreeStateError> {
1179 let mut proto: crate::protos::local_working_copy::TreeState = Default::default();
1180 proto.tree_ids = self
1181 .tree
1182 .tree_ids()
1183 .iter()
1184 .map(|id| id.to_bytes())
1185 .collect();
1186 proto.conflict_labels = self.tree.labels().as_slice().to_owned();
1187 proto.file_states = self.file_states.data.clone();
1188 proto.is_file_states_sorted = true;
1190 let mut sparse_patterns = crate::protos::local_working_copy::SparsePatterns::default();
1191 for path in &self.sparse_patterns {
1192 sparse_patterns
1193 .prefixes
1194 .push(path.as_internal_file_string().to_owned());
1195 }
1196 proto.sparse_patterns = Some(sparse_patterns);
1197 proto.watchman_clock = self.watchman_clock.clone();
1198
1199 let wrap_write_err = |source| TreeStateError::WriteTreeState {
1200 path: self.state_path.clone(),
1201 source,
1202 };
1203 let mut temp_file = NamedTempFile::new_in(&self.state_path).map_err(wrap_write_err)?;
1204 temp_file
1205 .as_file_mut()
1206 .write_all(&proto.encode_to_vec())
1207 .map_err(wrap_write_err)?;
1208 self.update_own_mtime();
1211 let target_path = self.state_path.join("tree_state");
1214 persist_temp_file(temp_file, &target_path).map_err(|source| {
1215 TreeStateError::PersistTreeState {
1216 path: target_path.clone(),
1217 source,
1218 }
1219 })?;
1220 Ok(())
1221 }
1222
1223 fn reset_watchman(&mut self) {
1224 self.watchman_clock.take();
1225 }
1226
1227 #[cfg(feature = "watchman")]
1228 #[instrument(skip(self))]
1229 pub async fn query_watchman(
1230 &self,
1231 config: &WatchmanConfig,
1232 ) -> Result<(watchman::Clock, Option<Vec<PathBuf>>), TreeStateError> {
1233 let previous_clock = self.watchman_clock.clone().map(watchman::Clock::from);
1234
1235 let tokio_fn = async || {
1236 let fsmonitor = watchman::Fsmonitor::init(&self.working_copy_path, config)
1237 .await
1238 .map_err(|err| TreeStateError::Fsmonitor(Box::new(err)))?;
1239 fsmonitor
1240 .query_changed_files(previous_clock)
1241 .await
1242 .map_err(|err| TreeStateError::Fsmonitor(Box::new(err)))
1243 };
1244
1245 match tokio::runtime::Handle::try_current() {
1246 Ok(_handle) => tokio_fn().await,
1247 Err(_) => {
1248 let runtime = tokio::runtime::Builder::new_current_thread()
1249 .enable_all()
1250 .build()
1251 .map_err(|err| TreeStateError::Fsmonitor(Box::new(err)))?;
1252 runtime.block_on(tokio_fn())
1253 }
1254 }
1255 }
1256
1257 #[cfg(feature = "watchman")]
1258 #[instrument(skip(self))]
1259 pub async fn is_watchman_trigger_registered(
1260 &self,
1261 config: &WatchmanConfig,
1262 ) -> Result<bool, TreeStateError> {
1263 let tokio_fn = async || {
1264 let fsmonitor = watchman::Fsmonitor::init(&self.working_copy_path, config)
1265 .await
1266 .map_err(|err| TreeStateError::Fsmonitor(Box::new(err)))?;
1267 fsmonitor
1268 .is_trigger_registered()
1269 .await
1270 .map_err(|err| TreeStateError::Fsmonitor(Box::new(err)))
1271 };
1272
1273 match tokio::runtime::Handle::try_current() {
1274 Ok(_handle) => tokio_fn().await,
1275 Err(_) => {
1276 let runtime = tokio::runtime::Builder::new_current_thread()
1277 .enable_all()
1278 .build()
1279 .map_err(|err| TreeStateError::Fsmonitor(Box::new(err)))?;
1280 runtime.block_on(tokio_fn())
1281 }
1282 }
1283 }
1284}
1285
1286impl TreeState {
1288 #[instrument(skip_all)]
1291 pub async fn snapshot(
1292 &mut self,
1293 options: &SnapshotOptions<'_>,
1294 ) -> Result<(bool, SnapshotStats), SnapshotError> {
1295 let SnapshotOptions {
1296 base_ignores,
1297 progress,
1298 start_tracking_matcher,
1299 force_tracking_matcher,
1300 max_new_file_size,
1301 } = options;
1302
1303 let sparse_matcher = self.sparse_matcher();
1304
1305 let fsmonitor_clock_needs_save = self.fsmonitor_settings != FsmonitorSettings::None;
1306 let mut is_dirty = fsmonitor_clock_needs_save;
1307 let FsmonitorMatcher {
1308 matcher: fsmonitor_matcher,
1309 watchman_clock,
1310 } = self
1311 .make_fsmonitor_matcher(&self.fsmonitor_settings)
1312 .await?;
1313 let fsmonitor_matcher = match fsmonitor_matcher.as_ref() {
1314 None => &EverythingMatcher,
1315 Some(fsmonitor_matcher) => fsmonitor_matcher.as_ref(),
1316 };
1317
1318 let matcher = IntersectionMatcher::new(
1319 sparse_matcher.as_ref(),
1320 UnionMatcher::new(fsmonitor_matcher, force_tracking_matcher),
1321 );
1322 if matcher.visit(RepoPath::root()).is_nothing() {
1323 self.watchman_clock = watchman_clock;
1325 return Ok((is_dirty, SnapshotStats::default()));
1326 }
1327
1328 let (tree_entries_tx, tree_entries_rx) = channel();
1329 let (file_states_tx, file_states_rx) = channel();
1330 let (untracked_paths_tx, untracked_paths_rx) = channel();
1331 let (deleted_files_tx, deleted_files_rx) = channel();
1332
1333 trace_span!("traverse filesystem").in_scope(|| -> Result<(), SnapshotError> {
1334 let snapshotter = FileSnapshotter {
1335 tree_state: self,
1336 current_tree: &self.tree,
1337 matcher: &matcher,
1338 start_tracking_matcher,
1339 force_tracking_matcher,
1340 tree_entries_tx,
1342 file_states_tx,
1343 untracked_paths_tx,
1344 deleted_files_tx,
1345 error: OnceLock::new(),
1346 progress: *progress,
1347 max_new_file_size: *max_new_file_size,
1348 };
1349 let directory_to_visit = DirectoryToVisit {
1350 dir: RepoPathBuf::root(),
1351 disk_dir: self.working_copy_path.clone(),
1352 git_ignore: base_ignores.clone(),
1353 file_states: self.file_states.all(),
1354 };
1355 rayon::scope(|scope| {
1357 snapshotter.spawn_ok(scope, |scope| {
1358 snapshotter.visit_directory(directory_to_visit, scope)
1359 });
1360 });
1361 snapshotter.into_result()
1362 })?;
1363
1364 let stats = SnapshotStats {
1365 untracked_paths: untracked_paths_rx.into_iter().collect(),
1366 };
1367 let mut tree_builder = MergedTreeBuilder::new(self.tree.clone());
1368 trace_span!("process tree entries").in_scope(|| {
1369 for (path, tree_values) in &tree_entries_rx {
1370 tree_builder.set_or_remove(path, tree_values);
1371 }
1372 });
1373 let deleted_files = trace_span!("process deleted tree entries").in_scope(|| {
1374 let deleted_files = HashSet::from_iter(deleted_files_rx);
1375 is_dirty |= !deleted_files.is_empty();
1376 for file in &deleted_files {
1377 tree_builder.set_or_remove(file.clone(), Merge::absent());
1378 }
1379 deleted_files
1380 });
1381 trace_span!("process file states").in_scope(|| {
1382 let changed_file_states = file_states_rx
1383 .iter()
1384 .sorted_unstable_by(|(path1, _), (path2, _)| path1.cmp(path2))
1385 .collect_vec();
1386 is_dirty |= !changed_file_states.is_empty();
1387 self.file_states
1388 .merge_in(changed_file_states, &deleted_files);
1389 });
1390 trace_span!("write tree")
1391 .in_scope(async || -> Result<(), BackendError> {
1392 let new_tree = tree_builder.write_tree().await?;
1393 is_dirty |= new_tree.tree_ids_and_labels() != self.tree.tree_ids_and_labels();
1394 self.tree = new_tree.clone();
1395 Ok(())
1396 })
1397 .await?;
1398 if cfg!(debug_assertions) {
1399 let tree_paths: HashSet<_> = self
1400 .tree
1401 .entries_matching(sparse_matcher.as_ref())
1402 .filter_map(|(path, result)| result.is_ok().then_some(path))
1403 .collect();
1404 let file_states = self.file_states.all();
1405 let state_paths: HashSet<_> = file_states.paths().map(|path| path.to_owned()).collect();
1406 assert_eq!(state_paths, tree_paths);
1407 }
1408 if stats.untracked_paths.is_empty() || watchman_clock.is_none() {
1412 self.watchman_clock = watchman_clock;
1413 } else {
1414 tracing::info!("not updating watchman clock because there are untracked files");
1415 }
1416 Ok((is_dirty, stats))
1417 }
1418
1419 #[instrument(skip_all)]
1420 async fn make_fsmonitor_matcher(
1421 &self,
1422 fsmonitor_settings: &FsmonitorSettings,
1423 ) -> Result<FsmonitorMatcher, SnapshotError> {
1424 let (watchman_clock, changed_files) = match fsmonitor_settings {
1425 FsmonitorSettings::None => (None, None),
1426 FsmonitorSettings::Test { changed_files } => (None, Some(changed_files.clone())),
1427 #[cfg(feature = "watchman")]
1428 FsmonitorSettings::Watchman(config) => match self.query_watchman(config).await {
1429 Ok((watchman_clock, changed_files)) => (Some(watchman_clock.into()), changed_files),
1430 Err(err) => {
1431 tracing::warn!(?err, "Failed to query filesystem monitor");
1432 (None, None)
1433 }
1434 },
1435 #[cfg(not(feature = "watchman"))]
1436 FsmonitorSettings::Watchman(_) => {
1437 return Err(SnapshotError::Other {
1438 message: "Failed to query the filesystem monitor".to_string(),
1439 err: "Cannot query Watchman because jj was not compiled with the `watchman` \
1440 feature (consider disabling `fsmonitor.backend`)"
1441 .into(),
1442 });
1443 }
1444 };
1445 let matcher: Option<Box<dyn Matcher>> = match changed_files {
1446 None => None,
1447 Some(changed_files) => {
1448 let (repo_paths, gitignore_prefixes) = trace_span!("processing fsmonitor paths")
1449 .in_scope(|| {
1450 let repo_paths = changed_files
1451 .iter()
1452 .filter_map(|path| RepoPathBuf::from_relative_path(path).ok())
1453 .collect_vec();
1454 let gitignore_prefixes = repo_paths
1457 .iter()
1458 .filter_map(|repo_path| {
1459 let (parent, basename) = repo_path.split()?;
1460 (basename.as_internal_str() == ".gitignore")
1461 .then(|| parent.to_owned())
1462 })
1463 .collect_vec();
1464 (repo_paths, gitignore_prefixes)
1465 });
1466
1467 let matcher: Box<dyn Matcher> = if gitignore_prefixes.is_empty() {
1468 Box::new(FilesMatcher::new(repo_paths))
1469 } else {
1470 Box::new(UnionMatcher::new(
1471 FilesMatcher::new(repo_paths),
1472 PrefixMatcher::new(gitignore_prefixes),
1473 ))
1474 };
1475
1476 Some(matcher)
1477 }
1478 };
1479 Ok(FsmonitorMatcher {
1480 matcher,
1481 watchman_clock,
1482 })
1483 }
1484}
1485
1486struct DirectoryToVisit<'a> {
1487 dir: RepoPathBuf,
1488 disk_dir: PathBuf,
1489 git_ignore: Arc<GitIgnoreFile>,
1490 file_states: FileStates<'a>,
1491}
1492
1493#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1494enum PresentDirEntryKind {
1495 Dir,
1496 File,
1497}
1498
1499#[derive(Clone, Debug)]
1500struct PresentDirEntries {
1501 dirs: HashSet<String>,
1502 files: HashSet<String>,
1503}
1504
1505struct FileSnapshotter<'a> {
1507 tree_state: &'a TreeState,
1508 current_tree: &'a MergedTree,
1509 matcher: &'a dyn Matcher,
1510 start_tracking_matcher: &'a dyn Matcher,
1511 force_tracking_matcher: &'a dyn Matcher,
1512 tree_entries_tx: Sender<(RepoPathBuf, MergedTreeValue)>,
1513 file_states_tx: Sender<(RepoPathBuf, FileState)>,
1514 untracked_paths_tx: Sender<(RepoPathBuf, UntrackedReason)>,
1515 deleted_files_tx: Sender<RepoPathBuf>,
1516 error: OnceLock<SnapshotError>,
1517 progress: Option<&'a SnapshotProgress<'a>>,
1518 max_new_file_size: u64,
1519}
1520
1521impl FileSnapshotter<'_> {
1522 fn spawn_ok<'scope, F>(&'scope self, scope: &rayon::Scope<'scope>, body: F)
1523 where
1524 F: FnOnce(&rayon::Scope<'scope>) -> Result<(), SnapshotError> + Send + 'scope,
1525 {
1526 scope.spawn(|scope| {
1527 if self.error.get().is_some() {
1528 return;
1529 }
1530 match body(scope) {
1531 Ok(()) => {}
1532 Err(err) => self.error.set(err).unwrap_or(()),
1533 }
1534 });
1535 }
1536
1537 fn into_result(self) -> Result<(), SnapshotError> {
1539 match self.error.into_inner() {
1540 Some(err) => Err(err),
1541 None => Ok(()),
1542 }
1543 }
1544
1545 fn visit_directory<'scope>(
1548 &'scope self,
1549 directory_to_visit: DirectoryToVisit<'scope>,
1550 scope: &rayon::Scope<'scope>,
1551 ) -> Result<(), SnapshotError> {
1552 let DirectoryToVisit {
1553 dir,
1554 disk_dir,
1555 git_ignore,
1556 file_states,
1557 } = directory_to_visit;
1558
1559 let git_ignore = git_ignore.chain_with_file(&dir, disk_dir.join(".gitignore"))?;
1560 let dir_entries: Vec<_> = disk_dir
1561 .read_dir()
1562 .and_then(|entries| entries.try_collect())
1563 .map_err(|err| SnapshotError::Other {
1564 message: format!("Failed to read directory {}", disk_dir.display()),
1565 err: err.into(),
1566 })?;
1567 let (dirs, files) = dir_entries
1568 .into_par_iter()
1569 .with_min_len(100)
1572 .filter_map(|entry| {
1573 self.process_dir_entry(&dir, &git_ignore, file_states, &entry, scope)
1574 .block_on()
1575 .transpose()
1576 })
1577 .map(|item| match item {
1578 Ok((PresentDirEntryKind::Dir, name)) => Ok(Either::Left(name)),
1579 Ok((PresentDirEntryKind::File, name)) => Ok(Either::Right(name)),
1580 Err(err) => Err(err),
1581 })
1582 .collect::<Result<_, _>>()?;
1583 let present_entries = PresentDirEntries { dirs, files };
1584 self.emit_deleted_files(&dir, file_states, &present_entries);
1585 Ok(())
1586 }
1587
1588 async fn process_dir_entry<'scope>(
1589 &'scope self,
1590 dir: &RepoPath,
1591 git_ignore: &Arc<GitIgnoreFile>,
1592 file_states: FileStates<'scope>,
1593 entry: &DirEntry,
1594 scope: &rayon::Scope<'scope>,
1595 ) -> Result<Option<(PresentDirEntryKind, String)>, SnapshotError> {
1596 let file_type = entry.file_type().unwrap();
1597 let file_name = entry.file_name();
1598 let name_string = file_name
1599 .into_string()
1600 .map_err(|path| SnapshotError::InvalidUtf8Path { path })?;
1601
1602 if RESERVED_DIR_NAMES.contains(&name_string.as_str()) {
1603 return Ok(None);
1604 }
1605 let name = RepoPathComponent::new(&name_string).unwrap();
1606 let path = dir.join(name);
1607 let maybe_current_file_state = file_states.get_at(dir, name);
1608 if let Some(file_state) = &maybe_current_file_state
1609 && file_state.file_type == FileType::GitSubmodule
1610 {
1611 return Ok(None);
1612 }
1613
1614 if file_type.is_dir() {
1615 let file_states = file_states.prefixed_at(dir, name);
1616 let disk_dir = entry.path();
1624 for &name in RESERVED_DIR_NAMES {
1625 if disk_dir.join(name).symlink_metadata().is_ok() {
1626 return Ok(None);
1627 }
1628 }
1629
1630 if git_ignore.matches_dir(&path)
1631 && self.force_tracking_matcher.visit(&path).is_nothing()
1632 {
1633 self.spawn_ok(scope, move |_| {
1639 self.visit_tracked_files(file_states).block_on()
1640 });
1641 } else if !self.matcher.visit(&path).is_nothing() {
1642 let directory_to_visit = DirectoryToVisit {
1643 dir: path,
1644 disk_dir,
1645 git_ignore: git_ignore.clone(),
1646 file_states,
1647 };
1648 self.spawn_ok(scope, |scope| {
1649 self.visit_directory(directory_to_visit, scope)
1650 });
1651 }
1652 Ok(Some((PresentDirEntryKind::Dir, name_string)))
1655 } else if self.matcher.matches(&path) {
1656 if let Some(progress) = self.progress {
1657 progress(&path);
1658 }
1659 if maybe_current_file_state.is_none()
1660 && (git_ignore.matches_file(&path) && !self.force_tracking_matcher.matches(&path))
1661 {
1662 Ok(None)
1665 } else if maybe_current_file_state.is_none()
1666 && !self.start_tracking_matcher.matches(&path)
1667 {
1668 self.untracked_paths_tx
1670 .send((path, UntrackedReason::FileNotAutoTracked))
1671 .ok();
1672 Ok(None)
1673 } else {
1674 let metadata = entry.metadata().map_err(|err| SnapshotError::Other {
1675 message: format!("Failed to stat file {}", entry.path().display()),
1676 err: err.into(),
1677 })?;
1678 if maybe_current_file_state.is_none()
1679 && (metadata.len() > self.max_new_file_size
1680 && !self.force_tracking_matcher.matches(&path))
1681 {
1682 let reason = UntrackedReason::FileTooLarge {
1684 size: metadata.len(),
1685 max_size: self.max_new_file_size,
1686 };
1687 self.untracked_paths_tx.send((path, reason)).ok();
1688 Ok(None)
1689 } else if let Some(new_file_state) = file_state(&metadata)
1690 .map_err(|err| snapshot_error_for_mtime_out_of_range(err, &entry.path()))?
1691 {
1692 self.process_present_file(
1693 path,
1694 &entry.path(),
1695 maybe_current_file_state.as_ref(),
1696 new_file_state,
1697 )
1698 .await?;
1699 Ok(Some((PresentDirEntryKind::File, name_string)))
1700 } else {
1701 Ok(None)
1703 }
1704 }
1705 } else {
1706 Ok(None)
1707 }
1708 }
1709
1710 async fn visit_tracked_files(&self, file_states: FileStates<'_>) -> Result<(), SnapshotError> {
1712 for (tracked_path, current_file_state) in file_states {
1713 if current_file_state.file_type == FileType::GitSubmodule {
1714 continue;
1715 }
1716 if !self.matcher.matches(tracked_path) {
1717 continue;
1718 }
1719 let disk_path = tracked_path.to_fs_path(&self.tree_state.working_copy_path)?;
1720 let metadata = match disk_path.symlink_metadata() {
1721 Ok(metadata) => Some(metadata),
1722 Err(err) if err.kind() == io::ErrorKind::NotFound => None,
1723 Err(err) => {
1724 return Err(SnapshotError::Other {
1725 message: format!("Failed to stat file {}", disk_path.display()),
1726 err: err.into(),
1727 });
1728 }
1729 };
1730 if let Some(metadata) = &metadata
1731 && let Some(new_file_state) = file_state(metadata)
1732 .map_err(|err| snapshot_error_for_mtime_out_of_range(err, &disk_path))?
1733 {
1734 self.process_present_file(
1735 tracked_path.to_owned(),
1736 &disk_path,
1737 Some(¤t_file_state),
1738 new_file_state,
1739 )
1740 .await?;
1741 } else {
1742 self.deleted_files_tx.send(tracked_path.to_owned()).ok();
1743 }
1744 }
1745 Ok(())
1746 }
1747
1748 async fn process_present_file(
1749 &self,
1750 path: RepoPathBuf,
1751 disk_path: &Path,
1752 maybe_current_file_state: Option<&FileState>,
1753 mut new_file_state: FileState,
1754 ) -> Result<(), SnapshotError> {
1755 let update = self
1756 .get_updated_tree_value(&path, disk_path, maybe_current_file_state, &new_file_state)
1757 .await?;
1758 if matches!(new_file_state.file_type, FileType::Normal { .. })
1760 && !update.as_ref().is_some_and(|update| update.is_resolved())
1761 {
1762 new_file_state.materialized_conflict_data =
1763 maybe_current_file_state.and_then(|state| state.materialized_conflict_data);
1764 }
1765 if let Some(tree_value) = update {
1766 self.tree_entries_tx.send((path.clone(), tree_value)).ok();
1767 }
1768 if Some(&new_file_state) != maybe_current_file_state {
1769 self.file_states_tx.send((path, new_file_state)).ok();
1770 }
1771 Ok(())
1772 }
1773
1774 fn emit_deleted_files(
1776 &self,
1777 dir: &RepoPath,
1778 file_states: FileStates<'_>,
1779 present_entries: &PresentDirEntries,
1780 ) {
1781 let file_state_chunks = file_states.iter().chunk_by(|(path, _state)| {
1782 debug_assert!(path.starts_with(dir));
1785 let slash = usize::from(!dir.is_root());
1786 let len = dir.as_internal_file_string().len() + slash;
1787 let tail = path.as_internal_file_string().get(len..).unwrap_or("");
1788 match tail.split_once('/') {
1789 Some((name, _)) => (PresentDirEntryKind::Dir, name),
1790 None => (PresentDirEntryKind::File, tail),
1791 }
1792 });
1793 file_state_chunks
1794 .into_iter()
1795 .filter(|&((kind, name), _)| match kind {
1796 PresentDirEntryKind::Dir => !present_entries.dirs.contains(name),
1797 PresentDirEntryKind::File => !present_entries.files.contains(name),
1798 })
1799 .flat_map(|(_, chunk)| chunk)
1800 .filter(|(_, state)| state.file_type != FileType::GitSubmodule)
1802 .filter(|(path, _)| self.matcher.matches(path))
1803 .try_for_each(|(path, _)| self.deleted_files_tx.send(path.to_owned()))
1804 .ok();
1805 }
1806
1807 async fn get_updated_tree_value(
1808 &self,
1809 repo_path: &RepoPath,
1810 disk_path: &Path,
1811 maybe_current_file_state: Option<&FileState>,
1812 new_file_state: &FileState,
1813 ) -> Result<Option<MergedTreeValue>, SnapshotError> {
1814 let clean = match maybe_current_file_state {
1815 None => {
1816 false
1818 }
1819 Some(current_file_state) => {
1820 new_file_state.is_clean(current_file_state)
1823 && current_file_state.mtime < self.tree_state.own_mtime
1824 }
1825 };
1826 if clean {
1827 Ok(None)
1828 } else {
1829 let current_tree_values = self.current_tree.path_value(repo_path).await?;
1830 let new_file_type = if !self.tree_state.symlink_support {
1831 let mut new_file_type = new_file_state.file_type.clone();
1832 if matches!(new_file_type, FileType::Normal { .. })
1833 && matches!(current_tree_values.as_normal(), Some(TreeValue::Symlink(_)))
1834 {
1835 new_file_type = FileType::Symlink;
1836 }
1837 new_file_type
1838 } else {
1839 new_file_state.file_type.clone()
1840 };
1841 let new_tree_values = match new_file_type {
1842 FileType::Normal { exec_bit } => {
1843 self.write_path_to_store(
1844 repo_path,
1845 disk_path,
1846 ¤t_tree_values,
1847 exec_bit,
1848 maybe_current_file_state.and_then(|state| state.materialized_conflict_data),
1849 )
1850 .await?
1851 }
1852 FileType::Symlink => {
1853 let id = self.write_symlink_to_store(repo_path, disk_path).await?;
1854 Merge::normal(TreeValue::Symlink(id))
1855 }
1856 FileType::GitSubmodule => panic!("git submodule cannot be written to store"),
1857 };
1858 if new_tree_values != current_tree_values {
1859 Ok(Some(new_tree_values))
1860 } else {
1861 Ok(None)
1862 }
1863 }
1864 }
1865
1866 fn store(&self) -> &Store {
1867 &self.tree_state.store
1868 }
1869
1870 async fn write_path_to_store(
1871 &self,
1872 repo_path: &RepoPath,
1873 disk_path: &Path,
1874 current_tree_values: &MergedTreeValue,
1875 exec_bit: ExecBit,
1876 materialized_conflict_data: Option<MaterializedConflictData>,
1877 ) -> Result<MergedTreeValue, SnapshotError> {
1878 if let Some(current_tree_value) = current_tree_values.as_resolved() {
1879 let id = self.write_file_to_store(repo_path, disk_path).await?;
1880 let executable = exec_bit.for_tree_value(self.tree_state.exec_policy, || {
1882 if let Some(TreeValue::File {
1883 id: _,
1884 executable,
1885 copy_id: _,
1886 }) = current_tree_value
1887 {
1888 Some(*executable)
1889 } else {
1890 None
1891 }
1892 });
1893 let copy_id = {
1895 if let Some(TreeValue::File {
1896 id: _,
1897 executable: _,
1898 copy_id,
1899 }) = current_tree_value
1900 {
1901 copy_id.clone()
1902 } else {
1903 CopyId::placeholder()
1904 }
1905 };
1906 Ok(Merge::normal(TreeValue::File {
1907 id,
1908 executable,
1909 copy_id,
1910 }))
1911 } else if let Some(old_file_ids) = current_tree_values.to_file_merge() {
1912 let copy_id_merge = current_tree_values.to_copy_id_merge().unwrap();
1914 let copy_id = copy_id_merge
1915 .resolve_trivial(SameChange::Accept)
1916 .cloned()
1917 .flatten()
1918 .unwrap_or_else(CopyId::placeholder);
1919 let mut contents = vec![];
1920 let file = File::open(disk_path).map_err(|err| SnapshotError::Other {
1921 message: format!("Failed to open file {}", disk_path.display()),
1922 err: err.into(),
1923 })?;
1924 self.tree_state
1925 .target_eol_strategy
1926 .convert_eol_for_snapshot(AllowStdIo::new(file))
1927 .await
1928 .map_err(|err| SnapshotError::Other {
1929 message: "Failed to convert the EOL".to_string(),
1930 err: err.into(),
1931 })?
1932 .read_to_end(&mut contents)
1933 .await
1934 .map_err(|err| SnapshotError::Other {
1935 message: "Failed to read the EOL converted contents".to_string(),
1936 err: err.into(),
1937 })?;
1938 let new_file_ids = conflicts::update_from_content(
1942 &old_file_ids,
1943 self.store(),
1944 repo_path,
1945 &contents,
1946 materialized_conflict_data.map_or(MIN_CONFLICT_MARKER_LEN, |data| {
1947 data.conflict_marker_len as usize
1948 }),
1949 )
1950 .await?;
1951 match new_file_ids.into_resolved() {
1952 Ok(file_id) => {
1953 let executable = exec_bit.for_tree_value(self.tree_state.exec_policy, || {
1955 current_tree_values
1956 .to_executable_merge()
1957 .as_ref()
1958 .and_then(conflicts::resolve_file_executable)
1959 });
1960 Ok(Merge::normal(TreeValue::File {
1961 id: file_id.unwrap(),
1962 executable,
1963 copy_id,
1964 }))
1965 }
1966 Err(new_file_ids) => {
1967 if new_file_ids != old_file_ids {
1968 Ok(current_tree_values.with_new_file_ids(&new_file_ids))
1969 } else {
1970 Ok(current_tree_values.clone())
1971 }
1972 }
1973 }
1974 } else {
1975 Ok(current_tree_values.clone())
1976 }
1977 }
1978
1979 async fn write_file_to_store(
1980 &self,
1981 path: &RepoPath,
1982 disk_path: &Path,
1983 ) -> Result<FileId, SnapshotError> {
1984 let file = File::open(disk_path).map_err(|err| SnapshotError::Other {
1985 message: format!("Failed to open file {}", disk_path.display()),
1986 err: err.into(),
1987 })?;
1988 let mut contents = self
1989 .tree_state
1990 .target_eol_strategy
1991 .convert_eol_for_snapshot(AllowStdIo::new(file))
1992 .await
1993 .map_err(|err| SnapshotError::Other {
1994 message: "Failed to convert the EOL".to_string(),
1995 err: err.into(),
1996 })?;
1997 Ok(self.store().write_file(path, &mut contents).await?)
1998 }
1999
2000 async fn write_symlink_to_store(
2001 &self,
2002 path: &RepoPath,
2003 disk_path: &Path,
2004 ) -> Result<SymlinkId, SnapshotError> {
2005 if self.tree_state.symlink_support {
2006 let target = disk_path.read_link().map_err(|err| SnapshotError::Other {
2007 message: format!("Failed to read symlink {}", disk_path.display()),
2008 err: err.into(),
2009 })?;
2010 let str_target = symlink_target_convert_to_store(&target).ok_or_else(|| {
2011 SnapshotError::InvalidUtf8SymlinkTarget {
2012 path: disk_path.to_path_buf(),
2013 }
2014 })?;
2015 Ok(self.store().write_symlink(path, &str_target).await?)
2016 } else {
2017 let target = fs::read(disk_path).map_err(|err| SnapshotError::Other {
2018 message: format!("Failed to read file {}", disk_path.display()),
2019 err: err.into(),
2020 })?;
2021 let string_target =
2022 String::from_utf8(target).map_err(|_| SnapshotError::InvalidUtf8SymlinkTarget {
2023 path: disk_path.to_path_buf(),
2024 })?;
2025 Ok(self.store().write_symlink(path, &string_target).await?)
2026 }
2027 }
2028}
2029
2030fn snapshot_error_for_mtime_out_of_range(err: MtimeOutOfRange, path: &Path) -> SnapshotError {
2031 SnapshotError::Other {
2032 message: format!("Failed to process file metadata {}", path.display()),
2033 err: err.into(),
2034 }
2035}
2036
2037impl TreeState {
2039 async fn write_file(
2040 &self,
2041 disk_path: &Path,
2042 contents: impl AsyncRead + Send + Unpin,
2043 exec_bit: ExecBit,
2044 apply_eol_conversion: bool,
2045 ) -> Result<FileState, CheckoutError> {
2046 let mut file = File::options()
2047 .write(true)
2048 .create_new(true) .open(disk_path)
2050 .map_err(|err| CheckoutError::Other {
2051 message: format!("Failed to open file {} for writing", disk_path.display()),
2052 err: err.into(),
2053 })?;
2054 let contents = if apply_eol_conversion {
2055 self.target_eol_strategy
2056 .convert_eol_for_update(contents)
2057 .await
2058 .map_err(|err| CheckoutError::Other {
2059 message: "Failed to convert the EOL for the content".to_string(),
2060 err: err.into(),
2061 })?
2062 } else {
2063 Box::new(contents)
2064 };
2065 let size = copy_async_to_sync(contents, &mut file)
2066 .await
2067 .map_err(|err| CheckoutError::Other {
2068 message: format!(
2069 "Failed to write the content to the file {}",
2070 disk_path.display()
2071 ),
2072 err: err.into(),
2073 })?;
2074 set_executable(exec_bit, disk_path)
2075 .map_err(|err| checkout_error_for_stat_error(err, disk_path))?;
2076 let metadata = file
2081 .metadata()
2082 .map_err(|err| checkout_error_for_stat_error(err, disk_path))?;
2083 FileState::for_file(exec_bit, size as u64, &metadata)
2084 .map_err(|err| checkout_error_for_mtime_out_of_range(err, disk_path))
2085 }
2086
2087 fn write_symlink(&self, disk_path: &Path, target: String) -> Result<FileState, CheckoutError> {
2088 let target = symlink_target_convert_to_disk(&target);
2089
2090 if cfg!(windows) {
2091 debug_assert_ne!(
2099 target.as_os_str().to_str().map(|path| path.contains('/')),
2100 Some(true),
2101 r#"Expect the symlink target doesn't contain "/", but got invalid symlink target: {}."#,
2102 target.display()
2103 );
2104 }
2105
2106 symlink_file(&target, disk_path).map_err(|err| CheckoutError::Other {
2110 message: format!(
2111 "Failed to create symlink from {} to {}",
2112 disk_path.display(),
2113 target.display()
2114 ),
2115 err: err.into(),
2116 })?;
2117 let metadata = disk_path
2118 .symlink_metadata()
2119 .map_err(|err| checkout_error_for_stat_error(err, disk_path))?;
2120 FileState::for_symlink(&metadata)
2121 .map_err(|err| checkout_error_for_mtime_out_of_range(err, disk_path))
2122 }
2123
2124 async fn write_conflict(
2125 &self,
2126 disk_path: &Path,
2127 contents: &[u8],
2128 exec_bit: ExecBit,
2129 ) -> Result<FileState, CheckoutError> {
2130 let contents = self
2131 .target_eol_strategy
2132 .convert_eol_for_update(contents)
2133 .await
2134 .map_err(|err| CheckoutError::Other {
2135 message: "Failed to convert the EOL when writing a merge conflict".to_string(),
2136 err: err.into(),
2137 })?;
2138 let mut file = OpenOptions::new()
2139 .write(true)
2140 .create_new(true) .open(disk_path)
2142 .map_err(|err| CheckoutError::Other {
2143 message: format!("Failed to open file {} for writing", disk_path.display()),
2144 err: err.into(),
2145 })?;
2146 let size = copy_async_to_sync(contents, &mut file)
2147 .await
2148 .map_err(|err| CheckoutError::Other {
2149 message: format!("Failed to write conflict to file {}", disk_path.display()),
2150 err: err.into(),
2151 })? as u64;
2152 set_executable(exec_bit, disk_path)
2153 .map_err(|err| checkout_error_for_stat_error(err, disk_path))?;
2154 let metadata = file
2155 .metadata()
2156 .map_err(|err| checkout_error_for_stat_error(err, disk_path))?;
2157 FileState::for_file(exec_bit, size, &metadata)
2158 .map_err(|err| checkout_error_for_mtime_out_of_range(err, disk_path))
2159 }
2160
2161 pub fn check_out(&mut self, new_tree: &MergedTree) -> Result<CheckoutStats, CheckoutError> {
2162 let old_tree = self.tree.clone();
2163 let stats = self
2164 .update(&old_tree, new_tree, self.sparse_matcher().as_ref())
2165 .block_on()?;
2166 self.tree = new_tree.clone();
2167 Ok(stats)
2168 }
2169
2170 pub fn set_sparse_patterns(
2171 &mut self,
2172 sparse_patterns: Vec<RepoPathBuf>,
2173 ) -> Result<CheckoutStats, CheckoutError> {
2174 let tree = self.tree.clone();
2175 let old_matcher = PrefixMatcher::new(&self.sparse_patterns);
2176 let new_matcher = PrefixMatcher::new(&sparse_patterns);
2177 let added_matcher = DifferenceMatcher::new(&new_matcher, &old_matcher);
2178 let removed_matcher = DifferenceMatcher::new(&old_matcher, &new_matcher);
2179 let empty_tree = self.store.empty_merged_tree();
2180 let added_stats = self.update(&empty_tree, &tree, &added_matcher).block_on()?;
2181 let removed_stats = self
2182 .update(&tree, &empty_tree, &removed_matcher)
2183 .block_on()?;
2184 self.sparse_patterns = sparse_patterns;
2185 assert_eq!(added_stats.updated_files, 0);
2186 assert_eq!(added_stats.removed_files, 0);
2187 assert_eq!(removed_stats.updated_files, 0);
2188 assert_eq!(removed_stats.added_files, 0);
2189 assert_eq!(removed_stats.skipped_files, 0);
2190 Ok(CheckoutStats {
2191 updated_files: 0,
2192 added_files: added_stats.added_files,
2193 removed_files: removed_stats.removed_files,
2194 skipped_files: added_stats.skipped_files,
2195 })
2196 }
2197
2198 async fn update(
2199 &mut self,
2200 old_tree: &MergedTree,
2201 new_tree: &MergedTree,
2202 matcher: &dyn Matcher,
2203 ) -> Result<CheckoutStats, CheckoutError> {
2204 let mut stats = CheckoutStats {
2207 updated_files: 0,
2208 added_files: 0,
2209 removed_files: 0,
2210 skipped_files: 0,
2211 };
2212 let mut changed_file_states = Vec::new();
2213 let mut deleted_files = HashSet::new();
2214 let mut prev_created_path: RepoPathBuf = RepoPathBuf::root();
2215
2216 let mut process_diff_entry = async |path: RepoPathBuf,
2217 before: MergedTreeValue,
2218 after: MaterializedTreeValue|
2219 -> Result<(), CheckoutError> {
2220 if after.is_absent() {
2221 stats.removed_files += 1;
2222 } else if before.is_absent() {
2223 stats.added_files += 1;
2224 } else {
2225 stats.updated_files += 1;
2226 }
2227
2228 if matches!(before.as_normal(), Some(TreeValue::GitSubmodule(_)))
2236 && matches!(after, MaterializedTreeValue::GitSubmodule(_))
2237 {
2238 eprintln!("ignoring git submodule at {path:?}");
2239 return Ok(());
2242 }
2243
2244 let (common_prefix, adjusted_diff_file_path) =
2249 path.split_common_prefix(&prev_created_path);
2250
2251 let disk_path = if adjusted_diff_file_path.is_root() {
2252 path.to_fs_path(self.working_copy_path())?
2267 } else {
2268 let adjusted_working_copy_path =
2269 common_prefix.to_fs_path(self.working_copy_path())?;
2270
2271 let Some(disk_path) =
2274 create_parent_dirs(&adjusted_working_copy_path, adjusted_diff_file_path)?
2275 else {
2276 changed_file_states.push((path, FileState::placeholder()));
2277 stats.skipped_files += 1;
2278 return Ok(());
2279 };
2280
2281 prev_created_path = path
2286 .parent()
2287 .map(RepoPath::to_owned)
2288 .expect("diff path has no parent");
2289
2290 disk_path
2291 };
2292
2293 let present_file_deleted = before.is_present()
2295 && if matches!(before.as_normal(), Some(TreeValue::GitSubmodule(_))) {
2296 remove_old_submodule_dir(&disk_path)?
2297 } else {
2298 remove_old_file(&disk_path)?
2299 };
2300
2301 if !present_file_deleted && !can_create_new_file(&disk_path)? {
2303 if matches!(after, MaterializedTreeValue::GitSubmodule(_)) && disk_path.is_dir() {
2304 } else if matches!(before.as_normal(), Some(TreeValue::GitSubmodule(_)))
2310 && after.is_absent()
2311 {
2312 } else {
2318 changed_file_states.push((path, FileState::placeholder()));
2319 stats.skipped_files += 1;
2320 return Ok(());
2321 }
2322 }
2323
2324 let get_prev_exec = || self.file_states().get_exec_bit(&path);
2328
2329 let file_state = match after {
2331 MaterializedTreeValue::Absent | MaterializedTreeValue::AccessDenied(_) => {
2332 prev_created_path = RepoPathBuf::root();
2336
2337 let mut parent_dir = disk_path.parent().unwrap();
2338 loop {
2339 if fs::remove_dir(parent_dir).is_err() {
2340 break;
2341 }
2342
2343 parent_dir = parent_dir.parent().unwrap();
2344 }
2345 deleted_files.insert(path);
2346 return Ok(());
2347 }
2348 MaterializedTreeValue::File(file) => {
2349 let exec_bit =
2350 ExecBit::new_from_repo(file.executable, self.exec_policy, get_prev_exec);
2351 self.write_file(&disk_path, file.reader, exec_bit, true)
2352 .await?
2353 }
2354 MaterializedTreeValue::Symlink { id: _, target } => {
2355 if self.symlink_support {
2356 self.write_symlink(&disk_path, target)?
2357 } else {
2358 self.write_file(&disk_path, target.as_bytes(), ExecBit(false), false)
2360 .await?
2361 }
2362 }
2363 MaterializedTreeValue::GitSubmodule(_) => {
2364 eprintln!("ignoring git submodule at {path:?}");
2365 match fs::create_dir(&disk_path) {
2368 Ok(()) => {}
2369 Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
2370 Err(err) => eprintln!(
2371 "warning: failed to create submodule directory {path:?}: {err}"
2372 ),
2373 }
2374 FileState::for_gitsubmodule()
2375 }
2376 MaterializedTreeValue::Tree(_) => {
2377 panic!("unexpected tree entry in diff at {path:?}");
2378 }
2379 MaterializedTreeValue::FileConflict(file) => {
2380 let conflict_marker_len =
2381 choose_materialized_conflict_marker_len(&file.contents);
2382 let options = ConflictMaterializeOptions {
2383 marker_style: self.conflict_marker_style,
2384 marker_len: Some(conflict_marker_len),
2385 merge: self.store.merge_options().clone(),
2386 };
2387 let exec_bit = ExecBit::new_from_repo(
2388 file.executable.unwrap_or(false),
2389 self.exec_policy,
2390 get_prev_exec,
2391 );
2392 let contents =
2393 materialize_merge_result_to_bytes(&file.contents, &file.labels, &options);
2394 let mut file_state =
2395 self.write_conflict(&disk_path, &contents, exec_bit).await?;
2396 file_state.materialized_conflict_data = Some(MaterializedConflictData {
2397 conflict_marker_len: conflict_marker_len.try_into().unwrap_or(u32::MAX),
2398 });
2399 file_state
2400 }
2401 MaterializedTreeValue::OtherConflict { id, labels } => {
2402 let contents = id.describe(&labels);
2405 self.write_conflict(&disk_path, contents.as_bytes(), ExecBit(false))
2407 .await?
2408 }
2409 };
2410 changed_file_states.push((path, file_state));
2411 Ok(())
2412 };
2413
2414 let mut diff_stream = old_tree
2415 .diff_stream_for_file_system(new_tree, matcher)
2416 .map(async |TreeDiffEntry { path, values }| match values {
2417 Ok(diff) => {
2418 let result =
2419 materialize_tree_value(&self.store, &path, diff.after, new_tree.labels())
2420 .await;
2421 (path, result.map(|value| (diff.before, value)))
2422 }
2423 Err(err) => (path, Err(err)),
2424 })
2425 .buffered(self.store.concurrency());
2426
2427 let mut conflicts_to_rematerialize: HashMap<RepoPathBuf, MergedTreeValue> =
2433 if old_tree.tree_ids().num_sides() == new_tree.tree_ids().num_sides()
2434 && old_tree.labels() != new_tree.labels()
2435 {
2436 new_tree
2440 .conflicts_matching(matcher)
2441 .map(|(path, value)| value.map(|value| (path, value)))
2442 .try_collect()?
2443 } else {
2444 HashMap::new()
2445 };
2446
2447 while let Some((path, data)) = diff_stream.next().await {
2448 let (before, after) = data?;
2449 conflicts_to_rematerialize.remove(&path);
2450 process_diff_entry(path, before, after).await?;
2451 }
2452
2453 if !conflicts_to_rematerialize.is_empty() {
2454 for (path, conflict) in conflicts_to_rematerialize {
2455 let materialized =
2456 materialize_tree_value(&self.store, &path, conflict.clone(), new_tree.labels())
2457 .await?;
2458 process_diff_entry(path, conflict, materialized).await?;
2459 }
2460
2461 changed_file_states.sort_unstable_by(|(path1, _), (path2, _)| path1.cmp(path2));
2464 }
2465
2466 self.file_states
2467 .merge_in(changed_file_states, &deleted_files);
2468 Ok(stats)
2469 }
2470
2471 pub async fn reset(&mut self, new_tree: &MergedTree) -> Result<(), ResetError> {
2472 let matcher = self.sparse_matcher();
2473 let mut changed_file_states = Vec::new();
2474 let mut deleted_files = HashSet::new();
2475 let mut diff_stream = self
2476 .tree
2477 .diff_stream_for_file_system(new_tree, matcher.as_ref());
2478 while let Some(TreeDiffEntry { path, values }) = diff_stream.next().await {
2479 let after = values?.after;
2480 if after.is_absent() {
2481 deleted_files.insert(path);
2482 } else {
2483 let file_type = match after.into_resolved() {
2484 Ok(value) => match value.unwrap() {
2485 TreeValue::File {
2486 id: _,
2487 executable,
2488 copy_id: _,
2489 } => {
2490 let get_prev_exec = || self.file_states().get_exec_bit(&path);
2491 let exec_bit =
2492 ExecBit::new_from_repo(executable, self.exec_policy, get_prev_exec);
2493 FileType::Normal { exec_bit }
2494 }
2495 TreeValue::Symlink(_id) => FileType::Symlink,
2496 TreeValue::GitSubmodule(_id) => {
2497 eprintln!("ignoring git submodule at {path:?}");
2498 FileType::GitSubmodule
2499 }
2500 TreeValue::Tree(_id) => {
2501 panic!("unexpected tree entry in diff at {path:?}");
2502 }
2503 },
2504 Err(_values) => {
2505 FileType::Normal {
2507 exec_bit: ExecBit(false),
2508 }
2509 }
2510 };
2511 let file_state = FileState {
2512 file_type,
2513 mtime: MillisSinceEpoch(0),
2514 size: 0,
2515 materialized_conflict_data: None,
2516 };
2517 changed_file_states.push((path, file_state));
2518 }
2519 }
2520 self.file_states
2521 .merge_in(changed_file_states, &deleted_files);
2522 self.tree = new_tree.clone();
2523 Ok(())
2524 }
2525
2526 pub async fn recover(&mut self, new_tree: &MergedTree) -> Result<(), ResetError> {
2527 self.file_states.clear();
2528 self.tree = self.store.empty_merged_tree();
2529 self.reset(new_tree).await
2530 }
2531}
2532
2533fn checkout_error_for_stat_error(err: io::Error, path: &Path) -> CheckoutError {
2534 CheckoutError::Other {
2535 message: format!("Failed to stat file {}", path.display()),
2536 err: err.into(),
2537 }
2538}
2539
2540fn checkout_error_for_mtime_out_of_range(err: MtimeOutOfRange, path: &Path) -> CheckoutError {
2541 CheckoutError::Other {
2542 message: format!("Failed to process file metadata {}", path.display()),
2543 err: err.into(),
2544 }
2545}
2546
2547#[derive(Clone, Debug)]
2549struct CheckoutState {
2550 operation_id: OperationId,
2551 workspace_name: WorkspaceNameBuf,
2552}
2553
2554impl CheckoutState {
2555 fn load(state_path: &Path) -> Result<Self, WorkingCopyStateError> {
2556 let wrap_err = |err| WorkingCopyStateError {
2557 message: "Failed to read checkout state".to_owned(),
2558 err,
2559 };
2560 let buf = fs::read(state_path.join("checkout")).map_err(|err| wrap_err(err.into()))?;
2561 let proto = crate::protos::local_working_copy::Checkout::decode(&*buf)
2562 .map_err(|err| wrap_err(err.into()))?;
2563 Ok(Self {
2564 operation_id: OperationId::new(proto.operation_id),
2565 workspace_name: if proto.workspace_name.is_empty() {
2566 WorkspaceName::DEFAULT.to_owned()
2569 } else {
2570 proto.workspace_name.into()
2571 },
2572 })
2573 }
2574
2575 #[instrument(skip_all)]
2576 fn save(&self, state_path: &Path) -> Result<(), WorkingCopyStateError> {
2577 let wrap_err = |err| WorkingCopyStateError {
2578 message: "Failed to write checkout state".to_owned(),
2579 err,
2580 };
2581 let proto = crate::protos::local_working_copy::Checkout {
2582 operation_id: self.operation_id.to_bytes(),
2583 workspace_name: (*self.workspace_name).into(),
2584 };
2585 let mut temp_file =
2586 NamedTempFile::new_in(state_path).map_err(|err| wrap_err(err.into()))?;
2587 temp_file
2588 .as_file_mut()
2589 .write_all(&proto.encode_to_vec())
2590 .map_err(|err| wrap_err(err.into()))?;
2591 persist_temp_file(temp_file, state_path.join("checkout"))
2594 .map_err(|err| wrap_err(err.into()))?;
2595 Ok(())
2596 }
2597}
2598
2599pub struct LocalWorkingCopy {
2600 store: Arc<Store>,
2601 working_copy_path: PathBuf,
2602 state_path: PathBuf,
2603 checkout_state: CheckoutState,
2604 tree_state: OnceCell<TreeState>,
2605 tree_state_settings: TreeStateSettings,
2606}
2607
2608#[async_trait(?Send)]
2609impl WorkingCopy for LocalWorkingCopy {
2610 fn name(&self) -> &str {
2611 Self::name()
2612 }
2613
2614 fn workspace_name(&self) -> &WorkspaceName {
2615 &self.checkout_state.workspace_name
2616 }
2617
2618 fn operation_id(&self) -> &OperationId {
2619 &self.checkout_state.operation_id
2620 }
2621
2622 fn tree(&self) -> Result<&MergedTree, WorkingCopyStateError> {
2623 Ok(self.tree_state()?.current_tree())
2624 }
2625
2626 fn sparse_patterns(&self) -> Result<&[RepoPathBuf], WorkingCopyStateError> {
2627 Ok(self.tree_state()?.sparse_patterns())
2628 }
2629
2630 async fn start_mutation(&self) -> Result<Box<dyn LockedWorkingCopy>, WorkingCopyStateError> {
2631 let lock_path = self.state_path.join("working_copy.lock");
2632 let lock = FileLock::lock(lock_path).map_err(|err| WorkingCopyStateError {
2633 message: "Failed to lock working copy".to_owned(),
2634 err: err.into(),
2635 })?;
2636
2637 let wc = Self {
2638 store: self.store.clone(),
2639 working_copy_path: self.working_copy_path.clone(),
2640 state_path: self.state_path.clone(),
2641 checkout_state: CheckoutState::load(&self.state_path)?,
2643 tree_state: OnceCell::new(),
2647 tree_state_settings: self.tree_state_settings.clone(),
2648 };
2649 let old_operation_id = wc.operation_id().clone();
2650 let old_tree = wc.tree()?.clone();
2651 Ok(Box::new(LockedLocalWorkingCopy {
2652 wc,
2653 old_operation_id,
2654 old_tree,
2655 tree_state_dirty: false,
2656 new_workspace_name: None,
2657 _lock: lock,
2658 }))
2659 }
2660}
2661
2662impl LocalWorkingCopy {
2663 pub fn name() -> &'static str {
2664 "local"
2665 }
2666
2667 pub fn init(
2671 store: Arc<Store>,
2672 working_copy_path: PathBuf,
2673 state_path: PathBuf,
2674 operation_id: OperationId,
2675 workspace_name: WorkspaceNameBuf,
2676 user_settings: &UserSettings,
2677 ) -> Result<Self, WorkingCopyStateError> {
2678 let checkout_state = CheckoutState {
2679 operation_id,
2680 workspace_name,
2681 };
2682 checkout_state.save(&state_path)?;
2683 let tree_state_settings = TreeStateSettings::try_from_user_settings(user_settings)
2684 .map_err(|err| WorkingCopyStateError {
2685 message: "Failed to read the tree state settings".to_string(),
2686 err: err.into(),
2687 })?;
2688 let tree_state = TreeState::init(
2689 store.clone(),
2690 working_copy_path.clone(),
2691 state_path.clone(),
2692 &tree_state_settings,
2693 )
2694 .map_err(|err| WorkingCopyStateError {
2695 message: "Failed to initialize working copy state".to_string(),
2696 err: err.into(),
2697 })?;
2698 Ok(Self {
2699 store,
2700 working_copy_path,
2701 state_path,
2702 checkout_state,
2703 tree_state: OnceCell::with_value(tree_state),
2704 tree_state_settings,
2705 })
2706 }
2707
2708 pub fn load(
2709 store: Arc<Store>,
2710 working_copy_path: PathBuf,
2711 state_path: PathBuf,
2712 user_settings: &UserSettings,
2713 ) -> Result<Self, WorkingCopyStateError> {
2714 let checkout_state = CheckoutState::load(&state_path)?;
2715 let tree_state_settings = TreeStateSettings::try_from_user_settings(user_settings)
2716 .map_err(|err| WorkingCopyStateError {
2717 message: "Failed to read the tree state settings".to_string(),
2718 err: err.into(),
2719 })?;
2720 Ok(Self {
2721 store,
2722 working_copy_path,
2723 state_path,
2724 checkout_state,
2725 tree_state: OnceCell::new(),
2726 tree_state_settings,
2727 })
2728 }
2729
2730 pub fn state_path(&self) -> &Path {
2731 &self.state_path
2732 }
2733
2734 #[instrument(skip_all)]
2735 fn tree_state(&self) -> Result<&TreeState, WorkingCopyStateError> {
2736 self.tree_state.get_or_try_init(|| {
2737 TreeState::load(
2738 self.store.clone(),
2739 self.working_copy_path.clone(),
2740 self.state_path.clone(),
2741 &self.tree_state_settings,
2742 )
2743 .map_err(|err| WorkingCopyStateError {
2744 message: "Failed to read working copy state".to_string(),
2745 err: err.into(),
2746 })
2747 })
2748 }
2749
2750 fn tree_state_mut(&mut self) -> Result<&mut TreeState, WorkingCopyStateError> {
2751 self.tree_state()?; Ok(self.tree_state.get_mut().unwrap())
2753 }
2754
2755 pub fn file_states(&self) -> Result<FileStates<'_>, WorkingCopyStateError> {
2756 Ok(self.tree_state()?.file_states())
2757 }
2758
2759 #[cfg(feature = "watchman")]
2760 pub async fn query_watchman(
2761 &self,
2762 config: &WatchmanConfig,
2763 ) -> Result<(watchman::Clock, Option<Vec<PathBuf>>), WorkingCopyStateError> {
2764 self.tree_state()?
2765 .query_watchman(config)
2766 .await
2767 .map_err(|err| WorkingCopyStateError {
2768 message: "Failed to query watchman".to_string(),
2769 err: err.into(),
2770 })
2771 }
2772
2773 #[cfg(feature = "watchman")]
2774 pub async fn is_watchman_trigger_registered(
2775 &self,
2776 config: &WatchmanConfig,
2777 ) -> Result<bool, WorkingCopyStateError> {
2778 self.tree_state()?
2779 .is_watchman_trigger_registered(config)
2780 .await
2781 .map_err(|err| WorkingCopyStateError {
2782 message: "Failed to query watchman".to_string(),
2783 err: err.into(),
2784 })
2785 }
2786}
2787
2788pub struct LocalWorkingCopyFactory {}
2789
2790impl WorkingCopyFactory for LocalWorkingCopyFactory {
2791 fn init_working_copy(
2792 &self,
2793 store: Arc<Store>,
2794 working_copy_path: PathBuf,
2795 state_path: PathBuf,
2796 operation_id: OperationId,
2797 workspace_name: WorkspaceNameBuf,
2798 settings: &UserSettings,
2799 ) -> Result<Box<dyn WorkingCopy>, WorkingCopyStateError> {
2800 Ok(Box::new(LocalWorkingCopy::init(
2801 store,
2802 working_copy_path,
2803 state_path,
2804 operation_id,
2805 workspace_name,
2806 settings,
2807 )?))
2808 }
2809
2810 fn load_working_copy(
2811 &self,
2812 store: Arc<Store>,
2813 working_copy_path: PathBuf,
2814 state_path: PathBuf,
2815 settings: &UserSettings,
2816 ) -> Result<Box<dyn WorkingCopy>, WorkingCopyStateError> {
2817 Ok(Box::new(LocalWorkingCopy::load(
2818 store,
2819 working_copy_path,
2820 state_path,
2821 settings,
2822 )?))
2823 }
2824}
2825
2826pub struct LockedLocalWorkingCopy {
2829 wc: LocalWorkingCopy,
2830 old_operation_id: OperationId,
2831 old_tree: MergedTree,
2832 tree_state_dirty: bool,
2833 new_workspace_name: Option<WorkspaceNameBuf>,
2834 _lock: FileLock,
2835}
2836
2837#[async_trait]
2838impl LockedWorkingCopy for LockedLocalWorkingCopy {
2839 fn old_operation_id(&self) -> &OperationId {
2840 &self.old_operation_id
2841 }
2842
2843 fn old_tree(&self) -> &MergedTree {
2844 &self.old_tree
2845 }
2846
2847 async fn snapshot(
2848 &mut self,
2849 options: &SnapshotOptions,
2850 ) -> Result<(MergedTree, SnapshotStats), SnapshotError> {
2851 let tree_state = self.wc.tree_state_mut()?;
2852 let (is_dirty, stats) = tree_state.snapshot(options).await?;
2853 self.tree_state_dirty |= is_dirty;
2854 Ok((tree_state.current_tree().clone(), stats))
2855 }
2856
2857 async fn check_out(&mut self, commit: &Commit) -> Result<CheckoutStats, CheckoutError> {
2858 let new_tree = commit.tree();
2861 let tree_state = self.wc.tree_state_mut()?;
2862 if tree_state.tree.tree_ids_and_labels() != new_tree.tree_ids_and_labels() {
2863 let stats = tree_state.check_out(&new_tree)?;
2864 self.tree_state_dirty = true;
2865 Ok(stats)
2866 } else {
2867 Ok(CheckoutStats::default())
2868 }
2869 }
2870
2871 fn rename_workspace(&mut self, new_name: WorkspaceNameBuf) {
2872 self.new_workspace_name = Some(new_name);
2873 }
2874
2875 async fn reset(&mut self, commit: &Commit) -> Result<(), ResetError> {
2876 let new_tree = commit.tree();
2877 self.wc.tree_state_mut()?.reset(&new_tree).await?;
2878 self.tree_state_dirty = true;
2879 Ok(())
2880 }
2881
2882 async fn recover(&mut self, commit: &Commit) -> Result<(), ResetError> {
2883 let new_tree = commit.tree();
2884 self.wc.tree_state_mut()?.recover(&new_tree).await?;
2885 self.tree_state_dirty = true;
2886 Ok(())
2887 }
2888
2889 fn sparse_patterns(&self) -> Result<&[RepoPathBuf], WorkingCopyStateError> {
2890 self.wc.sparse_patterns()
2891 }
2892
2893 async fn set_sparse_patterns(
2894 &mut self,
2895 new_sparse_patterns: Vec<RepoPathBuf>,
2896 ) -> Result<CheckoutStats, CheckoutError> {
2897 let stats = self
2900 .wc
2901 .tree_state_mut()?
2902 .set_sparse_patterns(new_sparse_patterns)?;
2903 self.tree_state_dirty = true;
2904 Ok(stats)
2905 }
2906
2907 #[instrument(skip_all)]
2908 async fn finish(
2909 mut self: Box<Self>,
2910 operation_id: OperationId,
2911 ) -> Result<Box<dyn WorkingCopy>, WorkingCopyStateError> {
2912 assert!(
2913 self.tree_state_dirty
2914 || self.old_tree.tree_ids_and_labels() == self.wc.tree()?.tree_ids_and_labels()
2915 );
2916 if self.tree_state_dirty {
2917 self.wc
2918 .tree_state_mut()?
2919 .save()
2920 .map_err(|err| WorkingCopyStateError {
2921 message: "Failed to write working copy state".to_string(),
2922 err: Box::new(err),
2923 })?;
2924 }
2925 if self.old_operation_id != operation_id || self.new_workspace_name.is_some() {
2926 self.wc.checkout_state.operation_id = operation_id;
2927 if let Some(workspace_name) = self.new_workspace_name {
2928 self.wc.checkout_state.workspace_name = workspace_name;
2929 }
2930 self.wc.checkout_state.save(&self.wc.state_path)?;
2931 }
2932 Ok(Box::new(self.wc))
2934 }
2935}
2936
2937impl LockedLocalWorkingCopy {
2938 pub fn reset_watchman(&mut self) -> Result<(), SnapshotError> {
2939 self.wc.tree_state_mut()?.reset_watchman();
2940 self.tree_state_dirty = true;
2941 Ok(())
2942 }
2943}
2944
2945#[cfg(test)]
2946mod tests {
2947 use std::time::Duration;
2948
2949 use maplit::hashset;
2950
2951 use super::*;
2952
2953 fn repo_path(value: &str) -> &RepoPath {
2954 RepoPath::from_internal_string(value).unwrap()
2955 }
2956
2957 fn repo_path_component(value: &str) -> &RepoPathComponent {
2958 RepoPathComponent::new(value).unwrap()
2959 }
2960
2961 fn new_state(size: u64) -> FileState {
2962 FileState {
2963 file_type: FileType::Normal {
2964 exec_bit: ExecBit(false),
2965 },
2966 mtime: MillisSinceEpoch(0),
2967 size,
2968 materialized_conflict_data: None,
2969 }
2970 }
2971
2972 #[test]
2973 fn test_file_states_merge() {
2974 let new_static_entry = |path: &'static str, size| (repo_path(path), new_state(size));
2975 let new_owned_entry = |path: &str, size| (repo_path(path).to_owned(), new_state(size));
2976 let new_proto_entry = |path: &str, size| {
2977 file_state_entry_to_proto(repo_path(path).to_owned(), &new_state(size))
2978 };
2979 let data = vec![
2980 new_proto_entry("aa", 0),
2981 new_proto_entry("b#", 4), new_proto_entry("b/c", 1),
2983 new_proto_entry("b/d/e", 2),
2984 new_proto_entry("b/e", 3),
2985 new_proto_entry("bc", 5),
2986 ];
2987 let mut file_states = FileStatesMap::from_proto(data, false);
2988
2989 let changed_file_states = vec![
2990 new_owned_entry("aa", 10), new_owned_entry("b/d/f", 11), new_owned_entry("b/e", 12), new_owned_entry("c", 13), ];
2995 let deleted_files = hashset! {
2996 repo_path("b/c").to_owned(),
2997 repo_path("b#").to_owned(),
2998 };
2999 file_states.merge_in(changed_file_states, &deleted_files);
3000 assert_eq!(
3001 file_states.all().iter().collect_vec(),
3002 vec![
3003 new_static_entry("aa", 10),
3004 new_static_entry("b/d/e", 2),
3005 new_static_entry("b/d/f", 11),
3006 new_static_entry("b/e", 12),
3007 new_static_entry("bc", 5),
3008 new_static_entry("c", 13),
3009 ],
3010 );
3011 }
3012
3013 #[test]
3014 fn test_file_states_lookup() {
3015 let new_proto_entry = |path: &str, size| {
3016 file_state_entry_to_proto(repo_path(path).to_owned(), &new_state(size))
3017 };
3018 let data = vec![
3019 new_proto_entry("aa", 0),
3020 new_proto_entry("b/c", 1),
3021 new_proto_entry("b/d/e", 2),
3022 new_proto_entry("b/e", 3),
3023 new_proto_entry("b#", 4), new_proto_entry("bc", 5),
3025 ];
3026 let file_states = FileStates::from_sorted(&data);
3027
3028 assert_eq!(
3029 file_states.prefixed(repo_path("")).paths().collect_vec(),
3030 ["aa", "b/c", "b/d/e", "b/e", "b#", "bc"].map(repo_path)
3031 );
3032 assert!(file_states.prefixed(repo_path("a")).is_empty());
3033 assert_eq!(
3034 file_states.prefixed(repo_path("aa")).paths().collect_vec(),
3035 ["aa"].map(repo_path)
3036 );
3037 assert_eq!(
3038 file_states.prefixed(repo_path("b")).paths().collect_vec(),
3039 ["b/c", "b/d/e", "b/e"].map(repo_path)
3040 );
3041 assert_eq!(
3042 file_states.prefixed(repo_path("b/d")).paths().collect_vec(),
3043 ["b/d/e"].map(repo_path)
3044 );
3045 assert_eq!(
3046 file_states.prefixed(repo_path("b#")).paths().collect_vec(),
3047 ["b#"].map(repo_path)
3048 );
3049 assert_eq!(
3050 file_states.prefixed(repo_path("bc")).paths().collect_vec(),
3051 ["bc"].map(repo_path)
3052 );
3053 assert!(file_states.prefixed(repo_path("z")).is_empty());
3054
3055 assert!(!file_states.contains_path(repo_path("a")));
3056 assert!(file_states.contains_path(repo_path("aa")));
3057 assert!(file_states.contains_path(repo_path("b/d/e")));
3058 assert!(!file_states.contains_path(repo_path("b/d")));
3059 assert!(file_states.contains_path(repo_path("b#")));
3060 assert!(file_states.contains_path(repo_path("bc")));
3061 assert!(!file_states.contains_path(repo_path("z")));
3062
3063 assert_eq!(file_states.get(repo_path("a")), None);
3064 assert_eq!(file_states.get(repo_path("aa")), Some(new_state(0)));
3065 assert_eq!(file_states.get(repo_path("b/d/e")), Some(new_state(2)));
3066 assert_eq!(file_states.get(repo_path("bc")), Some(new_state(5)));
3067 assert_eq!(file_states.get(repo_path("z")), None);
3068 }
3069
3070 #[test]
3071 fn test_file_states_lookup_at() {
3072 let new_proto_entry = |path: &str, size| {
3073 file_state_entry_to_proto(repo_path(path).to_owned(), &new_state(size))
3074 };
3075 let data = vec![
3076 new_proto_entry("b/c", 0),
3077 new_proto_entry("b/d/e", 1),
3078 new_proto_entry("b/d#", 2), new_proto_entry("b/e", 3),
3080 new_proto_entry("b#", 4), ];
3082 let file_states = FileStates::from_sorted(&data);
3083
3084 assert_eq!(
3086 file_states.get_at(RepoPath::root(), repo_path_component("b")),
3087 None
3088 );
3089 assert_eq!(
3090 file_states.get_at(RepoPath::root(), repo_path_component("b#")),
3091 Some(new_state(4))
3092 );
3093
3094 let prefixed_states = file_states.prefixed_at(RepoPath::root(), repo_path_component("b"));
3096 assert_eq!(
3097 prefixed_states.paths().collect_vec(),
3098 ["b/c", "b/d/e", "b/d#", "b/e"].map(repo_path)
3099 );
3100 assert_eq!(
3101 prefixed_states.get_at(repo_path("b"), repo_path_component("c")),
3102 Some(new_state(0))
3103 );
3104 assert_eq!(
3105 prefixed_states.get_at(repo_path("b"), repo_path_component("d")),
3106 None
3107 );
3108 assert_eq!(
3109 prefixed_states.get_at(repo_path("b"), repo_path_component("d#")),
3110 Some(new_state(2))
3111 );
3112
3113 let prefixed_states = prefixed_states.prefixed_at(repo_path("b"), repo_path_component("d"));
3115 assert_eq!(
3116 prefixed_states.paths().collect_vec(),
3117 ["b/d/e"].map(repo_path)
3118 );
3119 assert_eq!(
3120 prefixed_states.get_at(repo_path("b/d"), repo_path_component("e")),
3121 Some(new_state(1))
3122 );
3123 assert_eq!(
3124 prefixed_states.get_at(repo_path("b/d"), repo_path_component("#")),
3125 None
3126 );
3127
3128 let prefixed_states = file_states.prefixed_at(RepoPath::root(), repo_path_component("b#"));
3130 assert_eq!(prefixed_states.paths().collect_vec(), ["b#"].map(repo_path));
3131 assert_eq!(
3132 prefixed_states.get_at(repo_path("b#"), repo_path_component("#")),
3133 None
3134 );
3135 }
3136
3137 #[test]
3138 fn test_system_time_to_millis() {
3139 let epoch = SystemTime::UNIX_EPOCH;
3140 assert_eq!(system_time_to_millis(epoch), Some(MillisSinceEpoch(0)));
3141 if let Some(time) = epoch.checked_add(Duration::from_millis(1)) {
3142 assert_eq!(system_time_to_millis(time), Some(MillisSinceEpoch(1)));
3143 }
3144 if let Some(time) = epoch.checked_sub(Duration::from_millis(1)) {
3145 assert_eq!(system_time_to_millis(time), Some(MillisSinceEpoch(-1)));
3146 }
3147 if let Some(time) = epoch.checked_add(Duration::from_millis(i64::MAX as u64)) {
3148 assert_eq!(
3149 system_time_to_millis(time),
3150 Some(MillisSinceEpoch(i64::MAX))
3151 );
3152 }
3153 if let Some(time) = epoch.checked_sub(Duration::from_millis(i64::MAX as u64)) {
3154 assert_eq!(
3155 system_time_to_millis(time),
3156 Some(MillisSinceEpoch(-i64::MAX))
3157 );
3158 }
3159 if let Some(time) = epoch.checked_sub(Duration::from_millis(i64::MAX as u64 + 1)) {
3160 assert_eq!(system_time_to_millis(time), None);
3162 }
3163 }
3164}