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