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::StreamExt as _;
47use itertools::EitherOrBoth;
48use itertools::Itertools as _;
49use once_cell::unsync::OnceCell;
50use pollster::FutureExt as _;
51use prost::Message as _;
52use rayon::iter::IntoParallelIterator as _;
53use rayon::prelude::IndexedParallelIterator as _;
54use rayon::prelude::ParallelIterator as _;
55use tempfile::NamedTempFile;
56use thiserror::Error;
57use tokio::io::AsyncRead;
58use tokio::io::AsyncReadExt as _;
59use tracing::instrument;
60use tracing::trace_span;
61
62use crate::backend::BackendError;
63use crate::backend::CopyId;
64use crate::backend::FileId;
65use crate::backend::MillisSinceEpoch;
66use crate::backend::SymlinkId;
67use crate::backend::TreeId;
68use crate::backend::TreeValue;
69use crate::commit::Commit;
70use crate::config::ConfigGetError;
71use crate::conflict_labels::ConflictLabels;
72use crate::conflicts;
73use crate::conflicts::ConflictMarkerStyle;
74use crate::conflicts::ConflictMaterializeOptions;
75use crate::conflicts::MIN_CONFLICT_MARKER_LEN;
76use crate::conflicts::MaterializedTreeValue;
77use crate::conflicts::choose_materialized_conflict_marker_len;
78use crate::conflicts::materialize_merge_result_to_bytes;
79use crate::conflicts::materialize_tree_value;
80pub use crate::eol::EolConversionMode;
81use crate::eol::TargetEolStrategy;
82use crate::file_util::BlockingAsyncReader;
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 fn empty(
1062 store: Arc<Store>,
1063 working_copy_path: PathBuf,
1064 state_path: PathBuf,
1065 TreeStateSettings {
1066 conflict_marker_style,
1067 eol_conversion_mode,
1068 exec_change_setting,
1069 fsmonitor_settings,
1070 }: &TreeStateSettings,
1071 ) -> Self {
1072 let exec_policy = ExecChangePolicy::new(*exec_change_setting, &state_path);
1073 Self {
1074 store: store.clone(),
1075 working_copy_path,
1076 state_path,
1077 tree: store.empty_merged_tree(),
1078 file_states: FileStatesMap::new(),
1079 sparse_patterns: vec![RepoPathBuf::root()],
1080 own_mtime: MillisSinceEpoch(0),
1081 symlink_support: check_symlink_support().unwrap_or(false),
1082 watchman_clock: None,
1083 conflict_marker_style: *conflict_marker_style,
1084 exec_policy,
1085 fsmonitor_settings: fsmonitor_settings.clone(),
1086 target_eol_strategy: TargetEolStrategy::new(*eol_conversion_mode),
1087 }
1088 }
1089
1090 pub fn load(
1091 store: Arc<Store>,
1092 working_copy_path: PathBuf,
1093 state_path: PathBuf,
1094 tree_state_settings: &TreeStateSettings,
1095 ) -> Result<Self, TreeStateError> {
1096 let tree_state_path = state_path.join("tree_state");
1097 let file = match File::open(&tree_state_path) {
1098 Err(err) if err.kind() == io::ErrorKind::NotFound => {
1099 return Self::init(store, working_copy_path, state_path, tree_state_settings);
1100 }
1101 Err(err) => {
1102 return Err(TreeStateError::ReadTreeState {
1103 path: tree_state_path,
1104 source: err,
1105 });
1106 }
1107 Ok(file) => file,
1108 };
1109
1110 let mut wc = Self::empty(store, working_copy_path, state_path, tree_state_settings);
1111 wc.read(&tree_state_path, file)?;
1112 Ok(wc)
1113 }
1114
1115 fn update_own_mtime(&mut self) {
1116 if let Ok(metadata) = self.state_path.join("tree_state").symlink_metadata()
1117 && let Ok(mtime) = mtime_from_metadata(&metadata)
1118 {
1119 self.own_mtime = mtime;
1120 } else {
1121 self.own_mtime = MillisSinceEpoch(0);
1122 }
1123 }
1124
1125 fn read(&mut self, tree_state_path: &Path, mut file: File) -> Result<(), TreeStateError> {
1126 self.update_own_mtime();
1127 let mut buf = Vec::new();
1128 file.read_to_end(&mut buf)
1129 .map_err(|err| TreeStateError::ReadTreeState {
1130 path: tree_state_path.to_owned(),
1131 source: err,
1132 })?;
1133 let proto = crate::protos::local_working_copy::TreeState::decode(&*buf).map_err(|err| {
1134 TreeStateError::DecodeTreeState {
1135 path: tree_state_path.to_owned(),
1136 source: err,
1137 }
1138 })?;
1139 #[expect(deprecated)]
1140 if proto.tree_ids.is_empty() {
1141 self.tree = MergedTree::resolved(
1142 self.store.clone(),
1143 TreeId::new(proto.legacy_tree_id.clone()),
1144 );
1145 } else {
1146 let tree_ids_builder: MergeBuilder<TreeId> = proto
1147 .tree_ids
1148 .iter()
1149 .map(|id| TreeId::new(id.clone()))
1150 .collect();
1151 self.tree = MergedTree::new(
1152 self.store.clone(),
1153 tree_ids_builder.build(),
1154 ConflictLabels::from_vec(proto.conflict_labels),
1155 );
1156 }
1157 self.file_states =
1158 FileStatesMap::from_proto(proto.file_states, proto.is_file_states_sorted);
1159 self.sparse_patterns = sparse_patterns_from_proto(proto.sparse_patterns.as_ref());
1160 self.watchman_clock = proto.watchman_clock;
1161 Ok(())
1162 }
1163
1164 #[expect(clippy::assigning_clones, clippy::field_reassign_with_default)]
1165 pub fn save(&mut self) -> Result<(), TreeStateError> {
1166 let mut proto: crate::protos::local_working_copy::TreeState = Default::default();
1167 proto.tree_ids = self
1168 .tree
1169 .tree_ids()
1170 .iter()
1171 .map(|id| id.to_bytes())
1172 .collect();
1173 proto.conflict_labels = self.tree.labels().as_slice().to_owned();
1174 proto.file_states = self.file_states.data.clone();
1175 proto.is_file_states_sorted = true;
1177 let mut sparse_patterns = crate::protos::local_working_copy::SparsePatterns::default();
1178 for path in &self.sparse_patterns {
1179 sparse_patterns
1180 .prefixes
1181 .push(path.as_internal_file_string().to_owned());
1182 }
1183 proto.sparse_patterns = Some(sparse_patterns);
1184 proto.watchman_clock = self.watchman_clock.clone();
1185
1186 let wrap_write_err = |source| TreeStateError::WriteTreeState {
1187 path: self.state_path.clone(),
1188 source,
1189 };
1190 let mut temp_file = NamedTempFile::new_in(&self.state_path).map_err(wrap_write_err)?;
1191 temp_file
1192 .as_file_mut()
1193 .write_all(&proto.encode_to_vec())
1194 .map_err(wrap_write_err)?;
1195 self.update_own_mtime();
1198 let target_path = self.state_path.join("tree_state");
1201 persist_temp_file(temp_file, &target_path).map_err(|source| {
1202 TreeStateError::PersistTreeState {
1203 path: target_path.clone(),
1204 source,
1205 }
1206 })?;
1207 Ok(())
1208 }
1209
1210 fn reset_watchman(&mut self) {
1211 self.watchman_clock.take();
1212 }
1213
1214 #[cfg(feature = "watchman")]
1215 #[instrument(skip(self))]
1216 pub async fn query_watchman(
1217 &self,
1218 config: &WatchmanConfig,
1219 ) -> Result<(watchman::Clock, Option<Vec<PathBuf>>), TreeStateError> {
1220 let previous_clock = self.watchman_clock.clone().map(watchman::Clock::from);
1221
1222 let tokio_fn = async || {
1223 let fsmonitor = watchman::Fsmonitor::init(&self.working_copy_path, config)
1224 .await
1225 .map_err(|err| TreeStateError::Fsmonitor(Box::new(err)))?;
1226 fsmonitor
1227 .query_changed_files(previous_clock)
1228 .await
1229 .map_err(|err| TreeStateError::Fsmonitor(Box::new(err)))
1230 };
1231
1232 match tokio::runtime::Handle::try_current() {
1233 Ok(_handle) => tokio_fn().await,
1234 Err(_) => {
1235 let runtime = tokio::runtime::Builder::new_current_thread()
1236 .enable_all()
1237 .build()
1238 .map_err(|err| TreeStateError::Fsmonitor(Box::new(err)))?;
1239 runtime.block_on(tokio_fn())
1240 }
1241 }
1242 }
1243
1244 #[cfg(feature = "watchman")]
1245 #[instrument(skip(self))]
1246 pub async fn is_watchman_trigger_registered(
1247 &self,
1248 config: &WatchmanConfig,
1249 ) -> Result<bool, TreeStateError> {
1250 let tokio_fn = async || {
1251 let fsmonitor = watchman::Fsmonitor::init(&self.working_copy_path, config)
1252 .await
1253 .map_err(|err| TreeStateError::Fsmonitor(Box::new(err)))?;
1254 fsmonitor
1255 .is_trigger_registered()
1256 .await
1257 .map_err(|err| TreeStateError::Fsmonitor(Box::new(err)))
1258 };
1259
1260 match tokio::runtime::Handle::try_current() {
1261 Ok(_handle) => tokio_fn().await,
1262 Err(_) => {
1263 let runtime = tokio::runtime::Builder::new_current_thread()
1264 .enable_all()
1265 .build()
1266 .map_err(|err| TreeStateError::Fsmonitor(Box::new(err)))?;
1267 runtime.block_on(tokio_fn())
1268 }
1269 }
1270 }
1271}
1272
1273impl TreeState {
1275 #[instrument(skip_all)]
1278 pub async fn snapshot(
1279 &mut self,
1280 options: &SnapshotOptions<'_>,
1281 ) -> Result<(bool, SnapshotStats), SnapshotError> {
1282 let SnapshotOptions {
1283 base_ignores,
1284 progress,
1285 start_tracking_matcher,
1286 force_tracking_matcher,
1287 max_new_file_size,
1288 } = options;
1289
1290 let sparse_matcher = self.sparse_matcher();
1291
1292 let fsmonitor_clock_needs_save = self.fsmonitor_settings != FsmonitorSettings::None;
1293 let mut is_dirty = fsmonitor_clock_needs_save;
1294 let FsmonitorMatcher {
1295 matcher: fsmonitor_matcher,
1296 watchman_clock,
1297 } = self
1298 .make_fsmonitor_matcher(&self.fsmonitor_settings)
1299 .await?;
1300 let fsmonitor_matcher = match fsmonitor_matcher.as_ref() {
1301 None => &EverythingMatcher,
1302 Some(fsmonitor_matcher) => fsmonitor_matcher.as_ref(),
1303 };
1304
1305 let matcher = IntersectionMatcher::new(
1306 sparse_matcher.as_ref(),
1307 UnionMatcher::new(fsmonitor_matcher, force_tracking_matcher),
1308 );
1309 if matcher.visit(RepoPath::root()).is_nothing() {
1310 self.watchman_clock = watchman_clock;
1312 return Ok((is_dirty, SnapshotStats::default()));
1313 }
1314
1315 let (tree_entries_tx, tree_entries_rx) = channel();
1316 let (file_states_tx, file_states_rx) = channel();
1317 let (untracked_paths_tx, untracked_paths_rx) = channel();
1318 let (deleted_files_tx, deleted_files_rx) = channel();
1319
1320 trace_span!("traverse filesystem").in_scope(|| -> Result<(), SnapshotError> {
1321 let snapshotter = FileSnapshotter {
1322 tree_state: self,
1323 current_tree: &self.tree,
1324 matcher: &matcher,
1325 start_tracking_matcher,
1326 force_tracking_matcher,
1327 tree_entries_tx,
1329 file_states_tx,
1330 untracked_paths_tx,
1331 deleted_files_tx,
1332 error: OnceLock::new(),
1333 progress: *progress,
1334 max_new_file_size: *max_new_file_size,
1335 };
1336 let directory_to_visit = DirectoryToVisit {
1337 dir: RepoPathBuf::root(),
1338 disk_dir: self.working_copy_path.clone(),
1339 git_ignore: base_ignores.clone(),
1340 file_states: self.file_states.all(),
1341 };
1342 rayon::scope(|scope| {
1344 snapshotter.spawn_ok(scope, |scope| {
1345 snapshotter.visit_directory(directory_to_visit, scope)
1346 });
1347 });
1348 snapshotter.into_result()
1349 })?;
1350
1351 let stats = SnapshotStats {
1352 untracked_paths: untracked_paths_rx.into_iter().collect(),
1353 };
1354 let mut tree_builder = MergedTreeBuilder::new(self.tree.clone());
1355 trace_span!("process tree entries").in_scope(|| {
1356 for (path, tree_values) in &tree_entries_rx {
1357 tree_builder.set_or_remove(path, tree_values);
1358 }
1359 });
1360 let deleted_files = trace_span!("process deleted tree entries").in_scope(|| {
1361 let deleted_files = HashSet::from_iter(deleted_files_rx);
1362 is_dirty |= !deleted_files.is_empty();
1363 for file in &deleted_files {
1364 tree_builder.set_or_remove(file.clone(), Merge::absent());
1365 }
1366 deleted_files
1367 });
1368 trace_span!("process file states").in_scope(|| {
1369 let changed_file_states = file_states_rx
1370 .iter()
1371 .sorted_unstable_by(|(path1, _), (path2, _)| path1.cmp(path2))
1372 .collect_vec();
1373 is_dirty |= !changed_file_states.is_empty();
1374 self.file_states
1375 .merge_in(changed_file_states, &deleted_files);
1376 });
1377 trace_span!("write tree")
1378 .in_scope(async || -> Result<(), BackendError> {
1379 let new_tree = tree_builder.write_tree().await?;
1380 is_dirty |= new_tree.tree_ids_and_labels() != self.tree.tree_ids_and_labels();
1381 self.tree = new_tree.clone();
1382 Ok(())
1383 })
1384 .await?;
1385 if cfg!(debug_assertions) {
1386 let tree_paths: HashSet<_> = self
1387 .tree
1388 .entries_matching(sparse_matcher.as_ref())
1389 .filter_map(|(path, result)| result.is_ok().then_some(path))
1390 .collect();
1391 let file_states = self.file_states.all();
1392 let state_paths: HashSet<_> = file_states.paths().map(|path| path.to_owned()).collect();
1393 assert_eq!(state_paths, tree_paths);
1394 }
1395 if stats.untracked_paths.is_empty() || watchman_clock.is_none() {
1399 self.watchman_clock = watchman_clock;
1400 } else {
1401 tracing::info!("not updating watchman clock because there are untracked files");
1402 }
1403 Ok((is_dirty, stats))
1404 }
1405
1406 #[instrument(skip_all)]
1407 async fn make_fsmonitor_matcher(
1408 &self,
1409 fsmonitor_settings: &FsmonitorSettings,
1410 ) -> Result<FsmonitorMatcher, SnapshotError> {
1411 let (watchman_clock, changed_files) = match fsmonitor_settings {
1412 FsmonitorSettings::None => (None, None),
1413 FsmonitorSettings::Test { changed_files } => (None, Some(changed_files.clone())),
1414 #[cfg(feature = "watchman")]
1415 FsmonitorSettings::Watchman(config) => match self.query_watchman(config).await {
1416 Ok((watchman_clock, changed_files)) => (Some(watchman_clock.into()), changed_files),
1417 Err(err) => {
1418 tracing::warn!(?err, "Failed to query filesystem monitor");
1419 (None, None)
1420 }
1421 },
1422 #[cfg(not(feature = "watchman"))]
1423 FsmonitorSettings::Watchman(_) => {
1424 return Err(SnapshotError::Other {
1425 message: "Failed to query the filesystem monitor".to_string(),
1426 err: "Cannot query Watchman because jj was not compiled with the `watchman` \
1427 feature (consider disabling `fsmonitor.backend`)"
1428 .into(),
1429 });
1430 }
1431 };
1432 let matcher: Option<Box<dyn Matcher>> = match changed_files {
1433 None => None,
1434 Some(changed_files) => {
1435 let (repo_paths, gitignore_prefixes) = trace_span!("processing fsmonitor paths")
1436 .in_scope(|| {
1437 let repo_paths = changed_files
1438 .iter()
1439 .filter_map(|path| RepoPathBuf::from_relative_path(path).ok())
1440 .collect_vec();
1441 let gitignore_prefixes = repo_paths
1444 .iter()
1445 .filter_map(|repo_path| {
1446 let (parent, basename) = repo_path.split()?;
1447 (basename.as_internal_str() == ".gitignore")
1448 .then(|| parent.to_owned())
1449 })
1450 .collect_vec();
1451 (repo_paths, gitignore_prefixes)
1452 });
1453
1454 let matcher: Box<dyn Matcher> = if gitignore_prefixes.is_empty() {
1455 Box::new(FilesMatcher::new(repo_paths))
1456 } else {
1457 Box::new(UnionMatcher::new(
1458 FilesMatcher::new(repo_paths),
1459 PrefixMatcher::new(gitignore_prefixes),
1460 ))
1461 };
1462
1463 Some(matcher)
1464 }
1465 };
1466 Ok(FsmonitorMatcher {
1467 matcher,
1468 watchman_clock,
1469 })
1470 }
1471}
1472
1473struct DirectoryToVisit<'a> {
1474 dir: RepoPathBuf,
1475 disk_dir: PathBuf,
1476 git_ignore: Arc<GitIgnoreFile>,
1477 file_states: FileStates<'a>,
1478}
1479
1480#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1481enum PresentDirEntryKind {
1482 Dir,
1483 File,
1484}
1485
1486#[derive(Clone, Debug)]
1487struct PresentDirEntries {
1488 dirs: HashSet<String>,
1489 files: HashSet<String>,
1490}
1491
1492struct FileSnapshotter<'a> {
1494 tree_state: &'a TreeState,
1495 current_tree: &'a MergedTree,
1496 matcher: &'a dyn Matcher,
1497 start_tracking_matcher: &'a dyn Matcher,
1498 force_tracking_matcher: &'a dyn Matcher,
1499 tree_entries_tx: Sender<(RepoPathBuf, MergedTreeValue)>,
1500 file_states_tx: Sender<(RepoPathBuf, FileState)>,
1501 untracked_paths_tx: Sender<(RepoPathBuf, UntrackedReason)>,
1502 deleted_files_tx: Sender<RepoPathBuf>,
1503 error: OnceLock<SnapshotError>,
1504 progress: Option<&'a SnapshotProgress<'a>>,
1505 max_new_file_size: u64,
1506}
1507
1508impl FileSnapshotter<'_> {
1509 fn spawn_ok<'scope, F>(&'scope self, scope: &rayon::Scope<'scope>, body: F)
1510 where
1511 F: FnOnce(&rayon::Scope<'scope>) -> Result<(), SnapshotError> + Send + 'scope,
1512 {
1513 scope.spawn(|scope| {
1514 if self.error.get().is_some() {
1515 return;
1516 }
1517 match body(scope) {
1518 Ok(()) => {}
1519 Err(err) => self.error.set(err).unwrap_or(()),
1520 }
1521 });
1522 }
1523
1524 fn into_result(self) -> Result<(), SnapshotError> {
1526 match self.error.into_inner() {
1527 Some(err) => Err(err),
1528 None => Ok(()),
1529 }
1530 }
1531
1532 fn visit_directory<'scope>(
1535 &'scope self,
1536 directory_to_visit: DirectoryToVisit<'scope>,
1537 scope: &rayon::Scope<'scope>,
1538 ) -> Result<(), SnapshotError> {
1539 let DirectoryToVisit {
1540 dir,
1541 disk_dir,
1542 git_ignore,
1543 file_states,
1544 } = directory_to_visit;
1545
1546 let git_ignore = git_ignore.chain_with_file(&dir, disk_dir.join(".gitignore"))?;
1547 let dir_entries: Vec<_> = disk_dir
1548 .read_dir()
1549 .and_then(|entries| entries.try_collect())
1550 .map_err(|err| SnapshotError::Other {
1551 message: format!("Failed to read directory {}", disk_dir.display()),
1552 err: err.into(),
1553 })?;
1554 let (dirs, files) = dir_entries
1555 .into_par_iter()
1556 .with_min_len(100)
1559 .filter_map(|entry| {
1560 self.process_dir_entry(&dir, &git_ignore, file_states, &entry, scope)
1561 .block_on()
1562 .transpose()
1563 })
1564 .map(|item| match item {
1565 Ok((PresentDirEntryKind::Dir, name)) => Ok(Either::Left(name)),
1566 Ok((PresentDirEntryKind::File, name)) => Ok(Either::Right(name)),
1567 Err(err) => Err(err),
1568 })
1569 .collect::<Result<_, _>>()?;
1570 let present_entries = PresentDirEntries { dirs, files };
1571 self.emit_deleted_files(&dir, file_states, &present_entries);
1572 Ok(())
1573 }
1574
1575 async fn process_dir_entry<'scope>(
1576 &'scope self,
1577 dir: &RepoPath,
1578 git_ignore: &Arc<GitIgnoreFile>,
1579 file_states: FileStates<'scope>,
1580 entry: &DirEntry,
1581 scope: &rayon::Scope<'scope>,
1582 ) -> Result<Option<(PresentDirEntryKind, String)>, SnapshotError> {
1583 let file_type = entry.file_type().unwrap();
1584 let file_name = entry.file_name();
1585 let name_string = file_name
1586 .into_string()
1587 .map_err(|path| SnapshotError::InvalidUtf8Path { path })?;
1588
1589 if RESERVED_DIR_NAMES.contains(&name_string.as_str()) {
1590 return Ok(None);
1591 }
1592 let name = RepoPathComponent::new(&name_string).unwrap();
1593 let path = dir.join(name);
1594 let maybe_current_file_state = file_states.get_at(dir, name);
1595 if let Some(file_state) = &maybe_current_file_state
1596 && file_state.file_type == FileType::GitSubmodule
1597 {
1598 return Ok(None);
1599 }
1600
1601 if file_type.is_dir() {
1602 let file_states = file_states.prefixed_at(dir, name);
1603 let disk_dir = entry.path();
1611 for &name in RESERVED_DIR_NAMES {
1612 if disk_dir.join(name).symlink_metadata().is_ok() {
1613 return Ok(None);
1614 }
1615 }
1616
1617 if git_ignore.matches_dir(&path)
1618 && self.force_tracking_matcher.visit(&path).is_nothing()
1619 {
1620 self.spawn_ok(scope, move |_| {
1626 self.visit_tracked_files(file_states).block_on()
1627 });
1628 } else if !self.matcher.visit(&path).is_nothing() {
1629 let directory_to_visit = DirectoryToVisit {
1630 dir: path,
1631 disk_dir,
1632 git_ignore: git_ignore.clone(),
1633 file_states,
1634 };
1635 self.spawn_ok(scope, |scope| {
1636 self.visit_directory(directory_to_visit, scope)
1637 });
1638 }
1639 Ok(Some((PresentDirEntryKind::Dir, name_string)))
1642 } else if self.matcher.matches(&path) {
1643 if let Some(progress) = self.progress {
1644 progress(&path);
1645 }
1646 if maybe_current_file_state.is_none()
1647 && (git_ignore.matches_file(&path) && !self.force_tracking_matcher.matches(&path))
1648 {
1649 Ok(None)
1652 } else if maybe_current_file_state.is_none()
1653 && !self.start_tracking_matcher.matches(&path)
1654 {
1655 self.untracked_paths_tx
1657 .send((path, UntrackedReason::FileNotAutoTracked))
1658 .ok();
1659 Ok(None)
1660 } else {
1661 let metadata = entry.metadata().map_err(|err| SnapshotError::Other {
1662 message: format!("Failed to stat file {}", entry.path().display()),
1663 err: err.into(),
1664 })?;
1665 if maybe_current_file_state.is_none()
1666 && (metadata.len() > self.max_new_file_size
1667 && !self.force_tracking_matcher.matches(&path))
1668 {
1669 let reason = UntrackedReason::FileTooLarge {
1671 size: metadata.len(),
1672 max_size: self.max_new_file_size,
1673 };
1674 self.untracked_paths_tx.send((path, reason)).ok();
1675 Ok(None)
1676 } else if let Some(new_file_state) = file_state(&metadata)
1677 .map_err(|err| snapshot_error_for_mtime_out_of_range(err, &entry.path()))?
1678 {
1679 self.process_present_file(
1680 path,
1681 &entry.path(),
1682 maybe_current_file_state.as_ref(),
1683 new_file_state,
1684 )
1685 .await?;
1686 Ok(Some((PresentDirEntryKind::File, name_string)))
1687 } else {
1688 Ok(None)
1690 }
1691 }
1692 } else {
1693 Ok(None)
1694 }
1695 }
1696
1697 async fn visit_tracked_files(&self, file_states: FileStates<'_>) -> Result<(), SnapshotError> {
1699 for (tracked_path, current_file_state) in file_states {
1700 if current_file_state.file_type == FileType::GitSubmodule {
1701 continue;
1702 }
1703 if !self.matcher.matches(tracked_path) {
1704 continue;
1705 }
1706 let disk_path = tracked_path.to_fs_path(&self.tree_state.working_copy_path)?;
1707 let metadata = match disk_path.symlink_metadata() {
1708 Ok(metadata) => Some(metadata),
1709 Err(err) if err.kind() == io::ErrorKind::NotFound => None,
1710 Err(err) => {
1711 return Err(SnapshotError::Other {
1712 message: format!("Failed to stat file {}", disk_path.display()),
1713 err: err.into(),
1714 });
1715 }
1716 };
1717 if let Some(metadata) = &metadata
1718 && let Some(new_file_state) = file_state(metadata)
1719 .map_err(|err| snapshot_error_for_mtime_out_of_range(err, &disk_path))?
1720 {
1721 self.process_present_file(
1722 tracked_path.to_owned(),
1723 &disk_path,
1724 Some(¤t_file_state),
1725 new_file_state,
1726 )
1727 .await?;
1728 } else {
1729 self.deleted_files_tx.send(tracked_path.to_owned()).ok();
1730 }
1731 }
1732 Ok(())
1733 }
1734
1735 async fn process_present_file(
1736 &self,
1737 path: RepoPathBuf,
1738 disk_path: &Path,
1739 maybe_current_file_state: Option<&FileState>,
1740 mut new_file_state: FileState,
1741 ) -> Result<(), SnapshotError> {
1742 let update = self
1743 .get_updated_tree_value(&path, disk_path, maybe_current_file_state, &new_file_state)
1744 .await?;
1745 if matches!(new_file_state.file_type, FileType::Normal { .. })
1747 && !update.as_ref().is_some_and(|update| update.is_resolved())
1748 {
1749 new_file_state.materialized_conflict_data =
1750 maybe_current_file_state.and_then(|state| state.materialized_conflict_data);
1751 }
1752 if let Some(tree_value) = update {
1753 self.tree_entries_tx.send((path.clone(), tree_value)).ok();
1754 }
1755 if Some(&new_file_state) != maybe_current_file_state {
1756 self.file_states_tx.send((path, new_file_state)).ok();
1757 }
1758 Ok(())
1759 }
1760
1761 fn emit_deleted_files(
1763 &self,
1764 dir: &RepoPath,
1765 file_states: FileStates<'_>,
1766 present_entries: &PresentDirEntries,
1767 ) {
1768 let file_state_chunks = file_states.iter().chunk_by(|(path, _state)| {
1769 debug_assert!(path.starts_with(dir));
1772 let slash = usize::from(!dir.is_root());
1773 let len = dir.as_internal_file_string().len() + slash;
1774 let tail = path.as_internal_file_string().get(len..).unwrap_or("");
1775 match tail.split_once('/') {
1776 Some((name, _)) => (PresentDirEntryKind::Dir, name),
1777 None => (PresentDirEntryKind::File, tail),
1778 }
1779 });
1780 file_state_chunks
1781 .into_iter()
1782 .filter(|&((kind, name), _)| match kind {
1783 PresentDirEntryKind::Dir => !present_entries.dirs.contains(name),
1784 PresentDirEntryKind::File => !present_entries.files.contains(name),
1785 })
1786 .flat_map(|(_, chunk)| chunk)
1787 .filter(|(_, state)| state.file_type != FileType::GitSubmodule)
1789 .filter(|(path, _)| self.matcher.matches(path))
1790 .try_for_each(|(path, _)| self.deleted_files_tx.send(path.to_owned()))
1791 .ok();
1792 }
1793
1794 async fn get_updated_tree_value(
1795 &self,
1796 repo_path: &RepoPath,
1797 disk_path: &Path,
1798 maybe_current_file_state: Option<&FileState>,
1799 new_file_state: &FileState,
1800 ) -> Result<Option<MergedTreeValue>, SnapshotError> {
1801 let clean = match maybe_current_file_state {
1802 None => {
1803 false
1805 }
1806 Some(current_file_state) => {
1807 new_file_state.is_clean(current_file_state)
1810 && current_file_state.mtime < self.tree_state.own_mtime
1811 }
1812 };
1813 if clean {
1814 Ok(None)
1815 } else {
1816 let current_tree_values = self.current_tree.path_value(repo_path).await?;
1817 let new_file_type = if !self.tree_state.symlink_support {
1818 let mut new_file_type = new_file_state.file_type.clone();
1819 if matches!(new_file_type, FileType::Normal { .. })
1820 && matches!(current_tree_values.as_normal(), Some(TreeValue::Symlink(_)))
1821 {
1822 new_file_type = FileType::Symlink;
1823 }
1824 new_file_type
1825 } else {
1826 new_file_state.file_type.clone()
1827 };
1828 let new_tree_values = match new_file_type {
1829 FileType::Normal { exec_bit } => {
1830 self.write_path_to_store(
1831 repo_path,
1832 disk_path,
1833 ¤t_tree_values,
1834 exec_bit,
1835 maybe_current_file_state.and_then(|state| state.materialized_conflict_data),
1836 )
1837 .await?
1838 }
1839 FileType::Symlink => {
1840 let id = self.write_symlink_to_store(repo_path, disk_path).await?;
1841 Merge::normal(TreeValue::Symlink(id))
1842 }
1843 FileType::GitSubmodule => panic!("git submodule cannot be written to store"),
1844 };
1845 if new_tree_values != current_tree_values {
1846 Ok(Some(new_tree_values))
1847 } else {
1848 Ok(None)
1849 }
1850 }
1851 }
1852
1853 fn store(&self) -> &Store {
1854 &self.tree_state.store
1855 }
1856
1857 async fn write_path_to_store(
1858 &self,
1859 repo_path: &RepoPath,
1860 disk_path: &Path,
1861 current_tree_values: &MergedTreeValue,
1862 exec_bit: ExecBit,
1863 materialized_conflict_data: Option<MaterializedConflictData>,
1864 ) -> Result<MergedTreeValue, SnapshotError> {
1865 if let Some(current_tree_value) = current_tree_values.as_resolved() {
1866 let id = self.write_file_to_store(repo_path, disk_path).await?;
1867 let executable = exec_bit.for_tree_value(self.tree_state.exec_policy, || {
1869 if let Some(TreeValue::File {
1870 id: _,
1871 executable,
1872 copy_id: _,
1873 }) = current_tree_value
1874 {
1875 Some(*executable)
1876 } else {
1877 None
1878 }
1879 });
1880 let copy_id = {
1882 if let Some(TreeValue::File {
1883 id: _,
1884 executable: _,
1885 copy_id,
1886 }) = current_tree_value
1887 {
1888 copy_id.clone()
1889 } else {
1890 CopyId::placeholder()
1891 }
1892 };
1893 Ok(Merge::normal(TreeValue::File {
1894 id,
1895 executable,
1896 copy_id,
1897 }))
1898 } else if let Some(old_file_ids) = current_tree_values.to_file_merge() {
1899 let copy_id_merge = current_tree_values.to_copy_id_merge().unwrap();
1901 let copy_id = copy_id_merge
1902 .resolve_trivial(SameChange::Accept)
1903 .cloned()
1904 .flatten()
1905 .unwrap_or_else(CopyId::placeholder);
1906 let mut contents = vec![];
1907 let file = File::open(disk_path).map_err(|err| SnapshotError::Other {
1908 message: format!("Failed to open file {}", disk_path.display()),
1909 err: err.into(),
1910 })?;
1911 self.tree_state
1912 .target_eol_strategy
1913 .convert_eol_for_snapshot(BlockingAsyncReader::new(file))
1914 .await
1915 .map_err(|err| SnapshotError::Other {
1916 message: "Failed to convert the EOL".to_string(),
1917 err: err.into(),
1918 })?
1919 .read_to_end(&mut contents)
1920 .await
1921 .map_err(|err| SnapshotError::Other {
1922 message: "Failed to read the EOL converted contents".to_string(),
1923 err: err.into(),
1924 })?;
1925 let new_file_ids = conflicts::update_from_content(
1929 &old_file_ids,
1930 self.store(),
1931 repo_path,
1932 &contents,
1933 materialized_conflict_data.map_or(MIN_CONFLICT_MARKER_LEN, |data| {
1934 data.conflict_marker_len as usize
1935 }),
1936 )
1937 .await?;
1938 match new_file_ids.into_resolved() {
1939 Ok(file_id) => {
1940 let executable = exec_bit.for_tree_value(self.tree_state.exec_policy, || {
1942 current_tree_values
1943 .to_executable_merge()
1944 .as_ref()
1945 .and_then(conflicts::resolve_file_executable)
1946 });
1947 Ok(Merge::normal(TreeValue::File {
1948 id: file_id.unwrap(),
1949 executable,
1950 copy_id,
1951 }))
1952 }
1953 Err(new_file_ids) => {
1954 if new_file_ids != old_file_ids {
1955 Ok(current_tree_values.with_new_file_ids(&new_file_ids))
1956 } else {
1957 Ok(current_tree_values.clone())
1958 }
1959 }
1960 }
1961 } else {
1962 Ok(current_tree_values.clone())
1963 }
1964 }
1965
1966 async fn write_file_to_store(
1967 &self,
1968 path: &RepoPath,
1969 disk_path: &Path,
1970 ) -> Result<FileId, SnapshotError> {
1971 let file = File::open(disk_path).map_err(|err| SnapshotError::Other {
1972 message: format!("Failed to open file {}", disk_path.display()),
1973 err: err.into(),
1974 })?;
1975 let mut contents = self
1976 .tree_state
1977 .target_eol_strategy
1978 .convert_eol_for_snapshot(BlockingAsyncReader::new(file))
1979 .await
1980 .map_err(|err| SnapshotError::Other {
1981 message: "Failed to convert the EOL".to_string(),
1982 err: err.into(),
1983 })?;
1984 Ok(self.store().write_file(path, &mut contents).await?)
1985 }
1986
1987 async fn write_symlink_to_store(
1988 &self,
1989 path: &RepoPath,
1990 disk_path: &Path,
1991 ) -> Result<SymlinkId, SnapshotError> {
1992 if self.tree_state.symlink_support {
1993 let target = disk_path.read_link().map_err(|err| SnapshotError::Other {
1994 message: format!("Failed to read symlink {}", disk_path.display()),
1995 err: err.into(),
1996 })?;
1997 let str_target = symlink_target_convert_to_store(&target).ok_or_else(|| {
1998 SnapshotError::InvalidUtf8SymlinkTarget {
1999 path: disk_path.to_path_buf(),
2000 }
2001 })?;
2002 Ok(self.store().write_symlink(path, &str_target).await?)
2003 } else {
2004 let target = fs::read(disk_path).map_err(|err| SnapshotError::Other {
2005 message: format!("Failed to read file {}", disk_path.display()),
2006 err: err.into(),
2007 })?;
2008 let string_target =
2009 String::from_utf8(target).map_err(|_| SnapshotError::InvalidUtf8SymlinkTarget {
2010 path: disk_path.to_path_buf(),
2011 })?;
2012 Ok(self.store().write_symlink(path, &string_target).await?)
2013 }
2014 }
2015}
2016
2017fn snapshot_error_for_mtime_out_of_range(err: MtimeOutOfRange, path: &Path) -> SnapshotError {
2018 SnapshotError::Other {
2019 message: format!("Failed to process file metadata {}", path.display()),
2020 err: err.into(),
2021 }
2022}
2023
2024impl TreeState {
2026 async fn write_file(
2027 &self,
2028 disk_path: &Path,
2029 contents: impl AsyncRead + Send + Unpin,
2030 exec_bit: ExecBit,
2031 apply_eol_conversion: bool,
2032 ) -> Result<FileState, CheckoutError> {
2033 let mut file = File::options()
2034 .write(true)
2035 .create_new(true) .open(disk_path)
2037 .map_err(|err| CheckoutError::Other {
2038 message: format!("Failed to open file {} for writing", disk_path.display()),
2039 err: err.into(),
2040 })?;
2041 let contents = if apply_eol_conversion {
2042 self.target_eol_strategy
2043 .convert_eol_for_update(contents)
2044 .await
2045 .map_err(|err| CheckoutError::Other {
2046 message: "Failed to convert the EOL for the content".to_string(),
2047 err: err.into(),
2048 })?
2049 } else {
2050 Box::new(contents)
2051 };
2052 let size = copy_async_to_sync(contents, &mut file)
2053 .await
2054 .map_err(|err| CheckoutError::Other {
2055 message: format!(
2056 "Failed to write the content to the file {}",
2057 disk_path.display()
2058 ),
2059 err: err.into(),
2060 })?;
2061 set_executable(exec_bit, disk_path)
2062 .map_err(|err| checkout_error_for_stat_error(err, disk_path))?;
2063 let metadata = file
2068 .metadata()
2069 .map_err(|err| checkout_error_for_stat_error(err, disk_path))?;
2070 FileState::for_file(exec_bit, size as u64, &metadata)
2071 .map_err(|err| checkout_error_for_mtime_out_of_range(err, disk_path))
2072 }
2073
2074 fn write_symlink(&self, disk_path: &Path, target: String) -> Result<FileState, CheckoutError> {
2075 let target = symlink_target_convert_to_disk(&target);
2076
2077 if cfg!(windows) {
2078 debug_assert_ne!(
2086 target.as_os_str().to_str().map(|path| path.contains('/')),
2087 Some(true),
2088 "Expect the symlink target doesn't contain \"/\", but got invalid symlink target: \
2089 {}.",
2090 target.display()
2091 );
2092 }
2093
2094 symlink_file(&target, disk_path).map_err(|err| CheckoutError::Other {
2098 message: format!(
2099 "Failed to create symlink from {} to {}",
2100 disk_path.display(),
2101 target.display()
2102 ),
2103 err: err.into(),
2104 })?;
2105 let metadata = disk_path
2106 .symlink_metadata()
2107 .map_err(|err| checkout_error_for_stat_error(err, disk_path))?;
2108 FileState::for_symlink(&metadata)
2109 .map_err(|err| checkout_error_for_mtime_out_of_range(err, disk_path))
2110 }
2111
2112 async fn write_conflict(
2113 &self,
2114 disk_path: &Path,
2115 contents: &[u8],
2116 exec_bit: ExecBit,
2117 ) -> Result<FileState, CheckoutError> {
2118 let contents = self
2119 .target_eol_strategy
2120 .convert_eol_for_update(contents)
2121 .await
2122 .map_err(|err| CheckoutError::Other {
2123 message: "Failed to convert the EOL when writing a merge conflict".to_string(),
2124 err: err.into(),
2125 })?;
2126 let mut file = OpenOptions::new()
2127 .write(true)
2128 .create_new(true) .open(disk_path)
2130 .map_err(|err| CheckoutError::Other {
2131 message: format!("Failed to open file {} for writing", disk_path.display()),
2132 err: err.into(),
2133 })?;
2134 let size = copy_async_to_sync(contents, &mut file)
2135 .await
2136 .map_err(|err| CheckoutError::Other {
2137 message: format!("Failed to write conflict to file {}", disk_path.display()),
2138 err: err.into(),
2139 })? as u64;
2140 set_executable(exec_bit, disk_path)
2141 .map_err(|err| checkout_error_for_stat_error(err, disk_path))?;
2142 let metadata = file
2143 .metadata()
2144 .map_err(|err| checkout_error_for_stat_error(err, disk_path))?;
2145 FileState::for_file(exec_bit, size, &metadata)
2146 .map_err(|err| checkout_error_for_mtime_out_of_range(err, disk_path))
2147 }
2148
2149 pub fn check_out(&mut self, new_tree: &MergedTree) -> Result<CheckoutStats, CheckoutError> {
2150 let old_tree = self.tree.clone();
2151 let stats = self
2152 .update(&old_tree, new_tree, self.sparse_matcher().as_ref())
2153 .block_on()?;
2154 self.tree = new_tree.clone();
2155 Ok(stats)
2156 }
2157
2158 pub fn set_sparse_patterns(
2159 &mut self,
2160 sparse_patterns: Vec<RepoPathBuf>,
2161 ) -> Result<CheckoutStats, CheckoutError> {
2162 let tree = self.tree.clone();
2163 let old_matcher = PrefixMatcher::new(&self.sparse_patterns);
2164 let new_matcher = PrefixMatcher::new(&sparse_patterns);
2165 let added_matcher = DifferenceMatcher::new(&new_matcher, &old_matcher);
2166 let removed_matcher = DifferenceMatcher::new(&old_matcher, &new_matcher);
2167 let empty_tree = self.store.empty_merged_tree();
2168 let added_stats = self.update(&empty_tree, &tree, &added_matcher).block_on()?;
2169 let removed_stats = self
2170 .update(&tree, &empty_tree, &removed_matcher)
2171 .block_on()?;
2172 self.sparse_patterns = sparse_patterns;
2173 assert_eq!(added_stats.updated_files, 0);
2174 assert_eq!(added_stats.removed_files, 0);
2175 assert_eq!(removed_stats.updated_files, 0);
2176 assert_eq!(removed_stats.added_files, 0);
2177 assert_eq!(removed_stats.skipped_files, 0);
2178 Ok(CheckoutStats {
2179 updated_files: 0,
2180 added_files: added_stats.added_files,
2181 removed_files: removed_stats.removed_files,
2182 skipped_files: added_stats.skipped_files,
2183 })
2184 }
2185
2186 async fn update(
2187 &mut self,
2188 old_tree: &MergedTree,
2189 new_tree: &MergedTree,
2190 matcher: &dyn Matcher,
2191 ) -> Result<CheckoutStats, CheckoutError> {
2192 let mut stats = CheckoutStats {
2195 updated_files: 0,
2196 added_files: 0,
2197 removed_files: 0,
2198 skipped_files: 0,
2199 };
2200 let mut changed_file_states = Vec::new();
2201 let mut deleted_files = HashSet::new();
2202 let mut prev_created_path: RepoPathBuf = RepoPathBuf::root();
2203
2204 let mut process_diff_entry = async |path: RepoPathBuf,
2205 before: MergedTreeValue,
2206 after: MaterializedTreeValue|
2207 -> Result<(), CheckoutError> {
2208 if after.is_absent() {
2209 stats.removed_files += 1;
2210 } else if before.is_absent() {
2211 stats.added_files += 1;
2212 } else {
2213 stats.updated_files += 1;
2214 }
2215
2216 if matches!(before.as_normal(), Some(TreeValue::GitSubmodule(_)))
2224 && matches!(after, MaterializedTreeValue::GitSubmodule(_))
2225 {
2226 eprintln!("ignoring git submodule at {path:?}");
2227 return Ok(());
2230 }
2231
2232 let (common_prefix, adjusted_diff_file_path) =
2237 path.split_common_prefix(&prev_created_path);
2238
2239 let disk_path = if adjusted_diff_file_path.is_root() {
2240 path.to_fs_path(self.working_copy_path())?
2255 } else {
2256 let adjusted_working_copy_path =
2257 common_prefix.to_fs_path(self.working_copy_path())?;
2258
2259 let Some(disk_path) =
2262 create_parent_dirs(&adjusted_working_copy_path, adjusted_diff_file_path)?
2263 else {
2264 changed_file_states.push((path, FileState::placeholder()));
2265 stats.skipped_files += 1;
2266 return Ok(());
2267 };
2268
2269 prev_created_path = path
2274 .parent()
2275 .map(RepoPath::to_owned)
2276 .expect("diff path has no parent");
2277
2278 disk_path
2279 };
2280
2281 let present_file_deleted = before.is_present()
2283 && if matches!(before.as_normal(), Some(TreeValue::GitSubmodule(_))) {
2284 remove_old_submodule_dir(&disk_path)?
2285 } else {
2286 remove_old_file(&disk_path)?
2287 };
2288
2289 if !present_file_deleted && !can_create_new_file(&disk_path)? {
2291 if matches!(after, MaterializedTreeValue::GitSubmodule(_)) && disk_path.is_dir() {
2292 } else if matches!(before.as_normal(), Some(TreeValue::GitSubmodule(_)))
2298 && after.is_absent()
2299 {
2300 } else {
2306 changed_file_states.push((path, FileState::placeholder()));
2307 stats.skipped_files += 1;
2308 return Ok(());
2309 }
2310 }
2311
2312 let get_prev_exec = || self.file_states().get_exec_bit(&path);
2316
2317 let file_state = match after {
2319 MaterializedTreeValue::Absent | MaterializedTreeValue::AccessDenied(_) => {
2320 prev_created_path = RepoPathBuf::root();
2324
2325 let mut parent_dir = disk_path.parent().unwrap();
2326 loop {
2327 if fs::remove_dir(parent_dir).is_err() {
2328 break;
2329 }
2330
2331 parent_dir = parent_dir.parent().unwrap();
2332 }
2333 deleted_files.insert(path);
2334 return Ok(());
2335 }
2336 MaterializedTreeValue::File(file) => {
2337 let exec_bit =
2338 ExecBit::new_from_repo(file.executable, self.exec_policy, get_prev_exec);
2339 self.write_file(&disk_path, file.reader, exec_bit, true)
2340 .await?
2341 }
2342 MaterializedTreeValue::Symlink { id: _, target } => {
2343 if self.symlink_support {
2344 self.write_symlink(&disk_path, target)?
2345 } else {
2346 self.write_file(&disk_path, target.as_bytes(), ExecBit(false), false)
2348 .await?
2349 }
2350 }
2351 MaterializedTreeValue::GitSubmodule(_) => {
2352 eprintln!("ignoring git submodule at {path:?}");
2353 match fs::create_dir(&disk_path) {
2356 Ok(()) => {}
2357 Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
2358 Err(err) => eprintln!(
2359 "warning: failed to create submodule directory {path:?}: {err}"
2360 ),
2361 }
2362 FileState::for_gitsubmodule()
2363 }
2364 MaterializedTreeValue::Tree(_) => {
2365 panic!("unexpected tree entry in diff at {path:?}");
2366 }
2367 MaterializedTreeValue::FileConflict(file) => {
2368 let conflict_marker_len =
2369 choose_materialized_conflict_marker_len(&file.contents);
2370 let options = ConflictMaterializeOptions {
2371 marker_style: self.conflict_marker_style,
2372 marker_len: Some(conflict_marker_len),
2373 merge: self.store.merge_options().clone(),
2374 };
2375 let exec_bit = ExecBit::new_from_repo(
2376 file.executable.unwrap_or(false),
2377 self.exec_policy,
2378 get_prev_exec,
2379 );
2380 let contents =
2381 materialize_merge_result_to_bytes(&file.contents, &file.labels, &options);
2382 let mut file_state =
2383 self.write_conflict(&disk_path, &contents, exec_bit).await?;
2384 file_state.materialized_conflict_data = Some(MaterializedConflictData {
2385 conflict_marker_len: conflict_marker_len.try_into().unwrap_or(u32::MAX),
2386 });
2387 file_state
2388 }
2389 MaterializedTreeValue::OtherConflict { id, labels } => {
2390 let contents = id.describe(&labels);
2393 self.write_conflict(&disk_path, contents.as_bytes(), ExecBit(false))
2395 .await?
2396 }
2397 };
2398 changed_file_states.push((path, file_state));
2399 Ok(())
2400 };
2401
2402 let mut diff_stream = old_tree
2403 .diff_stream_for_file_system(new_tree, matcher)
2404 .map(async |TreeDiffEntry { path, values }| match values {
2405 Ok(diff) => {
2406 let result =
2407 materialize_tree_value(&self.store, &path, diff.after, new_tree.labels())
2408 .await;
2409 (path, result.map(|value| (diff.before, value)))
2410 }
2411 Err(err) => (path, Err(err)),
2412 })
2413 .buffered(self.store.concurrency().max(1));
2414
2415 let mut conflicts_to_rematerialize: HashMap<RepoPathBuf, MergedTreeValue> =
2421 if old_tree.tree_ids().num_sides() == new_tree.tree_ids().num_sides()
2422 && old_tree.labels() != new_tree.labels()
2423 {
2424 new_tree
2428 .conflicts_matching(matcher)
2429 .map(|(path, value)| value.map(|value| (path, value)))
2430 .try_collect()?
2431 } else {
2432 HashMap::new()
2433 };
2434
2435 while let Some((path, data)) = diff_stream.next().await {
2436 let (before, after) = data?;
2437 conflicts_to_rematerialize.remove(&path);
2438 process_diff_entry(path, before, after).await?;
2439 }
2440
2441 if !conflicts_to_rematerialize.is_empty() {
2442 for (path, conflict) in conflicts_to_rematerialize {
2443 let materialized =
2444 materialize_tree_value(&self.store, &path, conflict.clone(), new_tree.labels())
2445 .await?;
2446 process_diff_entry(path, conflict, materialized).await?;
2447 }
2448
2449 changed_file_states.sort_unstable_by(|(path1, _), (path2, _)| path1.cmp(path2));
2452 }
2453
2454 self.file_states
2455 .merge_in(changed_file_states, &deleted_files);
2456 Ok(stats)
2457 }
2458
2459 pub async fn reset(&mut self, new_tree: &MergedTree) -> Result<(), ResetError> {
2460 let matcher = self.sparse_matcher();
2461 let mut changed_file_states = Vec::new();
2462 let mut deleted_files = HashSet::new();
2463 let mut diff_stream = self
2464 .tree
2465 .diff_stream_for_file_system(new_tree, matcher.as_ref());
2466 while let Some(TreeDiffEntry { path, values }) = diff_stream.next().await {
2467 let after = values?.after;
2468 if after.is_absent() {
2469 deleted_files.insert(path);
2470 } else {
2471 let file_type = match after.into_resolved() {
2472 Ok(value) => match value.unwrap() {
2473 TreeValue::File {
2474 id: _,
2475 executable,
2476 copy_id: _,
2477 } => {
2478 let get_prev_exec = || self.file_states().get_exec_bit(&path);
2479 let exec_bit =
2480 ExecBit::new_from_repo(executable, self.exec_policy, get_prev_exec);
2481 FileType::Normal { exec_bit }
2482 }
2483 TreeValue::Symlink(_id) => FileType::Symlink,
2484 TreeValue::GitSubmodule(_id) => {
2485 eprintln!("ignoring git submodule at {path:?}");
2486 FileType::GitSubmodule
2487 }
2488 TreeValue::Tree(_id) => {
2489 panic!("unexpected tree entry in diff at {path:?}");
2490 }
2491 },
2492 Err(_values) => {
2493 FileType::Normal {
2495 exec_bit: ExecBit(false),
2496 }
2497 }
2498 };
2499 let file_state = FileState {
2500 file_type,
2501 mtime: MillisSinceEpoch(0),
2502 size: 0,
2503 materialized_conflict_data: None,
2504 };
2505 changed_file_states.push((path, file_state));
2506 }
2507 }
2508 self.file_states
2509 .merge_in(changed_file_states, &deleted_files);
2510 self.tree = new_tree.clone();
2511 Ok(())
2512 }
2513
2514 pub async fn recover(&mut self, new_tree: &MergedTree) -> Result<(), ResetError> {
2515 self.file_states.clear();
2516 self.tree = self.store.empty_merged_tree();
2517 self.reset(new_tree).await
2518 }
2519}
2520
2521fn checkout_error_for_stat_error(err: io::Error, path: &Path) -> CheckoutError {
2522 CheckoutError::Other {
2523 message: format!("Failed to stat file {}", path.display()),
2524 err: err.into(),
2525 }
2526}
2527
2528fn checkout_error_for_mtime_out_of_range(err: MtimeOutOfRange, path: &Path) -> CheckoutError {
2529 CheckoutError::Other {
2530 message: format!("Failed to process file metadata {}", path.display()),
2531 err: err.into(),
2532 }
2533}
2534
2535#[derive(Clone, Debug)]
2537struct CheckoutState {
2538 operation_id: OperationId,
2539 workspace_name: WorkspaceNameBuf,
2540}
2541
2542impl CheckoutState {
2543 fn load(state_path: &Path) -> Result<Self, WorkingCopyStateError> {
2544 let wrap_err = |err| WorkingCopyStateError {
2545 message: "Failed to read checkout state".to_owned(),
2546 err,
2547 };
2548 let buf = fs::read(state_path.join("checkout")).map_err(|err| wrap_err(err.into()))?;
2549 let proto = crate::protos::local_working_copy::Checkout::decode(&*buf)
2550 .map_err(|err| wrap_err(err.into()))?;
2551 Ok(Self {
2552 operation_id: OperationId::new(proto.operation_id),
2553 workspace_name: if proto.workspace_name.is_empty() {
2554 WorkspaceName::DEFAULT.to_owned()
2557 } else {
2558 proto.workspace_name.into()
2559 },
2560 })
2561 }
2562
2563 #[instrument(skip_all)]
2564 fn save(&self, state_path: &Path) -> Result<(), WorkingCopyStateError> {
2565 let wrap_err = |err| WorkingCopyStateError {
2566 message: "Failed to write checkout state".to_owned(),
2567 err,
2568 };
2569 let proto = crate::protos::local_working_copy::Checkout {
2570 operation_id: self.operation_id.to_bytes(),
2571 workspace_name: (*self.workspace_name).into(),
2572 };
2573 let mut temp_file =
2574 NamedTempFile::new_in(state_path).map_err(|err| wrap_err(err.into()))?;
2575 temp_file
2576 .as_file_mut()
2577 .write_all(&proto.encode_to_vec())
2578 .map_err(|err| wrap_err(err.into()))?;
2579 persist_temp_file(temp_file, state_path.join("checkout"))
2582 .map_err(|err| wrap_err(err.into()))?;
2583 Ok(())
2584 }
2585}
2586
2587pub struct LocalWorkingCopy {
2588 store: Arc<Store>,
2589 working_copy_path: PathBuf,
2590 state_path: PathBuf,
2591 checkout_state: CheckoutState,
2592 tree_state: OnceCell<TreeState>,
2593 tree_state_settings: TreeStateSettings,
2594}
2595
2596#[async_trait(?Send)]
2597impl WorkingCopy for LocalWorkingCopy {
2598 fn name(&self) -> &str {
2599 Self::name()
2600 }
2601
2602 fn workspace_name(&self) -> &WorkspaceName {
2603 &self.checkout_state.workspace_name
2604 }
2605
2606 fn operation_id(&self) -> &OperationId {
2607 &self.checkout_state.operation_id
2608 }
2609
2610 fn tree(&self) -> Result<&MergedTree, WorkingCopyStateError> {
2611 Ok(self.tree_state()?.current_tree())
2612 }
2613
2614 fn sparse_patterns(&self) -> Result<&[RepoPathBuf], WorkingCopyStateError> {
2615 Ok(self.tree_state()?.sparse_patterns())
2616 }
2617
2618 async fn start_mutation(&self) -> Result<Box<dyn LockedWorkingCopy>, WorkingCopyStateError> {
2619 let lock_path = self.state_path.join("working_copy.lock");
2620 let lock = FileLock::lock(lock_path).map_err(|err| WorkingCopyStateError {
2621 message: "Failed to lock working copy".to_owned(),
2622 err: err.into(),
2623 })?;
2624
2625 let wc = Self {
2626 store: self.store.clone(),
2627 working_copy_path: self.working_copy_path.clone(),
2628 state_path: self.state_path.clone(),
2629 checkout_state: CheckoutState::load(&self.state_path)?,
2631 tree_state: OnceCell::new(),
2635 tree_state_settings: self.tree_state_settings.clone(),
2636 };
2637 let old_operation_id = wc.operation_id().clone();
2638 let old_tree = wc.tree()?.clone();
2639 Ok(Box::new(LockedLocalWorkingCopy {
2640 wc,
2641 old_operation_id,
2642 old_tree,
2643 tree_state_dirty: false,
2644 new_workspace_name: None,
2645 _lock: lock,
2646 }))
2647 }
2648}
2649
2650impl LocalWorkingCopy {
2651 pub fn name() -> &'static str {
2652 "local"
2653 }
2654
2655 pub fn init(
2659 store: Arc<Store>,
2660 working_copy_path: PathBuf,
2661 state_path: PathBuf,
2662 operation_id: OperationId,
2663 workspace_name: WorkspaceNameBuf,
2664 user_settings: &UserSettings,
2665 ) -> Result<Self, WorkingCopyStateError> {
2666 let checkout_state = CheckoutState {
2667 operation_id,
2668 workspace_name,
2669 };
2670 checkout_state.save(&state_path)?;
2671 let tree_state_settings = TreeStateSettings::try_from_user_settings(user_settings)
2672 .map_err(|err| WorkingCopyStateError {
2673 message: "Failed to read the tree state settings".to_string(),
2674 err: err.into(),
2675 })?;
2676 let tree_state = TreeState::init(
2677 store.clone(),
2678 working_copy_path.clone(),
2679 state_path.clone(),
2680 &tree_state_settings,
2681 )
2682 .map_err(|err| WorkingCopyStateError {
2683 message: "Failed to initialize working copy state".to_string(),
2684 err: err.into(),
2685 })?;
2686 Ok(Self {
2687 store,
2688 working_copy_path,
2689 state_path,
2690 checkout_state,
2691 tree_state: OnceCell::with_value(tree_state),
2692 tree_state_settings,
2693 })
2694 }
2695
2696 pub fn load(
2697 store: Arc<Store>,
2698 working_copy_path: PathBuf,
2699 state_path: PathBuf,
2700 user_settings: &UserSettings,
2701 ) -> Result<Self, WorkingCopyStateError> {
2702 let checkout_state = CheckoutState::load(&state_path)?;
2703 let tree_state_settings = TreeStateSettings::try_from_user_settings(user_settings)
2704 .map_err(|err| WorkingCopyStateError {
2705 message: "Failed to read the tree state settings".to_string(),
2706 err: err.into(),
2707 })?;
2708 Ok(Self {
2709 store,
2710 working_copy_path,
2711 state_path,
2712 checkout_state,
2713 tree_state: OnceCell::new(),
2714 tree_state_settings,
2715 })
2716 }
2717
2718 pub fn state_path(&self) -> &Path {
2719 &self.state_path
2720 }
2721
2722 #[instrument(skip_all)]
2723 fn tree_state(&self) -> Result<&TreeState, WorkingCopyStateError> {
2724 self.tree_state.get_or_try_init(|| {
2725 TreeState::load(
2726 self.store.clone(),
2727 self.working_copy_path.clone(),
2728 self.state_path.clone(),
2729 &self.tree_state_settings,
2730 )
2731 .map_err(|err| WorkingCopyStateError {
2732 message: "Failed to read working copy state".to_string(),
2733 err: err.into(),
2734 })
2735 })
2736 }
2737
2738 fn tree_state_mut(&mut self) -> Result<&mut TreeState, WorkingCopyStateError> {
2739 self.tree_state()?; Ok(self.tree_state.get_mut().unwrap())
2741 }
2742
2743 pub fn file_states(&self) -> Result<FileStates<'_>, WorkingCopyStateError> {
2744 Ok(self.tree_state()?.file_states())
2745 }
2746
2747 #[cfg(feature = "watchman")]
2748 pub async fn query_watchman(
2749 &self,
2750 config: &WatchmanConfig,
2751 ) -> Result<(watchman::Clock, Option<Vec<PathBuf>>), WorkingCopyStateError> {
2752 self.tree_state()?
2753 .query_watchman(config)
2754 .await
2755 .map_err(|err| WorkingCopyStateError {
2756 message: "Failed to query watchman".to_string(),
2757 err: err.into(),
2758 })
2759 }
2760
2761 #[cfg(feature = "watchman")]
2762 pub async fn is_watchman_trigger_registered(
2763 &self,
2764 config: &WatchmanConfig,
2765 ) -> Result<bool, WorkingCopyStateError> {
2766 self.tree_state()?
2767 .is_watchman_trigger_registered(config)
2768 .await
2769 .map_err(|err| WorkingCopyStateError {
2770 message: "Failed to query watchman".to_string(),
2771 err: err.into(),
2772 })
2773 }
2774}
2775
2776pub struct LocalWorkingCopyFactory {}
2777
2778impl WorkingCopyFactory for LocalWorkingCopyFactory {
2779 fn init_working_copy(
2780 &self,
2781 store: Arc<Store>,
2782 working_copy_path: PathBuf,
2783 state_path: PathBuf,
2784 operation_id: OperationId,
2785 workspace_name: WorkspaceNameBuf,
2786 settings: &UserSettings,
2787 ) -> Result<Box<dyn WorkingCopy>, WorkingCopyStateError> {
2788 Ok(Box::new(LocalWorkingCopy::init(
2789 store,
2790 working_copy_path,
2791 state_path,
2792 operation_id,
2793 workspace_name,
2794 settings,
2795 )?))
2796 }
2797
2798 fn load_working_copy(
2799 &self,
2800 store: Arc<Store>,
2801 working_copy_path: PathBuf,
2802 state_path: PathBuf,
2803 settings: &UserSettings,
2804 ) -> Result<Box<dyn WorkingCopy>, WorkingCopyStateError> {
2805 Ok(Box::new(LocalWorkingCopy::load(
2806 store,
2807 working_copy_path,
2808 state_path,
2809 settings,
2810 )?))
2811 }
2812}
2813
2814pub struct LockedLocalWorkingCopy {
2817 wc: LocalWorkingCopy,
2818 old_operation_id: OperationId,
2819 old_tree: MergedTree,
2820 tree_state_dirty: bool,
2821 new_workspace_name: Option<WorkspaceNameBuf>,
2822 _lock: FileLock,
2823}
2824
2825#[async_trait]
2826impl LockedWorkingCopy for LockedLocalWorkingCopy {
2827 fn old_operation_id(&self) -> &OperationId {
2828 &self.old_operation_id
2829 }
2830
2831 fn old_tree(&self) -> &MergedTree {
2832 &self.old_tree
2833 }
2834
2835 async fn snapshot(
2836 &mut self,
2837 options: &SnapshotOptions,
2838 ) -> Result<(MergedTree, SnapshotStats), SnapshotError> {
2839 let tree_state = self.wc.tree_state_mut()?;
2840 let (is_dirty, stats) = tree_state.snapshot(options).await?;
2841 self.tree_state_dirty |= is_dirty;
2842 Ok((tree_state.current_tree().clone(), stats))
2843 }
2844
2845 async fn check_out(&mut self, commit: &Commit) -> Result<CheckoutStats, CheckoutError> {
2846 let new_tree = commit.tree();
2849 let tree_state = self.wc.tree_state_mut()?;
2850 if tree_state.tree.tree_ids_and_labels() != new_tree.tree_ids_and_labels() {
2851 let stats = tree_state.check_out(&new_tree)?;
2852 self.tree_state_dirty = true;
2853 Ok(stats)
2854 } else {
2855 Ok(CheckoutStats::default())
2856 }
2857 }
2858
2859 fn rename_workspace(&mut self, new_name: WorkspaceNameBuf) {
2860 self.new_workspace_name = Some(new_name);
2861 }
2862
2863 async fn reset(&mut self, commit: &Commit) -> Result<(), ResetError> {
2864 let new_tree = commit.tree();
2865 self.wc.tree_state_mut()?.reset(&new_tree).await?;
2866 self.tree_state_dirty = true;
2867 Ok(())
2868 }
2869
2870 async fn recover(&mut self, commit: &Commit) -> Result<(), ResetError> {
2871 let new_tree = commit.tree();
2872 self.wc.tree_state_mut()?.recover(&new_tree).await?;
2873 self.tree_state_dirty = true;
2874 Ok(())
2875 }
2876
2877 fn sparse_patterns(&self) -> Result<&[RepoPathBuf], WorkingCopyStateError> {
2878 self.wc.sparse_patterns()
2879 }
2880
2881 async fn set_sparse_patterns(
2882 &mut self,
2883 new_sparse_patterns: Vec<RepoPathBuf>,
2884 ) -> Result<CheckoutStats, CheckoutError> {
2885 let stats = self
2888 .wc
2889 .tree_state_mut()?
2890 .set_sparse_patterns(new_sparse_patterns)?;
2891 self.tree_state_dirty = true;
2892 Ok(stats)
2893 }
2894
2895 #[instrument(skip_all)]
2896 async fn finish(
2897 mut self: Box<Self>,
2898 operation_id: OperationId,
2899 ) -> Result<Box<dyn WorkingCopy>, WorkingCopyStateError> {
2900 assert!(
2901 self.tree_state_dirty
2902 || self.old_tree.tree_ids_and_labels() == self.wc.tree()?.tree_ids_and_labels()
2903 );
2904 if self.tree_state_dirty {
2905 self.wc
2906 .tree_state_mut()?
2907 .save()
2908 .map_err(|err| WorkingCopyStateError {
2909 message: "Failed to write working copy state".to_string(),
2910 err: Box::new(err),
2911 })?;
2912 }
2913 if self.old_operation_id != operation_id || self.new_workspace_name.is_some() {
2914 self.wc.checkout_state.operation_id = operation_id;
2915 if let Some(workspace_name) = self.new_workspace_name {
2916 self.wc.checkout_state.workspace_name = workspace_name;
2917 }
2918 self.wc.checkout_state.save(&self.wc.state_path)?;
2919 }
2920 Ok(Box::new(self.wc))
2922 }
2923}
2924
2925impl LockedLocalWorkingCopy {
2926 pub fn reset_watchman(&mut self) -> Result<(), SnapshotError> {
2927 self.wc.tree_state_mut()?.reset_watchman();
2928 self.tree_state_dirty = true;
2929 Ok(())
2930 }
2931}
2932
2933#[cfg(test)]
2934mod tests {
2935 use std::time::Duration;
2936
2937 use maplit::hashset;
2938
2939 use super::*;
2940
2941 fn repo_path(value: &str) -> &RepoPath {
2942 RepoPath::from_internal_string(value).unwrap()
2943 }
2944
2945 fn repo_path_component(value: &str) -> &RepoPathComponent {
2946 RepoPathComponent::new(value).unwrap()
2947 }
2948
2949 fn new_state(size: u64) -> FileState {
2950 FileState {
2951 file_type: FileType::Normal {
2952 exec_bit: ExecBit(false),
2953 },
2954 mtime: MillisSinceEpoch(0),
2955 size,
2956 materialized_conflict_data: None,
2957 }
2958 }
2959
2960 #[test]
2961 fn test_file_states_merge() {
2962 let new_static_entry = |path: &'static str, size| (repo_path(path), new_state(size));
2963 let new_owned_entry = |path: &str, size| (repo_path(path).to_owned(), new_state(size));
2964 let new_proto_entry = |path: &str, size| {
2965 file_state_entry_to_proto(repo_path(path).to_owned(), &new_state(size))
2966 };
2967 let data = vec![
2968 new_proto_entry("aa", 0),
2969 new_proto_entry("b#", 4), new_proto_entry("b/c", 1),
2971 new_proto_entry("b/d/e", 2),
2972 new_proto_entry("b/e", 3),
2973 new_proto_entry("bc", 5),
2974 ];
2975 let mut file_states = FileStatesMap::from_proto(data, false);
2976
2977 let changed_file_states = vec![
2978 new_owned_entry("aa", 10), new_owned_entry("b/d/f", 11), new_owned_entry("b/e", 12), new_owned_entry("c", 13), ];
2983 let deleted_files = hashset! {
2984 repo_path("b/c").to_owned(),
2985 repo_path("b#").to_owned(),
2986 };
2987 file_states.merge_in(changed_file_states, &deleted_files);
2988 assert_eq!(
2989 file_states.all().iter().collect_vec(),
2990 vec![
2991 new_static_entry("aa", 10),
2992 new_static_entry("b/d/e", 2),
2993 new_static_entry("b/d/f", 11),
2994 new_static_entry("b/e", 12),
2995 new_static_entry("bc", 5),
2996 new_static_entry("c", 13),
2997 ],
2998 );
2999 }
3000
3001 #[test]
3002 fn test_file_states_lookup() {
3003 let new_proto_entry = |path: &str, size| {
3004 file_state_entry_to_proto(repo_path(path).to_owned(), &new_state(size))
3005 };
3006 let data = vec![
3007 new_proto_entry("aa", 0),
3008 new_proto_entry("b/c", 1),
3009 new_proto_entry("b/d/e", 2),
3010 new_proto_entry("b/e", 3),
3011 new_proto_entry("b#", 4), new_proto_entry("bc", 5),
3013 ];
3014 let file_states = FileStates::from_sorted(&data);
3015
3016 assert_eq!(
3017 file_states.prefixed(repo_path("")).paths().collect_vec(),
3018 ["aa", "b/c", "b/d/e", "b/e", "b#", "bc"].map(repo_path)
3019 );
3020 assert!(file_states.prefixed(repo_path("a")).is_empty());
3021 assert_eq!(
3022 file_states.prefixed(repo_path("aa")).paths().collect_vec(),
3023 ["aa"].map(repo_path)
3024 );
3025 assert_eq!(
3026 file_states.prefixed(repo_path("b")).paths().collect_vec(),
3027 ["b/c", "b/d/e", "b/e"].map(repo_path)
3028 );
3029 assert_eq!(
3030 file_states.prefixed(repo_path("b/d")).paths().collect_vec(),
3031 ["b/d/e"].map(repo_path)
3032 );
3033 assert_eq!(
3034 file_states.prefixed(repo_path("b#")).paths().collect_vec(),
3035 ["b#"].map(repo_path)
3036 );
3037 assert_eq!(
3038 file_states.prefixed(repo_path("bc")).paths().collect_vec(),
3039 ["bc"].map(repo_path)
3040 );
3041 assert!(file_states.prefixed(repo_path("z")).is_empty());
3042
3043 assert!(!file_states.contains_path(repo_path("a")));
3044 assert!(file_states.contains_path(repo_path("aa")));
3045 assert!(file_states.contains_path(repo_path("b/d/e")));
3046 assert!(!file_states.contains_path(repo_path("b/d")));
3047 assert!(file_states.contains_path(repo_path("b#")));
3048 assert!(file_states.contains_path(repo_path("bc")));
3049 assert!(!file_states.contains_path(repo_path("z")));
3050
3051 assert_eq!(file_states.get(repo_path("a")), None);
3052 assert_eq!(file_states.get(repo_path("aa")), Some(new_state(0)));
3053 assert_eq!(file_states.get(repo_path("b/d/e")), Some(new_state(2)));
3054 assert_eq!(file_states.get(repo_path("bc")), Some(new_state(5)));
3055 assert_eq!(file_states.get(repo_path("z")), None);
3056 }
3057
3058 #[test]
3059 fn test_file_states_lookup_at() {
3060 let new_proto_entry = |path: &str, size| {
3061 file_state_entry_to_proto(repo_path(path).to_owned(), &new_state(size))
3062 };
3063 let data = vec![
3064 new_proto_entry("b/c", 0),
3065 new_proto_entry("b/d/e", 1),
3066 new_proto_entry("b/d#", 2), new_proto_entry("b/e", 3),
3068 new_proto_entry("b#", 4), ];
3070 let file_states = FileStates::from_sorted(&data);
3071
3072 assert_eq!(
3074 file_states.get_at(RepoPath::root(), repo_path_component("b")),
3075 None
3076 );
3077 assert_eq!(
3078 file_states.get_at(RepoPath::root(), repo_path_component("b#")),
3079 Some(new_state(4))
3080 );
3081
3082 let prefixed_states = file_states.prefixed_at(RepoPath::root(), repo_path_component("b"));
3084 assert_eq!(
3085 prefixed_states.paths().collect_vec(),
3086 ["b/c", "b/d/e", "b/d#", "b/e"].map(repo_path)
3087 );
3088 assert_eq!(
3089 prefixed_states.get_at(repo_path("b"), repo_path_component("c")),
3090 Some(new_state(0))
3091 );
3092 assert_eq!(
3093 prefixed_states.get_at(repo_path("b"), repo_path_component("d")),
3094 None
3095 );
3096 assert_eq!(
3097 prefixed_states.get_at(repo_path("b"), repo_path_component("d#")),
3098 Some(new_state(2))
3099 );
3100
3101 let prefixed_states = prefixed_states.prefixed_at(repo_path("b"), repo_path_component("d"));
3103 assert_eq!(
3104 prefixed_states.paths().collect_vec(),
3105 ["b/d/e"].map(repo_path)
3106 );
3107 assert_eq!(
3108 prefixed_states.get_at(repo_path("b/d"), repo_path_component("e")),
3109 Some(new_state(1))
3110 );
3111 assert_eq!(
3112 prefixed_states.get_at(repo_path("b/d"), repo_path_component("#")),
3113 None
3114 );
3115
3116 let prefixed_states = file_states.prefixed_at(RepoPath::root(), repo_path_component("b#"));
3118 assert_eq!(prefixed_states.paths().collect_vec(), ["b#"].map(repo_path));
3119 assert_eq!(
3120 prefixed_states.get_at(repo_path("b#"), repo_path_component("#")),
3121 None
3122 );
3123 }
3124
3125 #[test]
3126 fn test_system_time_to_millis() {
3127 let epoch = SystemTime::UNIX_EPOCH;
3128 assert_eq!(system_time_to_millis(epoch), Some(MillisSinceEpoch(0)));
3129 if let Some(time) = epoch.checked_add(Duration::from_millis(1)) {
3130 assert_eq!(system_time_to_millis(time), Some(MillisSinceEpoch(1)));
3131 }
3132 if let Some(time) = epoch.checked_sub(Duration::from_millis(1)) {
3133 assert_eq!(system_time_to_millis(time), Some(MillisSinceEpoch(-1)));
3134 }
3135 if let Some(time) = epoch.checked_add(Duration::from_millis(i64::MAX as u64)) {
3136 assert_eq!(
3137 system_time_to_millis(time),
3138 Some(MillisSinceEpoch(i64::MAX))
3139 );
3140 }
3141 if let Some(time) = epoch.checked_sub(Duration::from_millis(i64::MAX as u64)) {
3142 assert_eq!(
3143 system_time_to_millis(time),
3144 Some(MillisSinceEpoch(-i64::MAX))
3145 );
3146 }
3147 if let Some(time) = epoch.checked_sub(Duration::from_millis(i64::MAX as u64 + 1)) {
3148 assert_eq!(system_time_to_millis(time), None);
3150 }
3151 }
3152}