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