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),
743 Err(err) => Err(CheckoutError::Other {
744 message: format!("Failed to remove file {}", disk_path.display()),
745 err: err.into(),
746 }),
747 }
748}
749
750fn remove_old_submodule_dir(disk_path: &Path) -> Result<bool, CheckoutError> {
757 match fs::remove_dir(disk_path) {
758 Ok(()) => Ok(true),
759 Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(false),
760 Err(err) if err.kind() == io::ErrorKind::DirectoryNotEmpty => Ok(false),
761 Err(err) => Err(CheckoutError::Other {
762 message: format!(
763 "Failed to remove submodule directory {}",
764 disk_path.display()
765 ),
766 err: err.into(),
767 }),
768 }
769}
770
771fn can_create_new_file(disk_path: &Path) -> Result<bool, CheckoutError> {
781 let new_file = match OpenOptions::new()
786 .write(true)
787 .create_new(true) .open(disk_path)
789 {
790 Ok(file) => Some(file),
791 Err(err) if err.kind() == io::ErrorKind::AlreadyExists => None,
792 Err(_) => match disk_path.symlink_metadata() {
794 Ok(_) => None,
795 Err(err) => {
796 return Err(CheckoutError::Other {
797 message: format!("Failed to stat {}", disk_path.display()),
798 err: err.into(),
799 });
800 }
801 },
802 };
803
804 let new_file_created = new_file.is_some();
805
806 if let Some(new_file) = new_file {
807 reject_reserved_existing_file(new_file, disk_path).inspect_err(|_| {
808 fs::remove_file(disk_path).ok();
810 })?;
811
812 fs::remove_file(disk_path).map_err(|err| CheckoutError::Other {
813 message: format!("Failed to remove temporary file {}", disk_path.display()),
814 err: err.into(),
815 })?;
816 } else {
817 reject_reserved_existing_path(disk_path)?;
818 }
819 Ok(new_file_created)
820}
821
822const RESERVED_DIR_NAMES: &[&str] = &[".git", ".jj"];
823
824fn file_identity_from_symlink_path(disk_path: &Path) -> io::Result<Option<FileIdentity>> {
825 match FileIdentity::from_symlink_path(disk_path) {
826 Ok(identity) => Ok(Some(identity)),
827 Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(None),
828 Err(err) => Err(err),
829 }
830}
831
832fn reject_reserved_existing_file(file: File, disk_path: &Path) -> Result<(), CheckoutError> {
838 let file_identity = FileIdentity::from_file(file).map_err(|err| CheckoutError::Other {
841 message: format!("Failed to validate path {}", disk_path.display()),
842 err: err.into(),
843 })?;
844
845 reject_reserved_existing_file_identity(file_identity, disk_path)
846}
847
848fn reject_reserved_existing_path(disk_path: &Path) -> Result<(), CheckoutError> {
858 let Some(disk_identity) =
859 file_identity_from_symlink_path(disk_path).map_err(|err| CheckoutError::Other {
860 message: format!("Failed to validate path {}", disk_path.display()),
861 err: err.into(),
862 })?
863 else {
864 return Ok(());
868 };
869
870 reject_reserved_existing_file_identity(disk_identity, disk_path)
871}
872
873fn reject_reserved_existing_file_identity(
884 disk_identity: FileIdentity,
885 disk_path: &Path,
886) -> Result<(), CheckoutError> {
887 let parent_dir_path = disk_path.parent().expect("content path shouldn't be root");
888 for name in RESERVED_DIR_NAMES {
889 let reserved_path = parent_dir_path.join(name);
890
891 let Some(reserved_identity) =
892 file_identity_from_symlink_path(&reserved_path).map_err(|err| {
893 CheckoutError::Other {
894 message: format!("Failed to validate path {}", disk_path.display()),
895 err: err.into(),
896 }
897 })?
898 else {
899 continue;
903 };
904
905 if disk_identity == reserved_identity {
906 return Err(CheckoutError::ReservedPathComponent {
907 path: disk_path.to_owned(),
908 name,
909 });
910 }
911 }
912
913 Ok(())
914}
915
916#[derive(Debug, Error)]
917#[error("Out-of-range file modification time")]
918struct MtimeOutOfRange;
919
920fn mtime_from_metadata(metadata: &Metadata) -> Result<MillisSinceEpoch, MtimeOutOfRange> {
921 let time = metadata
922 .modified()
923 .expect("File mtime not supported on this platform?");
924 system_time_to_millis(time).ok_or(MtimeOutOfRange)
925}
926
927fn system_time_to_millis(time: SystemTime) -> Option<MillisSinceEpoch> {
928 let millis = match time.duration_since(SystemTime::UNIX_EPOCH) {
929 Ok(duration) => i64::try_from(duration.as_millis()).ok()?,
930 Err(err) => -i64::try_from(err.duration().as_millis()).ok()?,
931 };
932 Some(MillisSinceEpoch(millis))
933}
934
935fn file_state(metadata: &Metadata) -> Result<Option<FileState>, MtimeOutOfRange> {
937 let metadata_file_type = metadata.file_type();
938 let file_type = if metadata_file_type.is_dir() {
939 None
940 } else if metadata_file_type.is_symlink() {
941 Some(FileType::Symlink)
942 } else if metadata_file_type.is_file() {
943 let exec_bit = ExecBit::new_from_disk(metadata);
944 Some(FileType::Normal { exec_bit })
945 } else {
946 None
947 };
948 if let Some(file_type) = file_type {
949 Ok(Some(FileState {
950 file_type,
951 mtime: mtime_from_metadata(metadata)?,
952 size: metadata.len(),
953 materialized_conflict_data: None,
954 }))
955 } else {
956 Ok(None)
957 }
958}
959
960struct FsmonitorMatcher {
961 matcher: Option<Box<dyn Matcher>>,
962 watchman_clock: Option<crate::protos::local_working_copy::WatchmanClock>,
963 should_update_watchman_clock: bool,
964}
965
966#[derive(Clone, Debug)]
968pub struct TreeStateSettings {
969 pub conflict_marker_style: ConflictMarkerStyle,
972 pub eol_conversion_mode: EolConversionMode,
976 pub exec_change_setting: ExecChangeSetting,
978 pub fsmonitor_settings: FsmonitorSettings,
980}
981
982impl TreeStateSettings {
983 pub fn try_from_user_settings(user_settings: &UserSettings) -> Result<Self, ConfigGetError> {
985 Ok(Self {
986 conflict_marker_style: user_settings.get("ui.conflict-marker-style")?,
987 eol_conversion_mode: EolConversionMode::try_from_settings(user_settings)?,
988 exec_change_setting: user_settings.get("working-copy.exec-bit-change")?,
989 fsmonitor_settings: FsmonitorSettings::from_settings(user_settings)?,
990 })
991 }
992}
993
994pub struct TreeState {
995 store: Arc<Store>,
996 working_copy_path: PathBuf,
997 state_path: PathBuf,
998 tree: MergedTree,
999 file_states: FileStatesMap,
1000 sparse_patterns: Vec<RepoPathBuf>,
1002 own_mtime: MillisSinceEpoch,
1003 symlink_support: bool,
1004
1005 watchman_clock: Option<crate::protos::local_working_copy::WatchmanClock>,
1009
1010 conflict_marker_style: ConflictMarkerStyle,
1011 exec_policy: ExecChangePolicy,
1012 fsmonitor_settings: FsmonitorSettings,
1013 target_eol_strategy: TargetEolStrategy,
1014}
1015
1016#[derive(Debug, Error)]
1017pub enum TreeStateError {
1018 #[error("Reading tree state from {path}")]
1019 ReadTreeState { path: PathBuf, source: io::Error },
1020 #[error("Decoding tree state from {path}")]
1021 DecodeTreeState {
1022 path: PathBuf,
1023 source: prost::DecodeError,
1024 },
1025 #[error("Writing tree state to temporary file {path}")]
1026 WriteTreeState { path: PathBuf, source: io::Error },
1027 #[error("Persisting tree state to file {path}")]
1028 PersistTreeState { path: PathBuf, source: io::Error },
1029 #[error("Filesystem monitor error")]
1030 Fsmonitor(#[source] Box<dyn Error + Send + Sync>),
1031}
1032
1033impl TreeState {
1034 pub fn working_copy_path(&self) -> &Path {
1035 &self.working_copy_path
1036 }
1037
1038 pub fn current_tree(&self) -> &MergedTree {
1039 &self.tree
1040 }
1041
1042 pub fn file_states(&self) -> FileStates<'_> {
1043 self.file_states.all()
1044 }
1045
1046 pub fn sparse_patterns(&self) -> &Vec<RepoPathBuf> {
1047 &self.sparse_patterns
1048 }
1049
1050 fn sparse_matcher(&self) -> Box<dyn Matcher> {
1051 Box::new(PrefixMatcher::new(&self.sparse_patterns))
1052 }
1053
1054 pub fn init(
1055 store: Arc<Store>,
1056 working_copy_path: PathBuf,
1057 state_path: PathBuf,
1058 tree_state_settings: &TreeStateSettings,
1059 ) -> Result<Self, TreeStateError> {
1060 let mut wc = Self::empty(store, working_copy_path, state_path, tree_state_settings);
1061 wc.save()?;
1062 Ok(wc)
1063 }
1064
1065 pub fn init_without_saving(
1070 store: Arc<Store>,
1071 working_copy_path: PathBuf,
1072 state_path: PathBuf,
1073 tree_state_settings: &TreeStateSettings,
1074 ) -> Self {
1075 Self::empty(store, working_copy_path, state_path, tree_state_settings)
1076 }
1077
1078 fn empty(
1079 store: Arc<Store>,
1080 working_copy_path: PathBuf,
1081 state_path: PathBuf,
1082 TreeStateSettings {
1083 conflict_marker_style,
1084 eol_conversion_mode,
1085 exec_change_setting,
1086 fsmonitor_settings,
1087 }: &TreeStateSettings,
1088 ) -> Self {
1089 let exec_policy = ExecChangePolicy::new(*exec_change_setting, &state_path);
1090 Self {
1091 store: store.clone(),
1092 working_copy_path,
1093 state_path,
1094 tree: store.empty_merged_tree(),
1095 file_states: FileStatesMap::new(),
1096 sparse_patterns: vec![RepoPathBuf::root()],
1097 own_mtime: MillisSinceEpoch(0),
1098 symlink_support: check_symlink_support().unwrap_or(false),
1099 watchman_clock: None,
1100 conflict_marker_style: *conflict_marker_style,
1101 exec_policy,
1102 fsmonitor_settings: fsmonitor_settings.clone(),
1103 target_eol_strategy: TargetEolStrategy::new(*eol_conversion_mode),
1104 }
1105 }
1106
1107 pub fn load(
1108 store: Arc<Store>,
1109 working_copy_path: PathBuf,
1110 state_path: PathBuf,
1111 tree_state_settings: &TreeStateSettings,
1112 ) -> Result<Self, TreeStateError> {
1113 let tree_state_path = state_path.join("tree_state");
1114 let file = match File::open(&tree_state_path) {
1115 Err(err) if err.kind() == io::ErrorKind::NotFound => {
1116 return Self::init(store, working_copy_path, state_path, tree_state_settings);
1117 }
1118 Err(err) => {
1119 return Err(TreeStateError::ReadTreeState {
1120 path: tree_state_path,
1121 source: err,
1122 });
1123 }
1124 Ok(file) => file,
1125 };
1126
1127 let mut wc = Self::empty(store, working_copy_path, state_path, tree_state_settings);
1128 wc.read(&tree_state_path, file)?;
1129 Ok(wc)
1130 }
1131
1132 fn update_own_mtime(&mut self) {
1133 if let Ok(metadata) = self.state_path.join("tree_state").symlink_metadata()
1134 && let Ok(mtime) = mtime_from_metadata(&metadata)
1135 {
1136 self.own_mtime = mtime;
1137 } else {
1138 self.own_mtime = MillisSinceEpoch(0);
1139 }
1140 }
1141
1142 fn read(&mut self, tree_state_path: &Path, mut file: File) -> Result<(), TreeStateError> {
1143 self.update_own_mtime();
1144 let mut buf = Vec::new();
1145 file.read_to_end(&mut buf)
1146 .map_err(|err| TreeStateError::ReadTreeState {
1147 path: tree_state_path.to_owned(),
1148 source: err,
1149 })?;
1150 let proto = crate::protos::local_working_copy::TreeState::decode(&*buf).map_err(|err| {
1151 TreeStateError::DecodeTreeState {
1152 path: tree_state_path.to_owned(),
1153 source: err,
1154 }
1155 })?;
1156 #[expect(deprecated)]
1157 if proto.tree_ids.is_empty() {
1158 self.tree = MergedTree::resolved(
1159 self.store.clone(),
1160 TreeId::new(proto.legacy_tree_id.clone()),
1161 );
1162 } else {
1163 let tree_ids_builder: MergeBuilder<TreeId> = proto
1164 .tree_ids
1165 .iter()
1166 .map(|id| TreeId::new(id.clone()))
1167 .collect();
1168 self.tree = MergedTree::new(
1169 self.store.clone(),
1170 tree_ids_builder.build(),
1171 ConflictLabels::from_vec(proto.conflict_labels),
1172 );
1173 }
1174 self.file_states =
1175 FileStatesMap::from_proto(proto.file_states, proto.is_file_states_sorted);
1176 self.sparse_patterns = sparse_patterns_from_proto(proto.sparse_patterns.as_ref());
1177 self.watchman_clock = proto.watchman_clock;
1178 Ok(())
1179 }
1180
1181 #[expect(clippy::assigning_clones, clippy::field_reassign_with_default)]
1182 pub fn save(&mut self) -> Result<(), TreeStateError> {
1183 let mut proto: crate::protos::local_working_copy::TreeState = Default::default();
1184 proto.tree_ids = self
1185 .tree
1186 .tree_ids()
1187 .iter()
1188 .map(|id| id.to_bytes())
1189 .collect();
1190 proto.conflict_labels = self.tree.labels().as_slice().to_owned();
1191 proto.file_states = self.file_states.data.clone();
1192 proto.is_file_states_sorted = true;
1194 let mut sparse_patterns = crate::protos::local_working_copy::SparsePatterns::default();
1195 for path in &self.sparse_patterns {
1196 sparse_patterns
1197 .prefixes
1198 .push(path.as_internal_file_string().to_owned());
1199 }
1200 proto.sparse_patterns = Some(sparse_patterns);
1201 proto.watchman_clock = self.watchman_clock.clone();
1202
1203 let wrap_write_err = |source| TreeStateError::WriteTreeState {
1204 path: self.state_path.clone(),
1205 source,
1206 };
1207 let mut temp_file = NamedTempFile::new_in(&self.state_path).map_err(wrap_write_err)?;
1208 temp_file
1209 .as_file_mut()
1210 .write_all(&proto.encode_to_vec())
1211 .map_err(wrap_write_err)?;
1212 self.update_own_mtime();
1215 let target_path = self.state_path.join("tree_state");
1218 persist_temp_file(temp_file, &target_path).map_err(|source| {
1219 TreeStateError::PersistTreeState {
1220 path: target_path.clone(),
1221 source,
1222 }
1223 })?;
1224 Ok(())
1225 }
1226
1227 fn reset_watchman(&mut self) {
1228 self.watchman_clock.take();
1229 }
1230
1231 #[cfg(feature = "watchman")]
1232 #[instrument(skip(self))]
1233 pub async fn query_watchman(
1234 &self,
1235 config: &WatchmanConfig,
1236 ) -> Result<(watchman::Clock, Option<Vec<PathBuf>>), TreeStateError> {
1237 let previous_clock = self.watchman_clock.clone().map(watchman::Clock::from);
1238
1239 let tokio_fn = async || {
1240 let fsmonitor = watchman::Fsmonitor::init(&self.working_copy_path, config)
1241 .await
1242 .map_err(|err| TreeStateError::Fsmonitor(Box::new(err)))?;
1243 fsmonitor
1244 .query_changed_files(previous_clock)
1245 .await
1246 .map_err(|err| TreeStateError::Fsmonitor(Box::new(err)))
1247 };
1248
1249 match tokio::runtime::Handle::try_current() {
1250 Ok(_handle) => tokio_fn().await,
1251 Err(_) => {
1252 let runtime = tokio::runtime::Builder::new_current_thread()
1253 .enable_all()
1254 .build()
1255 .map_err(|err| TreeStateError::Fsmonitor(Box::new(err)))?;
1256 runtime.block_on(tokio_fn())
1257 }
1258 }
1259 }
1260
1261 #[cfg(feature = "watchman")]
1262 #[instrument(skip(self))]
1263 pub async fn is_watchman_trigger_registered(
1264 &self,
1265 config: &WatchmanConfig,
1266 ) -> Result<bool, TreeStateError> {
1267 let tokio_fn = async || {
1268 let fsmonitor = watchman::Fsmonitor::init(&self.working_copy_path, config)
1269 .await
1270 .map_err(|err| TreeStateError::Fsmonitor(Box::new(err)))?;
1271 fsmonitor
1272 .is_trigger_registered()
1273 .await
1274 .map_err(|err| TreeStateError::Fsmonitor(Box::new(err)))
1275 };
1276
1277 match tokio::runtime::Handle::try_current() {
1278 Ok(_handle) => tokio_fn().await,
1279 Err(_) => {
1280 let runtime = tokio::runtime::Builder::new_current_thread()
1281 .enable_all()
1282 .build()
1283 .map_err(|err| TreeStateError::Fsmonitor(Box::new(err)))?;
1284 runtime.block_on(tokio_fn())
1285 }
1286 }
1287 }
1288}
1289
1290impl TreeState {
1292 #[instrument(skip_all)]
1295 pub async fn snapshot(
1296 &mut self,
1297 options: &SnapshotOptions<'_>,
1298 ) -> Result<(bool, SnapshotStats), SnapshotError> {
1299 let SnapshotOptions {
1300 base_ignores,
1301 progress,
1302 start_tracking_matcher,
1303 force_tracking_matcher,
1304 max_new_file_size,
1305 } = options;
1306
1307 let sparse_matcher = self.sparse_matcher();
1308
1309 let mut is_dirty = false;
1310 let FsmonitorMatcher {
1311 matcher: fsmonitor_matcher,
1312 watchman_clock,
1313 should_update_watchman_clock,
1314 } = self
1315 .make_fsmonitor_matcher(&self.fsmonitor_settings)
1316 .await?;
1317 let fsmonitor_matcher = match fsmonitor_matcher.as_ref() {
1318 None => &EverythingMatcher,
1319 Some(fsmonitor_matcher) => fsmonitor_matcher.as_ref(),
1320 };
1321
1322 let matcher = IntersectionMatcher::new(
1323 sparse_matcher.as_ref(),
1324 UnionMatcher::new(fsmonitor_matcher, force_tracking_matcher),
1325 );
1326 if matcher.visit(RepoPath::root()).is_nothing() {
1327 if should_update_watchman_clock {
1329 is_dirty |= self.watchman_clock != watchman_clock;
1330 self.watchman_clock = watchman_clock;
1331 }
1332 return Ok((is_dirty, SnapshotStats::default()));
1333 }
1334
1335 let (tree_entries_tx, tree_entries_rx) = channel();
1336 let (file_states_tx, file_states_rx) = channel();
1337 let (untracked_paths_tx, untracked_paths_rx) = channel();
1338 let (invalid_utf8_paths_tx, invalid_utf8_paths_rx) = channel();
1339 let (deleted_files_tx, deleted_files_rx) = channel();
1340
1341 trace_span!("traverse filesystem").in_scope(|| -> Result<(), SnapshotError> {
1342 let snapshotter = FileSnapshotter {
1343 tree_state: self,
1344 current_tree: &self.tree,
1345 matcher: &matcher,
1346 sparse_matcher: sparse_matcher.as_ref(),
1347 start_tracking_matcher,
1348 force_tracking_matcher,
1349 tree_entries_tx,
1351 file_states_tx,
1352 untracked_paths_tx,
1353 invalid_utf8_paths_tx,
1354 deleted_files_tx,
1355 error: OnceLock::new(),
1356 progress: *progress,
1357 max_new_file_size: *max_new_file_size,
1358 };
1359 let directory_to_visit = DirectoryToVisit {
1360 dir: RepoPathBuf::root(),
1361 disk_dir: self.working_copy_path.clone(),
1362 git_ignore: base_ignores.clone(),
1363 file_states: self.file_states.all(),
1364 };
1365 rayon::scope(|scope| {
1367 snapshotter.spawn_ok(scope, |scope| {
1368 snapshotter.visit_directory(directory_to_visit, scope)
1369 });
1370 });
1371 snapshotter.into_result()
1372 })?;
1373
1374 let stats = SnapshotStats {
1375 untracked_paths: untracked_paths_rx.into_iter().collect(),
1376 invalid_utf8_paths: invalid_utf8_paths_rx.into_iter().collect(),
1377 };
1378 let mut tree_builder = MergedTreeBuilder::new(self.tree.clone());
1379 trace_span!("process tree entries").in_scope(|| {
1380 for (path, tree_values) in &tree_entries_rx {
1381 tree_builder.set_or_remove(path, tree_values);
1382 }
1383 });
1384 let deleted_files = trace_span!("process deleted tree entries").in_scope(|| {
1385 let deleted_files = HashSet::from_iter(deleted_files_rx);
1386 is_dirty |= !deleted_files.is_empty();
1387 for file in &deleted_files {
1388 tree_builder.set_or_remove(file.clone(), Merge::absent());
1389 }
1390 deleted_files
1391 });
1392 trace_span!("process file states").in_scope(|| {
1393 let changed_file_states = file_states_rx
1394 .iter()
1395 .sorted_unstable_by(|(path1, _), (path2, _)| path1.cmp(path2))
1396 .collect_vec();
1397 is_dirty |= !changed_file_states.is_empty();
1398 self.file_states
1399 .merge_in(changed_file_states, &deleted_files);
1400 });
1401 trace_span!("write tree")
1402 .in_scope(async || -> Result<(), BackendError> {
1403 let new_tree = tree_builder.write_tree().await?;
1404 is_dirty |= new_tree.tree_ids_and_labels() != self.tree.tree_ids_and_labels();
1405 self.tree = new_tree.clone();
1406 Ok(())
1407 })
1408 .await?;
1409 if cfg!(debug_assertions) {
1410 let tree_paths: HashSet<_> = self
1411 .tree
1412 .entries_matching(sparse_matcher.as_ref())
1413 .filter_map(|(path, result)| result.is_ok().then_some(path))
1414 .collect();
1415 let file_states = self.file_states.all();
1416 let state_paths: HashSet<_> = file_states.paths().map(|path| path.to_owned()).collect();
1417 assert_eq!(state_paths, tree_paths);
1418 }
1419 if should_update_watchman_clock {
1425 if (stats.untracked_paths.is_empty() && stats.invalid_utf8_paths.is_empty())
1426 || watchman_clock.is_none()
1427 {
1428 is_dirty |= self.watchman_clock != watchman_clock;
1429 self.watchman_clock = watchman_clock;
1430 } else {
1431 tracing::info!("not updating watchman clock because there are untracked files");
1432 }
1433 }
1434 Ok((is_dirty, stats))
1435 }
1436
1437 #[instrument(skip_all)]
1438 async fn make_fsmonitor_matcher(
1439 &self,
1440 fsmonitor_settings: &FsmonitorSettings,
1441 ) -> Result<FsmonitorMatcher, SnapshotError> {
1442 let (watchman_clock, changed_files) = match fsmonitor_settings {
1443 FsmonitorSettings::None => (None, None),
1444 FsmonitorSettings::Test { changed_files } => (None, Some(changed_files.clone())),
1445 #[cfg(feature = "watchman")]
1446 FsmonitorSettings::Watchman(config) => match self.query_watchman(config).await {
1447 Ok((watchman_clock, changed_files)) => (Some(watchman_clock.into()), changed_files),
1448 Err(err) => {
1449 tracing::warn!(?err, "Failed to query filesystem monitor");
1450 (None, None)
1451 }
1452 },
1453 #[cfg(not(feature = "watchman"))]
1454 FsmonitorSettings::Watchman(_) => {
1455 return Err(SnapshotError::Other {
1456 message: "Failed to query the filesystem monitor".to_string(),
1457 err: "Cannot query Watchman because jj was not compiled with the `watchman` \
1458 feature (consider disabling `fsmonitor.backend`)"
1459 .into(),
1460 });
1461 }
1462 };
1463 let should_update_watchman_clock = changed_files
1468 .as_ref()
1469 .is_none_or(|changed_files| !changed_files.is_empty());
1470 let matcher: Option<Box<dyn Matcher>> = match changed_files {
1471 None => None,
1472 Some(changed_files) => {
1473 let (repo_paths, gitignore_prefixes) = trace_span!("processing fsmonitor paths")
1474 .in_scope(|| {
1475 let repo_paths = changed_files
1476 .iter()
1477 .filter_map(|path| RepoPathBuf::from_relative_path(path).ok())
1478 .collect_vec();
1479 let gitignore_prefixes = repo_paths
1482 .iter()
1483 .filter_map(|repo_path| {
1484 let (parent, basename) = repo_path.split()?;
1485 (basename.as_internal_str() == ".gitignore")
1486 .then(|| parent.to_owned())
1487 })
1488 .collect_vec();
1489 (repo_paths, gitignore_prefixes)
1490 });
1491
1492 let matcher: Box<dyn Matcher> = if gitignore_prefixes.is_empty() {
1493 Box::new(FilesMatcher::new(repo_paths))
1494 } else {
1495 Box::new(UnionMatcher::new(
1496 FilesMatcher::new(repo_paths),
1497 PrefixMatcher::new(gitignore_prefixes),
1498 ))
1499 };
1500
1501 Some(matcher)
1502 }
1503 };
1504 Ok(FsmonitorMatcher {
1505 matcher,
1506 watchman_clock,
1507 should_update_watchman_clock,
1508 })
1509 }
1510}
1511
1512struct DirectoryToVisit<'a> {
1513 dir: RepoPathBuf,
1514 disk_dir: PathBuf,
1515 git_ignore: Arc<GitIgnoreFile>,
1516 file_states: FileStates<'a>,
1517}
1518
1519#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1520enum PresentDirEntryKind {
1521 Dir,
1522 File,
1523}
1524
1525#[derive(Clone, Debug)]
1526struct PresentDirEntries {
1527 dirs: HashSet<String>,
1528 files: HashSet<String>,
1529}
1530
1531struct FileSnapshotter<'a> {
1533 tree_state: &'a TreeState,
1534 current_tree: &'a MergedTree,
1535 matcher: &'a dyn Matcher,
1536 sparse_matcher: &'a dyn Matcher,
1537 start_tracking_matcher: &'a dyn Matcher,
1538 force_tracking_matcher: &'a dyn Matcher,
1539 tree_entries_tx: Sender<(RepoPathBuf, MergedTreeValue)>,
1540 file_states_tx: Sender<(RepoPathBuf, FileState)>,
1541 untracked_paths_tx: Sender<(RepoPathBuf, UntrackedReason)>,
1542 invalid_utf8_paths_tx: Sender<(RepoPathBuf, OsString)>,
1543 deleted_files_tx: Sender<RepoPathBuf>,
1544 error: OnceLock<SnapshotError>,
1545 progress: Option<&'a SnapshotProgress<'a>>,
1546 max_new_file_size: u64,
1547}
1548
1549impl FileSnapshotter<'_> {
1550 fn spawn_ok<'scope, F>(&'scope self, scope: &rayon::Scope<'scope>, body: F)
1551 where
1552 F: FnOnce(&rayon::Scope<'scope>) -> Result<(), SnapshotError> + Send + 'scope,
1553 {
1554 scope.spawn(|scope| {
1555 if self.error.get().is_some() {
1556 return;
1557 }
1558 match body(scope) {
1559 Ok(()) => {}
1560 Err(err) => self.error.set(err).unwrap_or(()),
1561 }
1562 });
1563 }
1564
1565 fn into_result(self) -> Result<(), SnapshotError> {
1567 match self.error.into_inner() {
1568 Some(err) => Err(err),
1569 None => Ok(()),
1570 }
1571 }
1572
1573 fn visit_directory<'scope>(
1576 &'scope self,
1577 directory_to_visit: DirectoryToVisit<'scope>,
1578 scope: &rayon::Scope<'scope>,
1579 ) -> Result<(), SnapshotError> {
1580 let DirectoryToVisit {
1581 dir,
1582 disk_dir,
1583 mut git_ignore,
1584 file_states,
1585 } = directory_to_visit;
1586
1587 let git_ignore_file_name = RepoPathComponent::new(".gitignore").expect("static string");
1588 if let Some(git_ignore_contents) = self
1589 .read_gitignore(&disk_dir, &dir, git_ignore_file_name)
1590 .block_on()?
1591 {
1592 let git_ignore_disk_path = disk_dir.join(git_ignore_file_name.as_internal_str());
1593 git_ignore = git_ignore.chain(&dir, &git_ignore_disk_path, &git_ignore_contents)?;
1594 }
1595 let dir_entries: Vec<_> = disk_dir
1596 .read_dir()
1597 .and_then(|entries| entries.try_collect())
1598 .map_err(|err| SnapshotError::Other {
1599 message: format!("Failed to read directory {}", disk_dir.display()),
1600 err: err.into(),
1601 })?;
1602 let (dirs, files) = dir_entries
1603 .into_par_iter()
1604 .with_min_len(100)
1607 .filter_map(|entry| {
1608 self.process_dir_entry(&dir, &git_ignore, file_states, &entry, scope)
1609 .block_on()
1610 .transpose()
1611 })
1612 .map(|item| match item {
1613 Ok((PresentDirEntryKind::Dir, name)) => Ok(Either::Left(name)),
1614 Ok((PresentDirEntryKind::File, name)) => Ok(Either::Right(name)),
1615 Err(err) => Err(err),
1616 })
1617 .collect::<Result<_, _>>()?;
1618 let present_entries = PresentDirEntries { dirs, files };
1619 self.emit_deleted_files(&dir, file_states, &present_entries);
1620 Ok(())
1621 }
1622
1623 async fn read_gitignore(
1639 &self,
1640 disk_dir: &Path,
1641 repo_dir: &RepoPath,
1642 file_name: &RepoPathComponent,
1643 ) -> Result<Option<Vec<u8>>, SnapshotError> {
1644 let ignore_disk_path = disk_dir.join(file_name.as_internal_str());
1645 match ignore_disk_path.symlink_metadata() {
1646 Ok(symlink_metadata) => {
1647 return symlink_metadata
1651 .is_file()
1652 .then(|| {
1653 fs::read(&ignore_disk_path).map_err(|err| SnapshotError::Other {
1654 message: format!(
1655 "Failed to read ignore patterns from file {}",
1656 ignore_disk_path.display()
1657 ),
1658 err: Box::new(err),
1659 })
1660 })
1661 .transpose();
1662 }
1663 Err(err) if err.kind() == io::ErrorKind::NotFound => {
1664 }
1666 Err(err) => {
1667 return Err(SnapshotError::Other {
1668 message: format!(
1669 "Error accessing ignore patterns file {}",
1670 ignore_disk_path.display()
1671 ),
1672 err: Box::new(err),
1673 });
1674 }
1675 }
1676
1677 let ignore_fallback_repo_path = repo_dir.join(file_name);
1678 if self.sparse_matcher.matches(&ignore_fallback_repo_path) {
1679 return Ok(None);
1681 }
1682
1683 let tree_values = self
1684 .current_tree
1685 .path_value(&ignore_fallback_repo_path)
1686 .await?;
1687 let file_id = match tree_values.as_normal() {
1688 Some(TreeValue::File { id, .. }) => id,
1689 None
1690 | Some(TreeValue::Symlink(_) | TreeValue::GitSubmodule(_) | TreeValue::Tree(_)) => {
1691 return Ok(None);
1692 }
1693 };
1694 let mut buf = vec![];
1695 let mut reader = self
1696 .store()
1697 .read_file(&ignore_fallback_repo_path, file_id)
1698 .await?;
1699 reader
1700 .read_to_end(&mut buf)
1701 .await
1702 .map_err(|err| BackendError::ReadFile {
1703 path: ignore_fallback_repo_path,
1704 id: file_id.clone(),
1705 source: err.into(),
1706 })?;
1707 Ok(Some(buf))
1708 }
1709
1710 async fn process_dir_entry<'scope>(
1711 &'scope self,
1712 dir: &RepoPath,
1713 git_ignore: &Arc<GitIgnoreFile>,
1714 file_states: FileStates<'scope>,
1715 entry: &DirEntry,
1716 scope: &rayon::Scope<'scope>,
1717 ) -> Result<Option<(PresentDirEntryKind, String)>, SnapshotError> {
1718 let file_type = entry.file_type().unwrap();
1719 let file_name = entry.file_name();
1720 let name_string = match file_name.into_string() {
1721 Ok(name_string) => name_string,
1722 Err(name) => {
1723 self.invalid_utf8_paths_tx.send((dir.to_owned(), name)).ok();
1727 return Ok(None);
1728 }
1729 };
1730
1731 if RESERVED_DIR_NAMES.contains(&name_string.as_str()) {
1732 return Ok(None);
1733 }
1734 let name = RepoPathComponent::new(&name_string).unwrap();
1735 let path = dir.join(name);
1736 let maybe_current_file_state = file_states.get_at(dir, name);
1737 if let Some(file_state) = &maybe_current_file_state
1738 && file_state.file_type == FileType::GitSubmodule
1739 {
1740 return Ok(None);
1741 }
1742
1743 if file_type.is_dir() {
1744 let file_states = file_states.prefixed_at(dir, name);
1745 let disk_dir = entry.path();
1753 for &name in RESERVED_DIR_NAMES {
1754 if disk_dir.join(name).symlink_metadata().is_ok() {
1755 return Ok(None);
1756 }
1757 }
1758
1759 if git_ignore.matches_dir(&path)
1760 && self.force_tracking_matcher.visit(&path).is_nothing()
1761 {
1762 self.spawn_ok(scope, move |_| {
1768 self.visit_tracked_files(file_states).block_on()
1769 });
1770 } else if !self.matcher.visit(&path).is_nothing() {
1771 let directory_to_visit = DirectoryToVisit {
1772 dir: path,
1773 disk_dir,
1774 git_ignore: git_ignore.clone(),
1775 file_states,
1776 };
1777 self.spawn_ok(scope, |scope| {
1778 self.visit_directory(directory_to_visit, scope)
1779 });
1780 }
1781 Ok(Some((PresentDirEntryKind::Dir, name_string)))
1784 } else if self.matcher.matches(&path) {
1785 if let Some(progress) = self.progress {
1786 progress(&path);
1787 }
1788 if maybe_current_file_state.is_none()
1789 && (git_ignore.matches_file(&path) && !self.force_tracking_matcher.matches(&path))
1790 {
1791 Ok(None)
1794 } else if maybe_current_file_state.is_none()
1795 && !self.start_tracking_matcher.matches(&path)
1796 {
1797 self.untracked_paths_tx
1799 .send((path, UntrackedReason::FileNotAutoTracked))
1800 .ok();
1801 Ok(None)
1802 } else {
1803 let metadata = entry.metadata().map_err(|err| SnapshotError::Other {
1804 message: format!("Failed to stat file {}", entry.path().display()),
1805 err: err.into(),
1806 })?;
1807 if maybe_current_file_state.is_none()
1808 && (metadata.len() > self.max_new_file_size
1809 && !self.force_tracking_matcher.matches(&path))
1810 {
1811 let reason = UntrackedReason::FileTooLarge {
1813 size: metadata.len(),
1814 max_size: self.max_new_file_size,
1815 };
1816 self.untracked_paths_tx.send((path, reason)).ok();
1817 Ok(None)
1818 } else if let Some(new_file_state) = file_state(&metadata)
1819 .map_err(|err| snapshot_error_for_mtime_out_of_range(err, &entry.path()))?
1820 {
1821 self.process_present_file(
1822 path,
1823 &entry.path(),
1824 maybe_current_file_state.as_ref(),
1825 new_file_state,
1826 )
1827 .await?;
1828 Ok(Some((PresentDirEntryKind::File, name_string)))
1829 } else {
1830 Ok(None)
1832 }
1833 }
1834 } else {
1835 Ok(None)
1836 }
1837 }
1838
1839 async fn visit_tracked_files(&self, file_states: FileStates<'_>) -> Result<(), SnapshotError> {
1841 for (tracked_path, current_file_state) in file_states {
1842 if current_file_state.file_type == FileType::GitSubmodule {
1843 continue;
1844 }
1845 if !self.matcher.matches(tracked_path) {
1846 continue;
1847 }
1848 let disk_path = tracked_path.to_fs_path(&self.tree_state.working_copy_path)?;
1849 let metadata = match disk_path.symlink_metadata() {
1850 Ok(metadata) => Some(metadata),
1851 Err(err) if err.kind() == io::ErrorKind::NotFound => None,
1852 Err(err) => {
1853 return Err(SnapshotError::Other {
1854 message: format!("Failed to stat file {}", disk_path.display()),
1855 err: err.into(),
1856 });
1857 }
1858 };
1859 if let Some(metadata) = &metadata
1860 && let Some(new_file_state) = file_state(metadata)
1861 .map_err(|err| snapshot_error_for_mtime_out_of_range(err, &disk_path))?
1862 {
1863 self.process_present_file(
1864 tracked_path.to_owned(),
1865 &disk_path,
1866 Some(¤t_file_state),
1867 new_file_state,
1868 )
1869 .await?;
1870 } else {
1871 self.deleted_files_tx.send(tracked_path.to_owned()).ok();
1872 }
1873 }
1874 Ok(())
1875 }
1876
1877 async fn process_present_file(
1878 &self,
1879 path: RepoPathBuf,
1880 disk_path: &Path,
1881 maybe_current_file_state: Option<&FileState>,
1882 mut new_file_state: FileState,
1883 ) -> Result<(), SnapshotError> {
1884 let update = self
1885 .get_updated_tree_value(&path, disk_path, maybe_current_file_state, &new_file_state)
1886 .await?;
1887 if matches!(new_file_state.file_type, FileType::Normal { .. })
1889 && !update.as_ref().is_some_and(|update| update.is_resolved())
1890 {
1891 new_file_state.materialized_conflict_data =
1892 maybe_current_file_state.and_then(|state| state.materialized_conflict_data);
1893 }
1894 if let Some(tree_value) = update {
1895 self.tree_entries_tx.send((path.clone(), tree_value)).ok();
1896 }
1897 if Some(&new_file_state) != maybe_current_file_state {
1898 self.file_states_tx.send((path, new_file_state)).ok();
1899 }
1900 Ok(())
1901 }
1902
1903 fn emit_deleted_files(
1905 &self,
1906 dir: &RepoPath,
1907 file_states: FileStates<'_>,
1908 present_entries: &PresentDirEntries,
1909 ) {
1910 let file_state_chunks = file_states.iter().chunk_by(|(path, _state)| {
1911 debug_assert!(path.starts_with(dir));
1914 let slash = usize::from(!dir.is_root());
1915 let len = dir.as_internal_file_string().len() + slash;
1916 let tail = path.as_internal_file_string().get(len..).unwrap_or("");
1917 match tail.split_once('/') {
1918 Some((name, _)) => (PresentDirEntryKind::Dir, name),
1919 None => (PresentDirEntryKind::File, tail),
1920 }
1921 });
1922 file_state_chunks
1923 .into_iter()
1924 .filter(|&((kind, name), _)| match kind {
1925 PresentDirEntryKind::Dir => !present_entries.dirs.contains(name),
1926 PresentDirEntryKind::File => !present_entries.files.contains(name),
1927 })
1928 .flat_map(|(_, chunk)| chunk)
1929 .filter(|(_, state)| state.file_type != FileType::GitSubmodule)
1931 .filter(|(path, _)| self.matcher.matches(path))
1932 .try_for_each(|(path, _)| self.deleted_files_tx.send(path.to_owned()))
1933 .ok();
1934 }
1935
1936 async fn get_updated_tree_value(
1937 &self,
1938 repo_path: &RepoPath,
1939 disk_path: &Path,
1940 maybe_current_file_state: Option<&FileState>,
1941 new_file_state: &FileState,
1942 ) -> Result<Option<MergedTreeValue>, SnapshotError> {
1943 let clean = match maybe_current_file_state {
1944 None => {
1945 false
1947 }
1948 Some(current_file_state) => {
1949 new_file_state.is_clean(current_file_state)
1952 && current_file_state.mtime < self.tree_state.own_mtime
1953 }
1954 };
1955 if clean {
1956 Ok(None)
1957 } else {
1958 let current_tree_values = self.current_tree.path_value(repo_path).await?;
1959 let new_file_type = if !self.tree_state.symlink_support {
1960 let mut new_file_type = new_file_state.file_type.clone();
1961 if matches!(new_file_type, FileType::Normal { .. })
1962 && matches!(current_tree_values.as_normal(), Some(TreeValue::Symlink(_)))
1963 {
1964 new_file_type = FileType::Symlink;
1965 }
1966 new_file_type
1967 } else {
1968 new_file_state.file_type.clone()
1969 };
1970 let new_tree_values = match new_file_type {
1971 FileType::Normal { exec_bit } => {
1972 self.write_path_to_store(
1973 repo_path,
1974 disk_path,
1975 ¤t_tree_values,
1976 exec_bit,
1977 maybe_current_file_state.and_then(|state| state.materialized_conflict_data),
1978 )
1979 .await?
1980 }
1981 FileType::Symlink => {
1982 let id = self.write_symlink_to_store(repo_path, disk_path).await?;
1983 Merge::normal(TreeValue::Symlink(id))
1984 }
1985 FileType::GitSubmodule => panic!("git submodule cannot be written to store"),
1986 };
1987 if new_tree_values != current_tree_values {
1988 Ok(Some(new_tree_values))
1989 } else {
1990 Ok(None)
1991 }
1992 }
1993 }
1994
1995 fn store(&self) -> &Store {
1996 &self.tree_state.store
1997 }
1998
1999 async fn write_path_to_store(
2000 &self,
2001 repo_path: &RepoPath,
2002 disk_path: &Path,
2003 current_tree_values: &MergedTreeValue,
2004 exec_bit: ExecBit,
2005 materialized_conflict_data: Option<MaterializedConflictData>,
2006 ) -> Result<MergedTreeValue, SnapshotError> {
2007 if let Some(current_tree_value) = current_tree_values.as_resolved() {
2008 let id = self.write_file_to_store(repo_path, disk_path).await?;
2009 let executable = exec_bit.for_tree_value(self.tree_state.exec_policy, || {
2011 if let Some(TreeValue::File {
2012 id: _,
2013 executable,
2014 copy_id: _,
2015 }) = current_tree_value
2016 {
2017 Some(*executable)
2018 } else {
2019 None
2020 }
2021 });
2022 let copy_id = {
2024 if let Some(TreeValue::File {
2025 id: _,
2026 executable: _,
2027 copy_id,
2028 }) = current_tree_value
2029 {
2030 copy_id.clone()
2031 } else {
2032 CopyId::placeholder()
2033 }
2034 };
2035 Ok(Merge::normal(TreeValue::File {
2036 id,
2037 executable,
2038 copy_id,
2039 }))
2040 } else if let Some(old_file_ids) = current_tree_values.to_file_merge() {
2041 let copy_id_merge = current_tree_values.to_copy_id_merge().unwrap();
2043 let copy_id = copy_id_merge
2044 .resolve_trivial(SameChange::Accept)
2045 .cloned()
2046 .flatten()
2047 .unwrap_or_else(CopyId::placeholder);
2048 let mut contents = vec![];
2049 let file = File::open(disk_path).map_err(|err| SnapshotError::Other {
2050 message: format!("Failed to open file {}", disk_path.display()),
2051 err: err.into(),
2052 })?;
2053 self.tree_state
2054 .target_eol_strategy
2055 .convert_eol_for_snapshot(AllowStdIo::new(file))
2056 .await
2057 .map_err(|err| SnapshotError::Other {
2058 message: "Failed to convert the EOL".to_string(),
2059 err: err.into(),
2060 })?
2061 .read_to_end(&mut contents)
2062 .await
2063 .map_err(|err| SnapshotError::Other {
2064 message: "Failed to read the EOL converted contents".to_string(),
2065 err: err.into(),
2066 })?;
2067 let new_file_ids = conflicts::update_from_content(
2071 &old_file_ids,
2072 self.store(),
2073 repo_path,
2074 &contents,
2075 materialized_conflict_data.map_or(MIN_CONFLICT_MARKER_LEN, |data| {
2076 data.conflict_marker_len as usize
2077 }),
2078 )
2079 .await?;
2080 match new_file_ids.into_resolved() {
2081 Ok(file_id) => {
2082 let executable = exec_bit.for_tree_value(self.tree_state.exec_policy, || {
2084 current_tree_values
2085 .to_executable_merge()
2086 .as_ref()
2087 .and_then(conflicts::resolve_file_executable)
2088 });
2089 Ok(Merge::normal(TreeValue::File {
2090 id: file_id.unwrap(),
2091 executable,
2092 copy_id,
2093 }))
2094 }
2095 Err(new_file_ids) => {
2096 if new_file_ids != old_file_ids {
2097 Ok(current_tree_values.with_new_file_ids(&new_file_ids))
2098 } else {
2099 Ok(current_tree_values.clone())
2100 }
2101 }
2102 }
2103 } else {
2104 Ok(current_tree_values.clone())
2105 }
2106 }
2107
2108 async fn write_file_to_store(
2109 &self,
2110 path: &RepoPath,
2111 disk_path: &Path,
2112 ) -> Result<FileId, SnapshotError> {
2113 let file = File::open(disk_path).map_err(|err| SnapshotError::Other {
2114 message: format!("Failed to open file {}", disk_path.display()),
2115 err: err.into(),
2116 })?;
2117 let mut contents = self
2118 .tree_state
2119 .target_eol_strategy
2120 .convert_eol_for_snapshot(AllowStdIo::new(file))
2121 .await
2122 .map_err(|err| SnapshotError::Other {
2123 message: "Failed to convert the EOL".to_string(),
2124 err: err.into(),
2125 })?;
2126 Ok(self.store().write_file(path, &mut contents).await?)
2127 }
2128
2129 async fn write_symlink_to_store(
2130 &self,
2131 path: &RepoPath,
2132 disk_path: &Path,
2133 ) -> Result<SymlinkId, SnapshotError> {
2134 if self.tree_state.symlink_support {
2135 let target = disk_path.read_link().map_err(|err| SnapshotError::Other {
2136 message: format!("Failed to read symlink {}", disk_path.display()),
2137 err: err.into(),
2138 })?;
2139 let str_target = symlink_target_convert_to_store(&target).ok_or_else(|| {
2140 SnapshotError::InvalidUtf8SymlinkTarget {
2141 path: disk_path.to_path_buf(),
2142 }
2143 })?;
2144 Ok(self.store().write_symlink(path, &str_target).await?)
2145 } else {
2146 let target = fs::read(disk_path).map_err(|err| SnapshotError::Other {
2147 message: format!("Failed to read file {}", disk_path.display()),
2148 err: err.into(),
2149 })?;
2150 let string_target =
2151 String::from_utf8(target).map_err(|_| SnapshotError::InvalidUtf8SymlinkTarget {
2152 path: disk_path.to_path_buf(),
2153 })?;
2154 Ok(self.store().write_symlink(path, &string_target).await?)
2155 }
2156 }
2157}
2158
2159fn snapshot_error_for_mtime_out_of_range(err: MtimeOutOfRange, path: &Path) -> SnapshotError {
2160 SnapshotError::Other {
2161 message: format!("Failed to process file metadata {}", path.display()),
2162 err: err.into(),
2163 }
2164}
2165
2166impl TreeState {
2168 async fn write_file(
2169 &self,
2170 disk_path: &Path,
2171 contents: impl AsyncRead + Send + Unpin,
2172 exec_bit: ExecBit,
2173 apply_eol_conversion: bool,
2174 ) -> Result<FileState, CheckoutError> {
2175 let mut file = File::options()
2176 .write(true)
2177 .create_new(true) .open(disk_path)
2179 .map_err(|err| CheckoutError::Other {
2180 message: format!("Failed to open file {} for writing", disk_path.display()),
2181 err: err.into(),
2182 })?;
2183 let contents = if apply_eol_conversion {
2184 self.target_eol_strategy
2185 .convert_eol_for_update(contents)
2186 .await
2187 .map_err(|err| CheckoutError::Other {
2188 message: "Failed to convert the EOL for the content".to_string(),
2189 err: err.into(),
2190 })?
2191 } else {
2192 Box::new(contents)
2193 };
2194 let size = copy_async_to_sync(contents, &mut file)
2195 .await
2196 .map_err(|err| CheckoutError::Other {
2197 message: format!(
2198 "Failed to write the content to the file {}",
2199 disk_path.display()
2200 ),
2201 err: err.into(),
2202 })?;
2203 set_executable(exec_bit, disk_path)
2204 .map_err(|err| checkout_error_for_stat_error(err, disk_path))?;
2205 let metadata = file
2210 .metadata()
2211 .map_err(|err| checkout_error_for_stat_error(err, disk_path))?;
2212 FileState::for_file(exec_bit, size as u64, &metadata)
2213 .map_err(|err| checkout_error_for_mtime_out_of_range(err, disk_path))
2214 }
2215
2216 fn write_symlink(&self, disk_path: &Path, target: String) -> Result<FileState, CheckoutError> {
2217 let target = symlink_target_convert_to_disk(&target);
2218
2219 if cfg!(windows) {
2220 debug_assert_ne!(
2228 target.as_os_str().to_str().map(|path| path.contains('/')),
2229 Some(true),
2230 r#"Expect the symlink target doesn't contain "/", but got invalid symlink target: {}."#,
2231 target.display()
2232 );
2233 }
2234
2235 symlink_file(&target, disk_path).map_err(|err| CheckoutError::Other {
2239 message: format!(
2240 "Failed to create symlink from {} to {}",
2241 disk_path.display(),
2242 target.display()
2243 ),
2244 err: err.into(),
2245 })?;
2246 let metadata = disk_path
2247 .symlink_metadata()
2248 .map_err(|err| checkout_error_for_stat_error(err, disk_path))?;
2249 FileState::for_symlink(&metadata)
2250 .map_err(|err| checkout_error_for_mtime_out_of_range(err, disk_path))
2251 }
2252
2253 async fn write_conflict(
2254 &self,
2255 disk_path: &Path,
2256 contents: &[u8],
2257 exec_bit: ExecBit,
2258 ) -> Result<FileState, CheckoutError> {
2259 let contents = self
2260 .target_eol_strategy
2261 .convert_eol_for_update(contents)
2262 .await
2263 .map_err(|err| CheckoutError::Other {
2264 message: "Failed to convert the EOL when writing a merge conflict".to_string(),
2265 err: err.into(),
2266 })?;
2267 let mut file = OpenOptions::new()
2268 .write(true)
2269 .create_new(true) .open(disk_path)
2271 .map_err(|err| CheckoutError::Other {
2272 message: format!("Failed to open file {} for writing", disk_path.display()),
2273 err: err.into(),
2274 })?;
2275 let size = copy_async_to_sync(contents, &mut file)
2276 .await
2277 .map_err(|err| CheckoutError::Other {
2278 message: format!("Failed to write conflict to file {}", disk_path.display()),
2279 err: err.into(),
2280 })? as u64;
2281 set_executable(exec_bit, disk_path)
2282 .map_err(|err| checkout_error_for_stat_error(err, disk_path))?;
2283 let metadata = file
2284 .metadata()
2285 .map_err(|err| checkout_error_for_stat_error(err, disk_path))?;
2286 FileState::for_file(exec_bit, size, &metadata)
2287 .map_err(|err| checkout_error_for_mtime_out_of_range(err, disk_path))
2288 }
2289
2290 pub fn check_out(&mut self, new_tree: &MergedTree) -> Result<CheckoutStats, CheckoutError> {
2291 let old_tree = self.tree.clone();
2292 let stats = self
2293 .update(&old_tree, new_tree, self.sparse_matcher().as_ref())
2294 .block_on()?;
2295 self.tree = new_tree.clone();
2296 Ok(stats)
2297 }
2298
2299 pub fn set_sparse_patterns(
2300 &mut self,
2301 sparse_patterns: Vec<RepoPathBuf>,
2302 ) -> Result<CheckoutStats, CheckoutError> {
2303 let tree = self.tree.clone();
2304 let old_matcher = PrefixMatcher::new(&self.sparse_patterns);
2305 let new_matcher = PrefixMatcher::new(&sparse_patterns);
2306 let added_matcher = DifferenceMatcher::new(&new_matcher, &old_matcher);
2307 let removed_matcher = DifferenceMatcher::new(&old_matcher, &new_matcher);
2308 let empty_tree = self.store.empty_merged_tree();
2309 let added_stats = self.update(&empty_tree, &tree, &added_matcher).block_on()?;
2310 let removed_stats = self
2311 .update(&tree, &empty_tree, &removed_matcher)
2312 .block_on()?;
2313 self.sparse_patterns = sparse_patterns;
2314 assert_eq!(added_stats.updated_files, 0);
2315 assert_eq!(added_stats.removed_files, 0);
2316 assert_eq!(removed_stats.updated_files, 0);
2317 assert_eq!(removed_stats.added_files, 0);
2318 assert_eq!(removed_stats.skipped_files, 0);
2319 Ok(CheckoutStats {
2320 updated_files: 0,
2321 added_files: added_stats.added_files,
2322 removed_files: removed_stats.removed_files,
2323 skipped_files: added_stats.skipped_files,
2324 })
2325 }
2326
2327 async fn update(
2328 &mut self,
2329 old_tree: &MergedTree,
2330 new_tree: &MergedTree,
2331 matcher: &dyn Matcher,
2332 ) -> Result<CheckoutStats, CheckoutError> {
2333 let mut stats = CheckoutStats {
2336 updated_files: 0,
2337 added_files: 0,
2338 removed_files: 0,
2339 skipped_files: 0,
2340 };
2341 let mut changed_file_states = Vec::new();
2342 let mut deleted_files = HashSet::new();
2343 let mut prev_created_path: RepoPathBuf = RepoPathBuf::root();
2344
2345 let mut process_diff_entry = async |path: RepoPathBuf,
2346 before: MergedTreeValue,
2347 after: MaterializedTreeValue|
2348 -> Result<(), CheckoutError> {
2349 if after.is_absent() {
2350 stats.removed_files += 1;
2351 } else if before.is_absent() {
2352 stats.added_files += 1;
2353 } else {
2354 stats.updated_files += 1;
2355 }
2356
2357 if matches!(before.as_normal(), Some(TreeValue::GitSubmodule(_)))
2365 && matches!(after, MaterializedTreeValue::GitSubmodule(_))
2366 {
2367 eprintln!("ignoring git submodule at {path:?}");
2368 return Ok(());
2371 }
2372
2373 let (common_prefix, adjusted_diff_file_path) =
2378 path.split_common_prefix(&prev_created_path);
2379
2380 let disk_path = if adjusted_diff_file_path.is_root() {
2381 path.to_fs_path(self.working_copy_path())?
2396 } else {
2397 let adjusted_working_copy_path =
2398 common_prefix.to_fs_path(self.working_copy_path())?;
2399
2400 let Some(disk_path) =
2403 create_parent_dirs(&adjusted_working_copy_path, adjusted_diff_file_path)?
2404 else {
2405 changed_file_states.push((path, FileState::placeholder()));
2406 stats.skipped_files += 1;
2407 return Ok(());
2408 };
2409
2410 prev_created_path = path
2415 .parent()
2416 .map(RepoPath::to_owned)
2417 .expect("diff path has no parent");
2418
2419 disk_path
2420 };
2421
2422 let present_file_deleted = before.is_present()
2424 && if matches!(before.as_normal(), Some(TreeValue::GitSubmodule(_))) {
2425 remove_old_submodule_dir(&disk_path)?
2426 } else {
2427 remove_old_file(&disk_path)?
2428 };
2429
2430 if !present_file_deleted && !can_create_new_file(&disk_path)? {
2432 if matches!(after, MaterializedTreeValue::GitSubmodule(_)) && disk_path.is_dir() {
2433 } else if matches!(before.as_normal(), Some(TreeValue::GitSubmodule(_)))
2439 && after.is_absent()
2440 {
2441 } else {
2447 changed_file_states.push((path, FileState::placeholder()));
2448 stats.skipped_files += 1;
2449 return Ok(());
2450 }
2451 }
2452
2453 let get_prev_exec = || self.file_states().get_exec_bit(&path);
2457
2458 let file_state = match after {
2460 MaterializedTreeValue::Absent | MaterializedTreeValue::AccessDenied(_) => {
2461 prev_created_path = RepoPathBuf::root();
2465
2466 let mut parent_dir = disk_path.parent().unwrap();
2467 loop {
2468 if fs::remove_dir(parent_dir).is_err() {
2469 break;
2470 }
2471
2472 parent_dir = parent_dir.parent().unwrap();
2473 }
2474 deleted_files.insert(path);
2475 return Ok(());
2476 }
2477 MaterializedTreeValue::File(file) => {
2478 let exec_bit =
2479 ExecBit::new_from_repo(file.executable, self.exec_policy, get_prev_exec);
2480 self.write_file(&disk_path, file.reader, exec_bit, true)
2481 .await?
2482 }
2483 MaterializedTreeValue::Symlink { id: _, target } => {
2484 if self.symlink_support {
2485 self.write_symlink(&disk_path, target)?
2486 } else {
2487 self.write_file(&disk_path, target.as_bytes(), ExecBit(false), false)
2489 .await?
2490 }
2491 }
2492 MaterializedTreeValue::GitSubmodule(_) => {
2493 eprintln!("ignoring git submodule at {path:?}");
2494 match fs::create_dir(&disk_path) {
2497 Ok(()) => {}
2498 Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
2499 Err(err) => eprintln!(
2500 "warning: failed to create submodule directory {path:?}: {err}"
2501 ),
2502 }
2503 FileState::for_gitsubmodule()
2504 }
2505 MaterializedTreeValue::Tree(_) => {
2506 panic!("unexpected tree entry in diff at {path:?}");
2507 }
2508 MaterializedTreeValue::FileConflict(file) => {
2509 let conflict_marker_len =
2510 choose_materialized_conflict_marker_len(&file.contents);
2511 let options = ConflictMaterializeOptions {
2512 marker_style: self.conflict_marker_style,
2513 marker_len: Some(conflict_marker_len),
2514 merge: self.store.merge_options().clone(),
2515 };
2516 let exec_bit = ExecBit::new_from_repo(
2517 file.executable.unwrap_or(false),
2518 self.exec_policy,
2519 get_prev_exec,
2520 );
2521 let contents =
2522 materialize_merge_result_to_bytes(&file.contents, &file.labels, &options);
2523 let mut file_state =
2524 self.write_conflict(&disk_path, &contents, exec_bit).await?;
2525 file_state.materialized_conflict_data = Some(MaterializedConflictData {
2526 conflict_marker_len: conflict_marker_len.try_into().unwrap_or(u32::MAX),
2527 });
2528 file_state
2529 }
2530 MaterializedTreeValue::OtherConflict { id, labels } => {
2531 let contents = id.describe(&labels);
2534 self.write_conflict(&disk_path, contents.as_bytes(), ExecBit(false))
2536 .await?
2537 }
2538 };
2539 changed_file_states.push((path, file_state));
2540 Ok(())
2541 };
2542
2543 let mut diff_stream = old_tree
2544 .diff_stream_for_file_system(new_tree, matcher)
2545 .map(async |TreeDiffEntry { path, values }| match values {
2546 Ok(diff) => {
2547 let result =
2548 materialize_tree_value(&self.store, &path, diff.after, new_tree.labels())
2549 .await;
2550 (path, result.map(|value| (diff.before, value)))
2551 }
2552 Err(err) => (path, Err(err)),
2553 })
2554 .buffered(self.store.concurrency());
2555
2556 let mut conflicts_to_rematerialize: HashMap<RepoPathBuf, MergedTreeValue> =
2562 if old_tree.tree_ids().num_sides() == new_tree.tree_ids().num_sides()
2563 && old_tree.labels() != new_tree.labels()
2564 {
2565 new_tree
2569 .conflicts_matching(matcher)
2570 .map(|(path, value)| value.map(|value| (path, value)))
2571 .try_collect()?
2572 } else {
2573 HashMap::new()
2574 };
2575
2576 while let Some((path, data)) = diff_stream.next().await {
2577 let (before, after) = data?;
2578 conflicts_to_rematerialize.remove(&path);
2579 process_diff_entry(path, before, after).await?;
2580 }
2581
2582 if !conflicts_to_rematerialize.is_empty() {
2583 for (path, conflict) in conflicts_to_rematerialize {
2584 let materialized =
2585 materialize_tree_value(&self.store, &path, conflict.clone(), new_tree.labels())
2586 .await?;
2587 process_diff_entry(path, conflict, materialized).await?;
2588 }
2589
2590 changed_file_states.sort_unstable_by(|(path1, _), (path2, _)| path1.cmp(path2));
2593 }
2594
2595 self.file_states
2596 .merge_in(changed_file_states, &deleted_files);
2597 Ok(stats)
2598 }
2599
2600 pub async fn reset(&mut self, new_tree: &MergedTree) -> Result<(), ResetError> {
2601 let matcher = self.sparse_matcher();
2602 let mut changed_file_states = Vec::new();
2603 let mut deleted_files = HashSet::new();
2604 let mut diff_stream = self
2605 .tree
2606 .diff_stream_for_file_system(new_tree, matcher.as_ref());
2607 while let Some(TreeDiffEntry { path, values }) = diff_stream.next().await {
2608 let after = values?.after;
2609 if after.is_absent() {
2610 deleted_files.insert(path);
2611 } else {
2612 let file_type = match after.into_resolved() {
2613 Ok(value) => match value.unwrap() {
2614 TreeValue::File {
2615 id: _,
2616 executable,
2617 copy_id: _,
2618 } => {
2619 let get_prev_exec = || self.file_states().get_exec_bit(&path);
2620 let exec_bit =
2621 ExecBit::new_from_repo(executable, self.exec_policy, get_prev_exec);
2622 FileType::Normal { exec_bit }
2623 }
2624 TreeValue::Symlink(_id) => FileType::Symlink,
2625 TreeValue::GitSubmodule(_id) => {
2626 eprintln!("ignoring git submodule at {path:?}");
2627 FileType::GitSubmodule
2628 }
2629 TreeValue::Tree(_id) => {
2630 panic!("unexpected tree entry in diff at {path:?}");
2631 }
2632 },
2633 Err(_values) => {
2634 FileType::Normal {
2636 exec_bit: ExecBit(false),
2637 }
2638 }
2639 };
2640 let file_state = FileState {
2641 file_type,
2642 mtime: MillisSinceEpoch(0),
2643 size: 0,
2644 materialized_conflict_data: None,
2645 };
2646 changed_file_states.push((path, file_state));
2647 }
2648 }
2649 self.file_states
2650 .merge_in(changed_file_states, &deleted_files);
2651 self.tree = new_tree.clone();
2652 Ok(())
2653 }
2654
2655 pub async fn recover(&mut self, new_tree: &MergedTree) -> Result<(), ResetError> {
2656 self.file_states.clear();
2657 self.tree = self.store.empty_merged_tree();
2658 self.reset(new_tree).await
2659 }
2660}
2661
2662fn checkout_error_for_stat_error(err: io::Error, path: &Path) -> CheckoutError {
2663 CheckoutError::Other {
2664 message: format!("Failed to stat file {}", path.display()),
2665 err: err.into(),
2666 }
2667}
2668
2669fn checkout_error_for_mtime_out_of_range(err: MtimeOutOfRange, path: &Path) -> CheckoutError {
2670 CheckoutError::Other {
2671 message: format!("Failed to process file metadata {}", path.display()),
2672 err: err.into(),
2673 }
2674}
2675
2676#[derive(Clone, Debug)]
2678struct CheckoutState {
2679 operation_id: OperationId,
2680 workspace_name: WorkspaceNameBuf,
2681}
2682
2683impl CheckoutState {
2684 fn load(state_path: &Path) -> Result<Self, WorkingCopyStateError> {
2685 let wrap_err = |err| WorkingCopyStateError {
2686 message: "Failed to read checkout state".to_owned(),
2687 err,
2688 };
2689 let buf = fs::read(state_path.join("checkout")).map_err(|err| wrap_err(err.into()))?;
2690 let proto = crate::protos::local_working_copy::Checkout::decode(&*buf)
2691 .map_err(|err| wrap_err(err.into()))?;
2692 Ok(Self {
2693 operation_id: OperationId::new(proto.operation_id),
2694 workspace_name: if proto.workspace_name.is_empty() {
2695 WorkspaceName::DEFAULT.to_owned()
2698 } else {
2699 proto.workspace_name.into()
2700 },
2701 })
2702 }
2703
2704 #[instrument(skip_all)]
2705 fn save(&self, state_path: &Path) -> Result<(), WorkingCopyStateError> {
2706 let wrap_err = |err| WorkingCopyStateError {
2707 message: "Failed to write checkout state".to_owned(),
2708 err,
2709 };
2710 let proto = crate::protos::local_working_copy::Checkout {
2711 operation_id: self.operation_id.to_bytes(),
2712 workspace_name: (*self.workspace_name).into(),
2713 };
2714 let mut temp_file =
2715 NamedTempFile::new_in(state_path).map_err(|err| wrap_err(err.into()))?;
2716 temp_file
2717 .as_file_mut()
2718 .write_all(&proto.encode_to_vec())
2719 .map_err(|err| wrap_err(err.into()))?;
2720 persist_temp_file(temp_file, state_path.join("checkout"))
2723 .map_err(|err| wrap_err(err.into()))?;
2724 Ok(())
2725 }
2726}
2727
2728pub struct LocalWorkingCopy {
2729 store: Arc<Store>,
2730 working_copy_path: PathBuf,
2731 state_path: PathBuf,
2732 checkout_state: CheckoutState,
2733 tree_state: OnceCell<TreeState>,
2734 tree_state_settings: TreeStateSettings,
2735}
2736
2737#[async_trait(?Send)]
2738impl WorkingCopy for LocalWorkingCopy {
2739 fn name(&self) -> &str {
2740 Self::NAME
2741 }
2742
2743 fn workspace_name(&self) -> &WorkspaceName {
2744 &self.checkout_state.workspace_name
2745 }
2746
2747 fn operation_id(&self) -> &OperationId {
2748 &self.checkout_state.operation_id
2749 }
2750
2751 fn tree(&self) -> Result<&MergedTree, WorkingCopyStateError> {
2752 Ok(self.tree_state()?.current_tree())
2753 }
2754
2755 fn sparse_patterns(&self) -> Result<&[RepoPathBuf], WorkingCopyStateError> {
2756 Ok(self.tree_state()?.sparse_patterns())
2757 }
2758
2759 async fn start_mutation(&self) -> Result<Box<dyn LockedWorkingCopy>, WorkingCopyStateError> {
2760 let lock_path = self.state_path.join("working_copy.lock");
2761 let lock = FileLock::lock(lock_path).map_err(|err| WorkingCopyStateError {
2762 message: "Failed to lock working copy".to_owned(),
2763 err: err.into(),
2764 })?;
2765
2766 let wc = Self {
2767 store: self.store.clone(),
2768 working_copy_path: self.working_copy_path.clone(),
2769 state_path: self.state_path.clone(),
2770 checkout_state: CheckoutState::load(&self.state_path)?,
2772 tree_state: OnceCell::new(),
2776 tree_state_settings: self.tree_state_settings.clone(),
2777 };
2778 let old_operation_id = wc.operation_id().clone();
2779 let old_tree = wc.tree()?.clone();
2780 Ok(Box::new(LockedLocalWorkingCopy {
2781 wc,
2782 old_operation_id,
2783 old_tree,
2784 tree_state_dirty: false,
2785 new_workspace_name: None,
2786 _lock: lock,
2787 }))
2788 }
2789}
2790
2791impl LocalWorkingCopy {
2792 pub const NAME: &str = "local";
2793
2794 pub fn init(
2798 store: Arc<Store>,
2799 working_copy_path: PathBuf,
2800 state_path: PathBuf,
2801 operation_id: OperationId,
2802 workspace_name: WorkspaceNameBuf,
2803 user_settings: &UserSettings,
2804 ) -> Result<Self, WorkingCopyStateError> {
2805 let checkout_state = CheckoutState {
2806 operation_id,
2807 workspace_name,
2808 };
2809 checkout_state.save(&state_path)?;
2810 let tree_state_settings = TreeStateSettings::try_from_user_settings(user_settings)
2811 .map_err(|err| WorkingCopyStateError {
2812 message: "Failed to read the tree state settings".to_string(),
2813 err: err.into(),
2814 })?;
2815 let tree_state = TreeState::init(
2816 store.clone(),
2817 working_copy_path.clone(),
2818 state_path.clone(),
2819 &tree_state_settings,
2820 )
2821 .map_err(|err| WorkingCopyStateError {
2822 message: "Failed to initialize working copy state".to_string(),
2823 err: err.into(),
2824 })?;
2825 Ok(Self {
2826 store,
2827 working_copy_path,
2828 state_path,
2829 checkout_state,
2830 tree_state: OnceCell::with_value(tree_state),
2831 tree_state_settings,
2832 })
2833 }
2834
2835 pub fn load(
2836 store: Arc<Store>,
2837 working_copy_path: PathBuf,
2838 state_path: PathBuf,
2839 user_settings: &UserSettings,
2840 ) -> Result<Self, WorkingCopyStateError> {
2841 let checkout_state = CheckoutState::load(&state_path)?;
2842 let tree_state_settings = TreeStateSettings::try_from_user_settings(user_settings)
2843 .map_err(|err| WorkingCopyStateError {
2844 message: "Failed to read the tree state settings".to_string(),
2845 err: err.into(),
2846 })?;
2847 Ok(Self {
2848 store,
2849 working_copy_path,
2850 state_path,
2851 checkout_state,
2852 tree_state: OnceCell::new(),
2853 tree_state_settings,
2854 })
2855 }
2856
2857 pub fn state_path(&self) -> &Path {
2858 &self.state_path
2859 }
2860
2861 #[instrument(skip_all)]
2862 fn tree_state(&self) -> Result<&TreeState, WorkingCopyStateError> {
2863 self.tree_state.get_or_try_init(|| {
2864 TreeState::load(
2865 self.store.clone(),
2866 self.working_copy_path.clone(),
2867 self.state_path.clone(),
2868 &self.tree_state_settings,
2869 )
2870 .map_err(|err| WorkingCopyStateError {
2871 message: "Failed to read working copy state".to_string(),
2872 err: err.into(),
2873 })
2874 })
2875 }
2876
2877 fn tree_state_mut(&mut self) -> Result<&mut TreeState, WorkingCopyStateError> {
2878 self.tree_state()?; Ok(self.tree_state.get_mut().unwrap())
2880 }
2881
2882 pub fn file_states(&self) -> Result<FileStates<'_>, WorkingCopyStateError> {
2883 Ok(self.tree_state()?.file_states())
2884 }
2885
2886 #[cfg(feature = "watchman")]
2887 pub async fn query_watchman(
2888 &self,
2889 config: &WatchmanConfig,
2890 ) -> Result<(watchman::Clock, Option<Vec<PathBuf>>), WorkingCopyStateError> {
2891 self.tree_state()?
2892 .query_watchman(config)
2893 .await
2894 .map_err(|err| WorkingCopyStateError {
2895 message: "Failed to query watchman".to_string(),
2896 err: err.into(),
2897 })
2898 }
2899
2900 #[cfg(feature = "watchman")]
2901 pub async fn is_watchman_trigger_registered(
2902 &self,
2903 config: &WatchmanConfig,
2904 ) -> Result<bool, WorkingCopyStateError> {
2905 self.tree_state()?
2906 .is_watchman_trigger_registered(config)
2907 .await
2908 .map_err(|err| WorkingCopyStateError {
2909 message: "Failed to query watchman".to_string(),
2910 err: err.into(),
2911 })
2912 }
2913}
2914
2915pub struct LocalWorkingCopyFactory {}
2916
2917impl WorkingCopyFactory for LocalWorkingCopyFactory {
2918 fn init_working_copy(
2919 &self,
2920 store: Arc<Store>,
2921 working_copy_path: PathBuf,
2922 state_path: PathBuf,
2923 operation_id: OperationId,
2924 workspace_name: WorkspaceNameBuf,
2925 settings: &UserSettings,
2926 ) -> Result<Box<dyn WorkingCopy>, WorkingCopyStateError> {
2927 Ok(Box::new(LocalWorkingCopy::init(
2928 store,
2929 working_copy_path,
2930 state_path,
2931 operation_id,
2932 workspace_name,
2933 settings,
2934 )?))
2935 }
2936
2937 fn load_working_copy(
2938 &self,
2939 store: Arc<Store>,
2940 working_copy_path: PathBuf,
2941 state_path: PathBuf,
2942 settings: &UserSettings,
2943 ) -> Result<Box<dyn WorkingCopy>, WorkingCopyStateError> {
2944 Ok(Box::new(LocalWorkingCopy::load(
2945 store,
2946 working_copy_path,
2947 state_path,
2948 settings,
2949 )?))
2950 }
2951}
2952
2953pub struct LockedLocalWorkingCopy {
2956 wc: LocalWorkingCopy,
2957 old_operation_id: OperationId,
2958 old_tree: MergedTree,
2959 tree_state_dirty: bool,
2960 new_workspace_name: Option<WorkspaceNameBuf>,
2961 _lock: FileLock,
2962}
2963
2964#[async_trait]
2965impl LockedWorkingCopy for LockedLocalWorkingCopy {
2966 fn old_operation_id(&self) -> &OperationId {
2967 &self.old_operation_id
2968 }
2969
2970 fn old_tree(&self) -> &MergedTree {
2971 &self.old_tree
2972 }
2973
2974 async fn snapshot(
2975 &mut self,
2976 options: &SnapshotOptions,
2977 ) -> Result<(MergedTree, SnapshotStats), SnapshotError> {
2978 let tree_state = self.wc.tree_state_mut()?;
2979 let (is_dirty, stats) = tree_state.snapshot(options).await?;
2980 self.tree_state_dirty |= is_dirty;
2981 Ok((tree_state.current_tree().clone(), stats))
2982 }
2983
2984 async fn check_out(&mut self, commit: &Commit) -> Result<CheckoutStats, CheckoutError> {
2985 let new_tree = commit.tree();
2988 let tree_state = self.wc.tree_state_mut()?;
2989 if tree_state.tree.tree_ids_and_labels() != new_tree.tree_ids_and_labels() {
2990 let stats = tree_state.check_out(&new_tree)?;
2991 self.tree_state_dirty = true;
2992 Ok(stats)
2993 } else {
2994 Ok(CheckoutStats::default())
2995 }
2996 }
2997
2998 fn rename_workspace(&mut self, new_name: WorkspaceNameBuf) {
2999 self.new_workspace_name = Some(new_name);
3000 }
3001
3002 async fn reset(&mut self, commit: &Commit) -> Result<(), ResetError> {
3003 let new_tree = commit.tree();
3004 self.wc.tree_state_mut()?.reset(&new_tree).await?;
3005 self.tree_state_dirty = true;
3006 Ok(())
3007 }
3008
3009 async fn recover(&mut self, commit: &Commit) -> Result<(), ResetError> {
3010 let new_tree = commit.tree();
3011 self.wc.tree_state_mut()?.recover(&new_tree).await?;
3012 self.tree_state_dirty = true;
3013 Ok(())
3014 }
3015
3016 fn sparse_patterns(&self) -> Result<&[RepoPathBuf], WorkingCopyStateError> {
3017 self.wc.sparse_patterns()
3018 }
3019
3020 async fn set_sparse_patterns(
3021 &mut self,
3022 new_sparse_patterns: Vec<RepoPathBuf>,
3023 ) -> Result<CheckoutStats, CheckoutError> {
3024 let stats = self
3027 .wc
3028 .tree_state_mut()?
3029 .set_sparse_patterns(new_sparse_patterns)?;
3030 self.tree_state_dirty = true;
3031 Ok(stats)
3032 }
3033
3034 #[instrument(skip_all)]
3035 async fn finish(
3036 mut self: Box<Self>,
3037 operation_id: OperationId,
3038 ) -> Result<Box<dyn WorkingCopy>, WorkingCopyStateError> {
3039 assert!(
3040 self.tree_state_dirty
3041 || self.old_tree.tree_ids_and_labels() == self.wc.tree()?.tree_ids_and_labels()
3042 );
3043 if self.tree_state_dirty {
3044 self.wc
3045 .tree_state_mut()?
3046 .save()
3047 .map_err(|err| WorkingCopyStateError {
3048 message: "Failed to write working copy state".to_string(),
3049 err: Box::new(err),
3050 })?;
3051 }
3052 if self.old_operation_id != operation_id || self.new_workspace_name.is_some() {
3053 self.wc.checkout_state.operation_id = operation_id;
3054 if let Some(workspace_name) = self.new_workspace_name {
3055 self.wc.checkout_state.workspace_name = workspace_name;
3056 }
3057 self.wc.checkout_state.save(&self.wc.state_path)?;
3058 }
3059 Ok(Box::new(self.wc))
3061 }
3062}
3063
3064impl LockedLocalWorkingCopy {
3065 pub fn reset_watchman(&mut self) -> Result<(), SnapshotError> {
3066 self.wc.tree_state_mut()?.reset_watchman();
3067 self.tree_state_dirty = true;
3068 Ok(())
3069 }
3070}
3071
3072#[cfg(test)]
3073mod tests {
3074 use std::time::Duration;
3075
3076 use maplit::hashset;
3077
3078 use super::*;
3079
3080 fn repo_path(value: &str) -> &RepoPath {
3081 RepoPath::from_internal_string(value).unwrap()
3082 }
3083
3084 fn repo_path_component(value: &str) -> &RepoPathComponent {
3085 RepoPathComponent::new(value).unwrap()
3086 }
3087
3088 fn new_state(size: u64) -> FileState {
3089 FileState {
3090 file_type: FileType::Normal {
3091 exec_bit: ExecBit(false),
3092 },
3093 mtime: MillisSinceEpoch(0),
3094 size,
3095 materialized_conflict_data: None,
3096 }
3097 }
3098
3099 #[test]
3100 fn test_file_states_merge() {
3101 let new_static_entry = |path: &'static str, size| (repo_path(path), new_state(size));
3102 let new_owned_entry = |path: &str, size| (repo_path(path).to_owned(), new_state(size));
3103 let new_proto_entry = |path: &str, size| {
3104 file_state_entry_to_proto(repo_path(path).to_owned(), &new_state(size))
3105 };
3106 let data = vec![
3107 new_proto_entry("aa", 0),
3108 new_proto_entry("b#", 4), new_proto_entry("b/c", 1),
3110 new_proto_entry("b/d/e", 2),
3111 new_proto_entry("b/e", 3),
3112 new_proto_entry("bc", 5),
3113 ];
3114 let mut file_states = FileStatesMap::from_proto(data, false);
3115
3116 let changed_file_states = vec![
3117 new_owned_entry("aa", 10), new_owned_entry("b/d/f", 11), new_owned_entry("b/e", 12), new_owned_entry("c", 13), ];
3122 let deleted_files = hashset! {
3123 repo_path("b/c").to_owned(),
3124 repo_path("b#").to_owned(),
3125 };
3126 file_states.merge_in(changed_file_states, &deleted_files);
3127 assert_eq!(
3128 file_states.all().iter().collect_vec(),
3129 vec![
3130 new_static_entry("aa", 10),
3131 new_static_entry("b/d/e", 2),
3132 new_static_entry("b/d/f", 11),
3133 new_static_entry("b/e", 12),
3134 new_static_entry("bc", 5),
3135 new_static_entry("c", 13),
3136 ],
3137 );
3138 }
3139
3140 #[test]
3141 fn test_file_states_lookup() {
3142 let new_proto_entry = |path: &str, size| {
3143 file_state_entry_to_proto(repo_path(path).to_owned(), &new_state(size))
3144 };
3145 let data = vec![
3146 new_proto_entry("aa", 0),
3147 new_proto_entry("b/c", 1),
3148 new_proto_entry("b/d/e", 2),
3149 new_proto_entry("b/e", 3),
3150 new_proto_entry("b#", 4), new_proto_entry("bc", 5),
3152 ];
3153 let file_states = FileStates::from_sorted(&data);
3154
3155 assert_eq!(
3156 file_states.prefixed(repo_path("")).paths().collect_vec(),
3157 ["aa", "b/c", "b/d/e", "b/e", "b#", "bc"].map(repo_path)
3158 );
3159 assert!(file_states.prefixed(repo_path("a")).is_empty());
3160 assert_eq!(
3161 file_states.prefixed(repo_path("aa")).paths().collect_vec(),
3162 ["aa"].map(repo_path)
3163 );
3164 assert_eq!(
3165 file_states.prefixed(repo_path("b")).paths().collect_vec(),
3166 ["b/c", "b/d/e", "b/e"].map(repo_path)
3167 );
3168 assert_eq!(
3169 file_states.prefixed(repo_path("b/d")).paths().collect_vec(),
3170 ["b/d/e"].map(repo_path)
3171 );
3172 assert_eq!(
3173 file_states.prefixed(repo_path("b#")).paths().collect_vec(),
3174 ["b#"].map(repo_path)
3175 );
3176 assert_eq!(
3177 file_states.prefixed(repo_path("bc")).paths().collect_vec(),
3178 ["bc"].map(repo_path)
3179 );
3180 assert!(file_states.prefixed(repo_path("z")).is_empty());
3181
3182 assert!(!file_states.contains_path(repo_path("a")));
3183 assert!(file_states.contains_path(repo_path("aa")));
3184 assert!(file_states.contains_path(repo_path("b/d/e")));
3185 assert!(!file_states.contains_path(repo_path("b/d")));
3186 assert!(file_states.contains_path(repo_path("b#")));
3187 assert!(file_states.contains_path(repo_path("bc")));
3188 assert!(!file_states.contains_path(repo_path("z")));
3189
3190 assert_eq!(file_states.get(repo_path("a")), None);
3191 assert_eq!(file_states.get(repo_path("aa")), Some(new_state(0)));
3192 assert_eq!(file_states.get(repo_path("b/d/e")), Some(new_state(2)));
3193 assert_eq!(file_states.get(repo_path("bc")), Some(new_state(5)));
3194 assert_eq!(file_states.get(repo_path("z")), None);
3195 }
3196
3197 #[test]
3198 fn test_file_states_lookup_at() {
3199 let new_proto_entry = |path: &str, size| {
3200 file_state_entry_to_proto(repo_path(path).to_owned(), &new_state(size))
3201 };
3202 let data = vec![
3203 new_proto_entry("b/c", 0),
3204 new_proto_entry("b/d/e", 1),
3205 new_proto_entry("b/d#", 2), new_proto_entry("b/e", 3),
3207 new_proto_entry("b#", 4), ];
3209 let file_states = FileStates::from_sorted(&data);
3210
3211 assert_eq!(
3213 file_states.get_at(RepoPath::root(), repo_path_component("b")),
3214 None
3215 );
3216 assert_eq!(
3217 file_states.get_at(RepoPath::root(), repo_path_component("b#")),
3218 Some(new_state(4))
3219 );
3220
3221 let prefixed_states = file_states.prefixed_at(RepoPath::root(), repo_path_component("b"));
3223 assert_eq!(
3224 prefixed_states.paths().collect_vec(),
3225 ["b/c", "b/d/e", "b/d#", "b/e"].map(repo_path)
3226 );
3227 assert_eq!(
3228 prefixed_states.get_at(repo_path("b"), repo_path_component("c")),
3229 Some(new_state(0))
3230 );
3231 assert_eq!(
3232 prefixed_states.get_at(repo_path("b"), repo_path_component("d")),
3233 None
3234 );
3235 assert_eq!(
3236 prefixed_states.get_at(repo_path("b"), repo_path_component("d#")),
3237 Some(new_state(2))
3238 );
3239
3240 let prefixed_states = prefixed_states.prefixed_at(repo_path("b"), repo_path_component("d"));
3242 assert_eq!(
3243 prefixed_states.paths().collect_vec(),
3244 ["b/d/e"].map(repo_path)
3245 );
3246 assert_eq!(
3247 prefixed_states.get_at(repo_path("b/d"), repo_path_component("e")),
3248 Some(new_state(1))
3249 );
3250 assert_eq!(
3251 prefixed_states.get_at(repo_path("b/d"), repo_path_component("#")),
3252 None
3253 );
3254
3255 let prefixed_states = file_states.prefixed_at(RepoPath::root(), repo_path_component("b#"));
3257 assert_eq!(prefixed_states.paths().collect_vec(), ["b#"].map(repo_path));
3258 assert_eq!(
3259 prefixed_states.get_at(repo_path("b#"), repo_path_component("#")),
3260 None
3261 );
3262 }
3263
3264 #[test]
3265 fn test_system_time_to_millis() {
3266 let epoch = SystemTime::UNIX_EPOCH;
3267 assert_eq!(system_time_to_millis(epoch), Some(MillisSinceEpoch(0)));
3268 if let Some(time) = epoch.checked_add(Duration::from_millis(1)) {
3269 assert_eq!(system_time_to_millis(time), Some(MillisSinceEpoch(1)));
3270 }
3271 if let Some(time) = epoch.checked_sub(Duration::from_millis(1)) {
3272 assert_eq!(system_time_to_millis(time), Some(MillisSinceEpoch(-1)));
3273 }
3274 if let Some(time) = epoch.checked_add(Duration::from_millis(i64::MAX as u64)) {
3275 assert_eq!(
3276 system_time_to_millis(time),
3277 Some(MillisSinceEpoch(i64::MAX))
3278 );
3279 }
3280 if let Some(time) = epoch.checked_sub(Duration::from_millis(i64::MAX as u64)) {
3281 assert_eq!(
3282 system_time_to_millis(time),
3283 Some(MillisSinceEpoch(-i64::MAX))
3284 );
3285 }
3286 if let Some(time) = epoch.checked_sub(Duration::from_millis(i64::MAX as u64 + 1)) {
3287 assert_eq!(system_time_to_millis(time), None);
3289 }
3290 }
3291}