1#![allow(missing_docs)]
16#![allow(clippy::let_unit_value)]
17
18use std::any::Any;
19use std::cmp::Ordering;
20use std::collections::HashSet;
21use std::error::Error;
22use std::fs;
23use std::fs::DirEntry;
24use std::fs::File;
25use std::fs::Metadata;
26use std::fs::OpenOptions;
27use std::io;
28use std::io::Read as _;
29use std::io::Write as _;
30use std::iter;
31use std::mem;
32use std::ops::Range;
33#[cfg(unix)]
34use std::os::unix::fs::PermissionsExt as _;
35use std::path::Path;
36use std::path::PathBuf;
37use std::slice;
38use std::sync::mpsc::channel;
39use std::sync::mpsc::Sender;
40use std::sync::Arc;
41use std::sync::OnceLock;
42use std::time::UNIX_EPOCH;
43
44use either::Either;
45use futures::StreamExt as _;
46use itertools::EitherOrBoth;
47use itertools::Itertools as _;
48use once_cell::unsync::OnceCell;
49use pollster::FutureExt as _;
50use prost::Message as _;
51use rayon::iter::IntoParallelIterator as _;
52use rayon::prelude::IndexedParallelIterator as _;
53use rayon::prelude::ParallelIterator as _;
54use tempfile::NamedTempFile;
55use thiserror::Error;
56use tokio::io::AsyncRead;
57use tracing::instrument;
58use tracing::trace_span;
59
60use crate::backend::BackendError;
61use crate::backend::BackendResult;
62use crate::backend::CopyId;
63use crate::backend::FileId;
64use crate::backend::MergedTreeId;
65use crate::backend::MillisSinceEpoch;
66use crate::backend::SymlinkId;
67use crate::backend::TreeId;
68use crate::backend::TreeValue;
69use crate::commit::Commit;
70use crate::conflicts;
71use crate::conflicts::choose_materialized_conflict_marker_len;
72use crate::conflicts::materialize_merge_result_to_bytes_with_marker_len;
73use crate::conflicts::materialize_tree_value;
74use crate::conflicts::ConflictMarkerStyle;
75use crate::conflicts::MaterializedTreeValue;
76use crate::conflicts::MIN_CONFLICT_MARKER_LEN;
77use crate::file_util::check_symlink_support;
78use crate::file_util::copy_async_to_sync;
79use crate::file_util::try_symlink;
80use crate::file_util::BlockingAsyncReader;
81#[cfg(feature = "watchman")]
82use crate::fsmonitor::watchman;
83use crate::fsmonitor::FsmonitorSettings;
84#[cfg(feature = "watchman")]
85use crate::fsmonitor::WatchmanConfig;
86use crate::gitignore::GitIgnoreFile;
87use crate::lock::FileLock;
88use crate::matchers::DifferenceMatcher;
89use crate::matchers::EverythingMatcher;
90use crate::matchers::FilesMatcher;
91use crate::matchers::IntersectionMatcher;
92use crate::matchers::Matcher;
93use crate::matchers::PrefixMatcher;
94use crate::merge::Merge;
95use crate::merge::MergeBuilder;
96use crate::merge::MergedTreeValue;
97use crate::merged_tree::MergedTree;
98use crate::merged_tree::MergedTreeBuilder;
99use crate::merged_tree::TreeDiffEntry;
100use crate::object_id::ObjectId as _;
101use crate::op_store::OperationId;
102use crate::ref_name::WorkspaceName;
103use crate::ref_name::WorkspaceNameBuf;
104use crate::repo_path::RepoPath;
105use crate::repo_path::RepoPathBuf;
106use crate::repo_path::RepoPathComponent;
107use crate::store::Store;
108use crate::tree::Tree;
109use crate::working_copy::CheckoutError;
110use crate::working_copy::CheckoutOptions;
111use crate::working_copy::CheckoutStats;
112use crate::working_copy::LockedWorkingCopy;
113use crate::working_copy::ResetError;
114use crate::working_copy::SnapshotError;
115use crate::working_copy::SnapshotOptions;
116use crate::working_copy::SnapshotProgress;
117use crate::working_copy::SnapshotStats;
118use crate::working_copy::UntrackedReason;
119use crate::working_copy::WorkingCopy;
120use crate::working_copy::WorkingCopyFactory;
121use crate::working_copy::WorkingCopyStateError;
122
123#[derive(Clone, Copy, Debug, Eq, PartialEq)]
127pub struct FileExecutableFlag(#[cfg(unix)] bool);
128
129#[cfg(unix)]
130impl FileExecutableFlag {
131 pub const fn from_bool_lossy(executable: bool) -> Self {
132 FileExecutableFlag(executable)
133 }
134
135 pub fn unwrap_or_else(self, _: impl FnOnce() -> bool) -> bool {
136 self.0
137 }
138}
139
140#[cfg(windows)]
142impl FileExecutableFlag {
143 pub const fn from_bool_lossy(_executable: bool) -> Self {
144 FileExecutableFlag()
145 }
146
147 pub fn unwrap_or_else(self, f: impl FnOnce() -> bool) -> bool {
148 f()
149 }
150}
151
152#[derive(Debug, PartialEq, Eq, Clone)]
153pub enum FileType {
154 Normal { executable: FileExecutableFlag },
155 Symlink,
156 GitSubmodule,
157}
158
159#[derive(Debug, PartialEq, Eq, Clone, Copy)]
160pub struct MaterializedConflictData {
161 pub conflict_marker_len: u32,
162}
163
164#[derive(Debug, PartialEq, Eq, Clone)]
165pub struct FileState {
166 pub file_type: FileType,
167 pub mtime: MillisSinceEpoch,
168 pub size: u64,
169 pub materialized_conflict_data: Option<MaterializedConflictData>,
170 }
174
175impl FileState {
176 pub fn is_clean(&self, old_file_state: &Self) -> bool {
179 self.file_type == old_file_state.file_type
180 && self.mtime == old_file_state.mtime
181 && self.size == old_file_state.size
182 }
183
184 fn placeholder() -> Self {
187 let executable = FileExecutableFlag::from_bool_lossy(false);
188 FileState {
189 file_type: FileType::Normal { executable },
190 mtime: MillisSinceEpoch(0),
191 size: 0,
192 materialized_conflict_data: None,
193 }
194 }
195
196 fn for_file(
197 executable: bool,
198 size: u64,
199 metadata: &Metadata,
200 materialized_conflict_data: Option<MaterializedConflictData>,
201 ) -> Self {
202 let executable = FileExecutableFlag::from_bool_lossy(executable);
203 FileState {
204 file_type: FileType::Normal { executable },
205 mtime: mtime_from_metadata(metadata),
206 size,
207 materialized_conflict_data,
208 }
209 }
210
211 fn for_symlink(metadata: &Metadata) -> Self {
212 FileState {
216 file_type: FileType::Symlink,
217 mtime: mtime_from_metadata(metadata),
218 size: metadata.len(),
219 materialized_conflict_data: None,
220 }
221 }
222
223 fn for_gitsubmodule() -> Self {
224 FileState {
225 file_type: FileType::GitSubmodule,
226 mtime: MillisSinceEpoch(0),
227 size: 0,
228 materialized_conflict_data: None,
229 }
230 }
231}
232
233#[derive(Clone, Debug)]
235struct FileStatesMap {
236 data: Vec<crate::protos::working_copy::FileStateEntry>,
237}
238
239impl FileStatesMap {
240 fn new() -> Self {
241 FileStatesMap { data: Vec::new() }
242 }
243
244 fn from_proto(
245 mut data: Vec<crate::protos::working_copy::FileStateEntry>,
246 is_sorted: bool,
247 ) -> Self {
248 if !is_sorted {
249 data.sort_unstable_by(|entry1, entry2| {
250 let path1 = RepoPath::from_internal_string(&entry1.path).unwrap();
251 let path2 = RepoPath::from_internal_string(&entry2.path).unwrap();
252 path1.cmp(path2)
253 });
254 }
255 debug_assert!(is_file_state_entries_proto_unique_and_sorted(&data));
256 FileStatesMap { data }
257 }
258
259 fn merge_in(
262 &mut self,
263 changed_file_states: Vec<(RepoPathBuf, FileState)>,
264 deleted_files: &HashSet<RepoPathBuf>,
265 ) {
266 if changed_file_states.is_empty() && deleted_files.is_empty() {
267 return;
268 }
269 debug_assert!(
270 changed_file_states
271 .iter()
272 .tuple_windows()
273 .all(|((path1, _), (path2, _))| path1 < path2),
274 "changed_file_states must be sorted and have no duplicates"
275 );
276 self.data = itertools::merge_join_by(
277 mem::take(&mut self.data),
278 changed_file_states,
279 |old_entry, (changed_path, _)| {
280 RepoPath::from_internal_string(&old_entry.path)
281 .unwrap()
282 .cmp(changed_path)
283 },
284 )
285 .filter_map(|diff| match diff {
286 EitherOrBoth::Both(_, (path, state)) | EitherOrBoth::Right((path, state)) => {
287 debug_assert!(!deleted_files.contains(&path));
288 Some(file_state_entry_to_proto(path, &state))
289 }
290 EitherOrBoth::Left(entry) => {
291 let present =
292 !deleted_files.contains(RepoPath::from_internal_string(&entry.path).unwrap());
293 present.then_some(entry)
294 }
295 })
296 .collect();
297 }
298
299 fn clear(&mut self) {
300 self.data.clear();
301 }
302
303 fn all(&self) -> FileStates<'_> {
305 FileStates::from_sorted(&self.data)
306 }
307}
308
309#[derive(Clone, Copy, Debug)]
311pub struct FileStates<'a> {
312 data: &'a [crate::protos::working_copy::FileStateEntry],
313}
314
315impl<'a> FileStates<'a> {
316 fn from_sorted(data: &'a [crate::protos::working_copy::FileStateEntry]) -> Self {
317 debug_assert!(is_file_state_entries_proto_unique_and_sorted(data));
318 FileStates { data }
319 }
320
321 pub fn prefixed(&self, base: &RepoPath) -> Self {
323 let range = self.prefixed_range(base);
324 Self::from_sorted(&self.data[range])
325 }
326
327 fn prefixed_at(&self, dir: &RepoPath, base: &RepoPathComponent) -> Self {
330 let range = self.prefixed_range_at(dir, base);
331 Self::from_sorted(&self.data[range])
332 }
333
334 pub fn is_empty(&self) -> bool {
336 self.data.is_empty()
337 }
338
339 pub fn contains_path(&self, path: &RepoPath) -> bool {
341 self.exact_position(path).is_some()
342 }
343
344 pub fn get(&self, path: &RepoPath) -> Option<FileState> {
346 let pos = self.exact_position(path)?;
347 let (_, state) = file_state_entry_from_proto(&self.data[pos]);
348 Some(state)
349 }
350
351 fn get_at(&self, dir: &RepoPath, name: &RepoPathComponent) -> Option<FileState> {
354 let pos = self.exact_position_at(dir, name)?;
355 let (_, state) = file_state_entry_from_proto(&self.data[pos]);
356 Some(state)
357 }
358
359 fn exact_position(&self, path: &RepoPath) -> Option<usize> {
360 self.data
361 .binary_search_by(|entry| {
362 RepoPath::from_internal_string(&entry.path)
363 .unwrap()
364 .cmp(path)
365 })
366 .ok()
367 }
368
369 fn exact_position_at(&self, dir: &RepoPath, name: &RepoPathComponent) -> Option<usize> {
370 debug_assert!(self.paths().all(|path| path.starts_with(dir)));
371 let slash_len = !dir.is_root() as usize;
372 let prefix_len = dir.as_internal_file_string().len() + slash_len;
373 self.data
374 .binary_search_by(|entry| {
375 let tail = entry.path.get(prefix_len..).unwrap_or("");
376 match tail.split_once('/') {
377 Some((pre, _)) => pre.cmp(name.as_internal_str()).then(Ordering::Greater),
379 None => tail.cmp(name.as_internal_str()),
380 }
381 })
382 .ok()
383 }
384
385 fn prefixed_range(&self, base: &RepoPath) -> Range<usize> {
386 let start = self
387 .data
388 .partition_point(|entry| RepoPath::from_internal_string(&entry.path).unwrap() < base);
389 let len = self.data[start..].partition_point(|entry| {
390 RepoPath::from_internal_string(&entry.path)
391 .unwrap()
392 .starts_with(base)
393 });
394 start..(start + len)
395 }
396
397 fn prefixed_range_at(&self, dir: &RepoPath, base: &RepoPathComponent) -> Range<usize> {
398 debug_assert!(self.paths().all(|path| path.starts_with(dir)));
399 let slash_len = !dir.is_root() as usize;
400 let prefix_len = dir.as_internal_file_string().len() + slash_len;
401 let start = self.data.partition_point(|entry| {
402 let tail = entry.path.get(prefix_len..).unwrap_or("");
403 let entry_name = tail.split_once('/').map_or(tail, |(name, _)| name);
404 entry_name < base.as_internal_str()
405 });
406 let len = self.data[start..].partition_point(|entry| {
407 let tail = entry.path.get(prefix_len..).unwrap_or("");
408 let entry_name = tail.split_once('/').map_or(tail, |(name, _)| name);
409 entry_name == base.as_internal_str()
410 });
411 start..(start + len)
412 }
413
414 pub fn iter(&self) -> FileStatesIter<'a> {
416 self.data.iter().map(file_state_entry_from_proto)
417 }
418
419 pub fn paths(&self) -> impl ExactSizeIterator<Item = &'a RepoPath> + use<'a> {
421 self.data
422 .iter()
423 .map(|entry| RepoPath::from_internal_string(&entry.path).unwrap())
424 }
425}
426
427type FileStatesIter<'a> = iter::Map<
428 slice::Iter<'a, crate::protos::working_copy::FileStateEntry>,
429 fn(&crate::protos::working_copy::FileStateEntry) -> (&RepoPath, FileState),
430>;
431
432impl<'a> IntoIterator for FileStates<'a> {
433 type Item = (&'a RepoPath, FileState);
434 type IntoIter = FileStatesIter<'a>;
435
436 fn into_iter(self) -> Self::IntoIter {
437 self.iter()
438 }
439}
440
441pub struct TreeState {
442 store: Arc<Store>,
443 working_copy_path: PathBuf,
444 state_path: PathBuf,
445 tree_id: MergedTreeId,
446 file_states: FileStatesMap,
447 sparse_patterns: Vec<RepoPathBuf>,
449 own_mtime: MillisSinceEpoch,
450 symlink_support: bool,
451
452 watchman_clock: Option<crate::protos::working_copy::WatchmanClock>,
456}
457
458fn file_state_from_proto(proto: &crate::protos::working_copy::FileState) -> FileState {
459 let file_type = match proto.file_type() {
460 crate::protos::working_copy::FileType::Normal => FileType::Normal {
461 executable: FileExecutableFlag::from_bool_lossy(false),
462 },
463 crate::protos::working_copy::FileType::Executable => FileType::Normal {
466 executable: FileExecutableFlag::from_bool_lossy(true),
467 },
468 crate::protos::working_copy::FileType::Symlink => FileType::Symlink,
469 crate::protos::working_copy::FileType::Conflict => FileType::Normal {
470 executable: FileExecutableFlag::from_bool_lossy(false),
471 },
472 crate::protos::working_copy::FileType::GitSubmodule => FileType::GitSubmodule,
473 };
474 FileState {
475 file_type,
476 mtime: MillisSinceEpoch(proto.mtime_millis_since_epoch),
477 size: proto.size,
478 materialized_conflict_data: proto.materialized_conflict_data.as_ref().map(|data| {
479 MaterializedConflictData {
480 conflict_marker_len: data.conflict_marker_len,
481 }
482 }),
483 }
484}
485
486fn file_state_to_proto(file_state: &FileState) -> crate::protos::working_copy::FileState {
487 let mut proto = crate::protos::working_copy::FileState::default();
488 let file_type = match &file_state.file_type {
489 FileType::Normal { executable } => {
490 if executable.unwrap_or_else(Default::default) {
491 crate::protos::working_copy::FileType::Executable
492 } else {
493 crate::protos::working_copy::FileType::Normal
494 }
495 }
496 FileType::Symlink => crate::protos::working_copy::FileType::Symlink,
497 FileType::GitSubmodule => crate::protos::working_copy::FileType::GitSubmodule,
498 };
499 proto.file_type = file_type as i32;
500 proto.mtime_millis_since_epoch = file_state.mtime.0;
501 proto.size = file_state.size;
502 proto.materialized_conflict_data = file_state.materialized_conflict_data.map(|data| {
503 crate::protos::working_copy::MaterializedConflictData {
504 conflict_marker_len: data.conflict_marker_len,
505 }
506 });
507 proto
508}
509
510fn file_state_entry_from_proto(
511 proto: &crate::protos::working_copy::FileStateEntry,
512) -> (&RepoPath, FileState) {
513 let path = RepoPath::from_internal_string(&proto.path).unwrap();
514 (path, file_state_from_proto(proto.state.as_ref().unwrap()))
515}
516
517fn file_state_entry_to_proto(
518 path: RepoPathBuf,
519 state: &FileState,
520) -> crate::protos::working_copy::FileStateEntry {
521 crate::protos::working_copy::FileStateEntry {
522 path: path.into_internal_string(),
523 state: Some(file_state_to_proto(state)),
524 }
525}
526
527fn is_file_state_entries_proto_unique_and_sorted(
528 data: &[crate::protos::working_copy::FileStateEntry],
529) -> bool {
530 data.iter()
531 .map(|entry| RepoPath::from_internal_string(&entry.path).unwrap())
532 .tuple_windows()
533 .all(|(path1, path2)| path1 < path2)
534}
535
536fn sparse_patterns_from_proto(
537 proto: Option<&crate::protos::working_copy::SparsePatterns>,
538) -> Vec<RepoPathBuf> {
539 let mut sparse_patterns = vec![];
540 if let Some(proto_sparse_patterns) = proto {
541 for prefix in &proto_sparse_patterns.prefixes {
542 sparse_patterns.push(RepoPathBuf::from_internal_string(prefix).unwrap());
543 }
544 } else {
545 sparse_patterns.push(RepoPathBuf::root());
548 }
549 sparse_patterns
550}
551
552fn create_parent_dirs(
566 working_copy_path: &Path,
567 repo_path: &RepoPath,
568) -> Result<Option<PathBuf>, CheckoutError> {
569 let (parent_path, basename) = repo_path.split().expect("repo path shouldn't be root");
570 let mut dir_path = working_copy_path.to_owned();
571 for c in parent_path.components() {
572 dir_path.push(c.to_fs_name().map_err(|err| err.with_path(repo_path))?);
574 let new_dir_created = match fs::create_dir(&dir_path) {
578 Ok(()) => true, Err(err) => match dir_path.symlink_metadata() {
580 Ok(m) if m.is_dir() => false, Ok(_) => {
582 return Ok(None); }
584 Err(_) => {
585 return Err(CheckoutError::Other {
586 message: format!(
587 "Failed to create parent directories for {}",
588 repo_path.to_fs_path_unchecked(working_copy_path).display(),
589 ),
590 err: err.into(),
591 })
592 }
593 },
594 };
595 reject_reserved_existing_path(&dir_path).inspect_err(|_| {
598 if new_dir_created {
599 fs::remove_dir(&dir_path).ok();
600 }
601 })?;
602 }
603
604 let mut file_path = dir_path;
605 file_path.push(
606 basename
607 .to_fs_name()
608 .map_err(|err| err.with_path(repo_path))?,
609 );
610 Ok(Some(file_path))
611}
612
613fn remove_old_file(disk_path: &Path) -> Result<bool, CheckoutError> {
619 reject_reserved_existing_path(disk_path)?;
620 match fs::remove_file(disk_path) {
621 Ok(()) => Ok(true),
622 Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(false),
623 Err(_) if disk_path.symlink_metadata().is_ok_and(|m| m.is_dir()) => Ok(false),
625 Err(err) => Err(CheckoutError::Other {
626 message: format!("Failed to remove file {}", disk_path.display()),
627 err: err.into(),
628 }),
629 }
630}
631
632fn can_create_new_file(disk_path: &Path) -> Result<bool, CheckoutError> {
642 let new_file_created = match OpenOptions::new()
647 .write(true)
648 .create_new(true) .open(disk_path)
650 {
651 Ok(_) => true,
652 Err(err) if err.kind() == io::ErrorKind::AlreadyExists => false,
653 Err(_) => match disk_path.symlink_metadata() {
655 Ok(_) => false,
656 Err(err) => {
657 return Err(CheckoutError::Other {
658 message: format!("Failed to stat {}", disk_path.display()),
659 err: err.into(),
660 })
661 }
662 },
663 };
664 reject_reserved_existing_path(disk_path).inspect_err(|_| {
665 if new_file_created {
666 fs::remove_file(disk_path).ok();
667 }
668 })?;
669 if new_file_created {
670 fs::remove_file(disk_path).map_err(|err| CheckoutError::Other {
671 message: format!("Failed to remove temporary file {}", disk_path.display()),
672 err: err.into(),
673 })?;
674 }
675 Ok(new_file_created)
676}
677
678const RESERVED_DIR_NAMES: &[&str] = &[".git", ".jj"];
679
680fn reject_reserved_existing_path(disk_path: &Path) -> Result<(), CheckoutError> {
683 let parent_dir_path = disk_path.parent().expect("content path shouldn't be root");
684 for name in RESERVED_DIR_NAMES {
685 let reserved_path = parent_dir_path.join(name);
686 match same_file::is_same_file(disk_path, &reserved_path) {
687 Ok(true) => {
688 return Err(CheckoutError::ReservedPathComponent {
689 path: disk_path.to_owned(),
690 name,
691 });
692 }
693 Ok(false) => {}
694 Err(err) if err.kind() == io::ErrorKind::NotFound => {}
697 Err(err) => {
698 return Err(CheckoutError::Other {
699 message: format!("Failed to validate path {}", disk_path.display()),
700 err: err.into(),
701 });
702 }
703 }
704 }
705 Ok(())
706}
707
708fn mtime_from_metadata(metadata: &Metadata) -> MillisSinceEpoch {
709 let time = metadata
710 .modified()
711 .expect("File mtime not supported on this platform?");
712 let since_epoch = time
713 .duration_since(UNIX_EPOCH)
714 .expect("mtime before unix epoch");
715
716 MillisSinceEpoch(
717 i64::try_from(since_epoch.as_millis())
718 .expect("mtime billions of years into the future or past"),
719 )
720}
721
722fn file_state(metadata: &Metadata) -> Option<FileState> {
723 let metadata_file_type = metadata.file_type();
724 let file_type = if metadata_file_type.is_dir() {
725 None
726 } else if metadata_file_type.is_symlink() {
727 Some(FileType::Symlink)
728 } else if metadata_file_type.is_file() {
729 #[cfg(unix)]
730 let executable = metadata.permissions().mode() & 0o111 != 0;
731 #[cfg(windows)]
732 let executable = false;
733 let executable = FileExecutableFlag::from_bool_lossy(executable);
734 Some(FileType::Normal { executable })
735 } else {
736 None
737 };
738 file_type.map(|file_type| {
739 let mtime = mtime_from_metadata(metadata);
740 let size = metadata.len();
741 FileState {
742 file_type,
743 mtime,
744 size,
745 materialized_conflict_data: None,
746 }
747 })
748}
749
750struct FsmonitorMatcher {
751 matcher: Option<Box<dyn Matcher>>,
752 watchman_clock: Option<crate::protos::working_copy::WatchmanClock>,
753}
754
755#[derive(Debug, Error)]
756pub enum TreeStateError {
757 #[error("Reading tree state from {path}")]
758 ReadTreeState { path: PathBuf, source: io::Error },
759 #[error("Decoding tree state from {path}")]
760 DecodeTreeState {
761 path: PathBuf,
762 source: prost::DecodeError,
763 },
764 #[error("Writing tree state to temporary file {path}")]
765 WriteTreeState { path: PathBuf, source: io::Error },
766 #[error("Persisting tree state to file {path}")]
767 PersistTreeState { path: PathBuf, source: io::Error },
768 #[error("Filesystem monitor error")]
769 Fsmonitor(#[source] Box<dyn Error + Send + Sync>),
770}
771
772impl TreeState {
773 pub fn working_copy_path(&self) -> &Path {
774 &self.working_copy_path
775 }
776
777 pub fn current_tree_id(&self) -> &MergedTreeId {
778 &self.tree_id
779 }
780
781 pub fn file_states(&self) -> FileStates<'_> {
782 self.file_states.all()
783 }
784
785 pub fn sparse_patterns(&self) -> &Vec<RepoPathBuf> {
786 &self.sparse_patterns
787 }
788
789 fn sparse_matcher(&self) -> Box<dyn Matcher> {
790 Box::new(PrefixMatcher::new(&self.sparse_patterns))
791 }
792
793 pub fn init(
794 store: Arc<Store>,
795 working_copy_path: PathBuf,
796 state_path: PathBuf,
797 ) -> Result<TreeState, TreeStateError> {
798 let mut wc = TreeState::empty(store, working_copy_path, state_path);
799 wc.save()?;
800 Ok(wc)
801 }
802
803 fn empty(store: Arc<Store>, working_copy_path: PathBuf, state_path: PathBuf) -> TreeState {
804 let tree_id = store.empty_merged_tree_id();
805 TreeState {
806 store,
807 working_copy_path,
808 state_path,
809 tree_id,
810 file_states: FileStatesMap::new(),
811 sparse_patterns: vec![RepoPathBuf::root()],
812 own_mtime: MillisSinceEpoch(0),
813 symlink_support: check_symlink_support().unwrap_or(false),
814 watchman_clock: None,
815 }
816 }
817
818 pub fn load(
819 store: Arc<Store>,
820 working_copy_path: PathBuf,
821 state_path: PathBuf,
822 ) -> Result<TreeState, TreeStateError> {
823 let tree_state_path = state_path.join("tree_state");
824 let file = match File::open(&tree_state_path) {
825 Err(ref err) if err.kind() == io::ErrorKind::NotFound => {
826 return TreeState::init(store, working_copy_path, state_path);
827 }
828 Err(err) => {
829 return Err(TreeStateError::ReadTreeState {
830 path: tree_state_path,
831 source: err,
832 });
833 }
834 Ok(file) => file,
835 };
836
837 let mut wc = TreeState::empty(store, working_copy_path, state_path);
838 wc.read(&tree_state_path, file)?;
839 Ok(wc)
840 }
841
842 fn update_own_mtime(&mut self) {
843 if let Ok(metadata) = self.state_path.join("tree_state").symlink_metadata() {
844 self.own_mtime = mtime_from_metadata(&metadata);
845 } else {
846 self.own_mtime = MillisSinceEpoch(0);
847 }
848 }
849
850 fn read(&mut self, tree_state_path: &Path, mut file: File) -> Result<(), TreeStateError> {
851 self.update_own_mtime();
852 let mut buf = Vec::new();
853 file.read_to_end(&mut buf)
854 .map_err(|err| TreeStateError::ReadTreeState {
855 path: tree_state_path.to_owned(),
856 source: err,
857 })?;
858 let proto = crate::protos::working_copy::TreeState::decode(&*buf).map_err(|err| {
859 TreeStateError::DecodeTreeState {
860 path: tree_state_path.to_owned(),
861 source: err,
862 }
863 })?;
864 if proto.tree_ids.is_empty() {
865 self.tree_id = MergedTreeId::Legacy(TreeId::new(proto.legacy_tree_id.clone()));
866 } else {
867 let tree_ids_builder: MergeBuilder<TreeId> = proto
868 .tree_ids
869 .iter()
870 .map(|id| TreeId::new(id.clone()))
871 .collect();
872 self.tree_id = MergedTreeId::Merge(tree_ids_builder.build());
873 }
874 self.file_states =
875 FileStatesMap::from_proto(proto.file_states, proto.is_file_states_sorted);
876 self.sparse_patterns = sparse_patterns_from_proto(proto.sparse_patterns.as_ref());
877 self.watchman_clock = proto.watchman_clock;
878 Ok(())
879 }
880
881 #[expect(clippy::assigning_clones)]
882 fn save(&mut self) -> Result<(), TreeStateError> {
883 let mut proto: crate::protos::working_copy::TreeState = Default::default();
884 match &self.tree_id {
885 MergedTreeId::Legacy(tree_id) => {
886 proto.legacy_tree_id = tree_id.to_bytes();
887 }
888 MergedTreeId::Merge(tree_ids) => {
889 proto.tree_ids = tree_ids.iter().map(|id| id.to_bytes()).collect();
890 }
891 }
892
893 proto.file_states = self.file_states.data.clone();
894 proto.is_file_states_sorted = true;
896 let mut sparse_patterns = crate::protos::working_copy::SparsePatterns::default();
897 for path in &self.sparse_patterns {
898 sparse_patterns
899 .prefixes
900 .push(path.as_internal_file_string().to_owned());
901 }
902 proto.sparse_patterns = Some(sparse_patterns);
903 proto.watchman_clock = self.watchman_clock.clone();
904
905 let mut temp_file = NamedTempFile::new_in(&self.state_path).unwrap();
906 temp_file
907 .as_file_mut()
908 .write_all(&proto.encode_to_vec())
909 .map_err(|err| TreeStateError::WriteTreeState {
910 path: self.state_path.clone(),
911 source: err,
912 })?;
913 self.update_own_mtime();
916 let target_path = self.state_path.join("tree_state");
919 temp_file
920 .persist(&target_path)
921 .map_err(|tempfile::PersistError { error, file: _ }| {
922 TreeStateError::PersistTreeState {
923 path: target_path.clone(),
924 source: error,
925 }
926 })?;
927 Ok(())
928 }
929
930 fn current_tree(&self) -> BackendResult<MergedTree> {
931 self.store.get_root_tree(&self.tree_id)
932 }
933
934 fn reset_watchman(&mut self) {
935 self.watchman_clock.take();
936 }
937
938 #[cfg(feature = "watchman")]
939 #[tokio::main(flavor = "current_thread")]
940 #[instrument(skip(self))]
941 pub async fn query_watchman(
942 &self,
943 config: &WatchmanConfig,
944 ) -> Result<(watchman::Clock, Option<Vec<PathBuf>>), TreeStateError> {
945 let fsmonitor = watchman::Fsmonitor::init(&self.working_copy_path, config)
946 .await
947 .map_err(|err| TreeStateError::Fsmonitor(Box::new(err)))?;
948 let previous_clock = self.watchman_clock.clone().map(watchman::Clock::from);
949 let changed_files = fsmonitor
950 .query_changed_files(previous_clock)
951 .await
952 .map_err(|err| TreeStateError::Fsmonitor(Box::new(err)))?;
953 Ok(changed_files)
954 }
955
956 #[cfg(feature = "watchman")]
957 #[tokio::main(flavor = "current_thread")]
958 #[instrument(skip(self))]
959 pub async fn is_watchman_trigger_registered(
960 &self,
961 config: &WatchmanConfig,
962 ) -> Result<bool, TreeStateError> {
963 let fsmonitor = watchman::Fsmonitor::init(&self.working_copy_path, config)
964 .await
965 .map_err(|err| TreeStateError::Fsmonitor(Box::new(err)))?;
966 fsmonitor
967 .is_trigger_registered()
968 .await
969 .map_err(|err| TreeStateError::Fsmonitor(Box::new(err)))
970 }
971}
972
973impl TreeState {
975 #[instrument(skip_all)]
978 pub fn snapshot(
979 &mut self,
980 options: &SnapshotOptions,
981 ) -> Result<(bool, SnapshotStats), SnapshotError> {
982 let &SnapshotOptions {
983 ref base_ignores,
984 ref fsmonitor_settings,
985 progress,
986 start_tracking_matcher,
987 max_new_file_size,
988 conflict_marker_style,
989 } = options;
990
991 let sparse_matcher = self.sparse_matcher();
992
993 let fsmonitor_clock_needs_save = *fsmonitor_settings != FsmonitorSettings::None;
994 let mut is_dirty = fsmonitor_clock_needs_save;
995 let FsmonitorMatcher {
996 matcher: fsmonitor_matcher,
997 watchman_clock,
998 } = self.make_fsmonitor_matcher(fsmonitor_settings)?;
999 let fsmonitor_matcher = match fsmonitor_matcher.as_ref() {
1000 None => &EverythingMatcher,
1001 Some(fsmonitor_matcher) => fsmonitor_matcher.as_ref(),
1002 };
1003
1004 let matcher = IntersectionMatcher::new(sparse_matcher.as_ref(), fsmonitor_matcher);
1005 if matcher.visit(RepoPath::root()).is_nothing() {
1006 self.watchman_clock = watchman_clock;
1008 return Ok((is_dirty, SnapshotStats::default()));
1009 }
1010
1011 let (tree_entries_tx, tree_entries_rx) = channel();
1012 let (file_states_tx, file_states_rx) = channel();
1013 let (untracked_paths_tx, untracked_paths_rx) = channel();
1014 let (deleted_files_tx, deleted_files_rx) = channel();
1015
1016 trace_span!("traverse filesystem").in_scope(|| -> Result<(), SnapshotError> {
1017 let snapshotter = FileSnapshotter {
1018 tree_state: self,
1019 current_tree: &self.current_tree()?,
1020 matcher: &matcher,
1021 start_tracking_matcher,
1022 tree_entries_tx,
1024 file_states_tx,
1025 untracked_paths_tx,
1026 deleted_files_tx,
1027 error: OnceLock::new(),
1028 progress,
1029 max_new_file_size,
1030 conflict_marker_style,
1031 };
1032 let directory_to_visit = DirectoryToVisit {
1033 dir: RepoPathBuf::root(),
1034 disk_dir: self.working_copy_path.clone(),
1035 git_ignore: base_ignores.clone(),
1036 file_states: self.file_states.all(),
1037 };
1038 rayon::scope(|scope| {
1040 snapshotter.spawn_ok(scope, |scope| {
1041 snapshotter.visit_directory(directory_to_visit, scope)
1042 });
1043 });
1044 snapshotter.into_result()
1045 })?;
1046
1047 let stats = SnapshotStats {
1048 untracked_paths: untracked_paths_rx.into_iter().collect(),
1049 };
1050 let mut tree_builder = MergedTreeBuilder::new(self.tree_id.clone());
1051 trace_span!("process tree entries").in_scope(|| {
1052 for (path, tree_values) in &tree_entries_rx {
1053 tree_builder.set_or_remove(path, tree_values);
1054 }
1055 });
1056 let deleted_files = trace_span!("process deleted tree entries").in_scope(|| {
1057 let deleted_files = HashSet::from_iter(deleted_files_rx);
1058 is_dirty |= !deleted_files.is_empty();
1059 for file in &deleted_files {
1060 tree_builder.set_or_remove(file.clone(), Merge::absent());
1061 }
1062 deleted_files
1063 });
1064 trace_span!("process file states").in_scope(|| {
1065 let changed_file_states = file_states_rx
1066 .iter()
1067 .sorted_unstable_by(|(path1, _), (path2, _)| path1.cmp(path2))
1068 .collect_vec();
1069 is_dirty |= !changed_file_states.is_empty();
1070 self.file_states
1071 .merge_in(changed_file_states, &deleted_files);
1072 });
1073 trace_span!("write tree").in_scope(|| {
1074 let new_tree_id = tree_builder.write_tree(&self.store).unwrap();
1075 is_dirty |= new_tree_id != self.tree_id;
1076 self.tree_id = new_tree_id;
1077 });
1078 if cfg!(debug_assertions) {
1079 let tree = self.current_tree().unwrap();
1080 let tree_paths: HashSet<_> = tree
1081 .entries_matching(sparse_matcher.as_ref())
1082 .filter_map(|(path, result)| result.is_ok().then_some(path))
1083 .collect();
1084 let file_states = self.file_states.all();
1085 let state_paths: HashSet<_> = file_states.paths().map(|path| path.to_owned()).collect();
1086 assert_eq!(state_paths, tree_paths);
1087 }
1088 if stats.untracked_paths.is_empty() || watchman_clock.is_none() {
1092 self.watchman_clock = watchman_clock;
1093 } else {
1094 tracing::info!("not updating watchman clock because there are untracked files");
1095 }
1096 Ok((is_dirty, stats))
1097 }
1098
1099 #[instrument(skip_all)]
1100 fn make_fsmonitor_matcher(
1101 &self,
1102 fsmonitor_settings: &FsmonitorSettings,
1103 ) -> Result<FsmonitorMatcher, SnapshotError> {
1104 let (watchman_clock, changed_files) = match fsmonitor_settings {
1105 FsmonitorSettings::None => (None, None),
1106 FsmonitorSettings::Test { changed_files } => (None, Some(changed_files.clone())),
1107 #[cfg(feature = "watchman")]
1108 FsmonitorSettings::Watchman(config) => match self.query_watchman(config) {
1109 Ok((watchman_clock, changed_files)) => (Some(watchman_clock.into()), changed_files),
1110 Err(err) => {
1111 tracing::warn!(?err, "Failed to query filesystem monitor");
1112 (None, None)
1113 }
1114 },
1115 #[cfg(not(feature = "watchman"))]
1116 FsmonitorSettings::Watchman(_) => {
1117 return Err(SnapshotError::Other {
1118 message: "Failed to query the filesystem monitor".to_string(),
1119 err: "Cannot query Watchman because jj was not compiled with the `watchman` \
1120 feature (consider disabling `core.fsmonitor`)"
1121 .into(),
1122 });
1123 }
1124 };
1125 let matcher: Option<Box<dyn Matcher>> = match changed_files {
1126 None => None,
1127 Some(changed_files) => {
1128 let repo_paths = trace_span!("processing fsmonitor paths").in_scope(|| {
1129 changed_files
1130 .into_iter()
1131 .filter_map(|path| RepoPathBuf::from_relative_path(path).ok())
1132 .collect_vec()
1133 });
1134
1135 Some(Box::new(FilesMatcher::new(repo_paths)))
1136 }
1137 };
1138 Ok(FsmonitorMatcher {
1139 matcher,
1140 watchman_clock,
1141 })
1142 }
1143}
1144
1145struct DirectoryToVisit<'a> {
1146 dir: RepoPathBuf,
1147 disk_dir: PathBuf,
1148 git_ignore: Arc<GitIgnoreFile>,
1149 file_states: FileStates<'a>,
1150}
1151
1152#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1153enum PresentDirEntryKind {
1154 Dir,
1155 File,
1156}
1157
1158#[derive(Clone, Debug)]
1159struct PresentDirEntries {
1160 dirs: HashSet<String>,
1161 files: HashSet<String>,
1162}
1163
1164struct FileSnapshotter<'a> {
1166 tree_state: &'a TreeState,
1167 current_tree: &'a MergedTree,
1168 matcher: &'a dyn Matcher,
1169 start_tracking_matcher: &'a dyn Matcher,
1170 tree_entries_tx: Sender<(RepoPathBuf, MergedTreeValue)>,
1171 file_states_tx: Sender<(RepoPathBuf, FileState)>,
1172 untracked_paths_tx: Sender<(RepoPathBuf, UntrackedReason)>,
1173 deleted_files_tx: Sender<RepoPathBuf>,
1174 error: OnceLock<SnapshotError>,
1175 progress: Option<&'a SnapshotProgress<'a>>,
1176 max_new_file_size: u64,
1177 conflict_marker_style: ConflictMarkerStyle,
1178}
1179
1180impl FileSnapshotter<'_> {
1181 fn spawn_ok<'scope, F>(&'scope self, scope: &rayon::Scope<'scope>, body: F)
1182 where
1183 F: FnOnce(&rayon::Scope<'scope>) -> Result<(), SnapshotError> + Send + 'scope,
1184 {
1185 scope.spawn(|scope| {
1186 if self.error.get().is_some() {
1187 return;
1188 }
1189 match body(scope) {
1190 Ok(()) => {}
1191 Err(err) => self.error.set(err).unwrap_or(()),
1192 };
1193 });
1194 }
1195
1196 fn into_result(self) -> Result<(), SnapshotError> {
1198 match self.error.into_inner() {
1199 Some(err) => Err(err),
1200 None => Ok(()),
1201 }
1202 }
1203
1204 fn visit_directory<'scope>(
1207 &'scope self,
1208 directory_to_visit: DirectoryToVisit<'scope>,
1209 scope: &rayon::Scope<'scope>,
1210 ) -> Result<(), SnapshotError> {
1211 let DirectoryToVisit {
1212 dir,
1213 disk_dir,
1214 git_ignore,
1215 file_states,
1216 } = directory_to_visit;
1217
1218 let git_ignore = git_ignore
1219 .chain_with_file(&dir.to_internal_dir_string(), disk_dir.join(".gitignore"))?;
1220 let dir_entries: Vec<_> = disk_dir
1221 .read_dir()
1222 .and_then(|entries| entries.try_collect())
1223 .map_err(|err| SnapshotError::Other {
1224 message: format!("Failed to read directory {}", disk_dir.display()),
1225 err: err.into(),
1226 })?;
1227 let (dirs, files) = dir_entries
1228 .into_par_iter()
1229 .with_min_len(100)
1232 .filter_map(|entry| {
1233 self.process_dir_entry(&dir, &git_ignore, file_states, &entry, scope)
1234 .transpose()
1235 })
1236 .map(|item| match item {
1237 Ok((PresentDirEntryKind::Dir, name)) => Ok(Either::Left(name)),
1238 Ok((PresentDirEntryKind::File, name)) => Ok(Either::Right(name)),
1239 Err(err) => Err(err),
1240 })
1241 .collect::<Result<_, _>>()?;
1242 let present_entries = PresentDirEntries { dirs, files };
1243 self.emit_deleted_files(&dir, file_states, &present_entries);
1244 Ok(())
1245 }
1246
1247 fn process_dir_entry<'scope>(
1248 &'scope self,
1249 dir: &RepoPath,
1250 git_ignore: &Arc<GitIgnoreFile>,
1251 file_states: FileStates<'scope>,
1252 entry: &DirEntry,
1253 scope: &rayon::Scope<'scope>,
1254 ) -> Result<Option<(PresentDirEntryKind, String)>, SnapshotError> {
1255 let file_type = entry.file_type().unwrap();
1256 let file_name = entry.file_name();
1257 let name_string = file_name
1258 .into_string()
1259 .map_err(|path| SnapshotError::InvalidUtf8Path { path })?;
1260
1261 if RESERVED_DIR_NAMES.contains(&name_string.as_str()) {
1262 return Ok(None);
1263 }
1264 let name = RepoPathComponent::new(&name_string).unwrap();
1265 let path = dir.join(name);
1266 let maybe_current_file_state = file_states.get_at(dir, name);
1267 if let Some(file_state) = &maybe_current_file_state {
1268 if file_state.file_type == FileType::GitSubmodule {
1269 return Ok(None);
1270 }
1271 }
1272
1273 if file_type.is_dir() {
1274 let file_states = file_states.prefixed_at(dir, name);
1275 if git_ignore.matches(&path.to_internal_dir_string()) {
1276 self.spawn_ok(scope, move |_| self.visit_tracked_files(file_states));
1282 } else if !self.matcher.visit(&path).is_nothing() {
1283 let directory_to_visit = DirectoryToVisit {
1284 dir: path,
1285 disk_dir: entry.path(),
1286 git_ignore: git_ignore.clone(),
1287 file_states,
1288 };
1289 self.spawn_ok(scope, |scope| {
1290 self.visit_directory(directory_to_visit, scope)
1291 });
1292 }
1293 Ok(Some((PresentDirEntryKind::Dir, name_string)))
1296 } else if self.matcher.matches(&path) {
1297 if let Some(progress) = self.progress {
1298 progress(&path);
1299 }
1300 if maybe_current_file_state.is_none()
1301 && git_ignore.matches(path.as_internal_file_string())
1302 {
1303 Ok(None)
1306 } else if maybe_current_file_state.is_none()
1307 && !self.start_tracking_matcher.matches(&path)
1308 {
1309 self.untracked_paths_tx
1311 .send((path, UntrackedReason::FileNotAutoTracked))
1312 .ok();
1313 Ok(None)
1314 } else {
1315 let metadata = entry.metadata().map_err(|err| SnapshotError::Other {
1316 message: format!("Failed to stat file {}", entry.path().display()),
1317 err: err.into(),
1318 })?;
1319 if maybe_current_file_state.is_none() && metadata.len() > self.max_new_file_size {
1320 let reason = UntrackedReason::FileTooLarge {
1322 size: metadata.len(),
1323 max_size: self.max_new_file_size,
1324 };
1325 self.untracked_paths_tx.send((path, reason)).ok();
1326 Ok(None)
1327 } else if let Some(new_file_state) = file_state(&metadata) {
1328 self.process_present_file(
1329 path,
1330 &entry.path(),
1331 maybe_current_file_state.as_ref(),
1332 new_file_state,
1333 )?;
1334 Ok(Some((PresentDirEntryKind::File, name_string)))
1335 } else {
1336 Ok(None)
1338 }
1339 }
1340 } else {
1341 Ok(None)
1342 }
1343 }
1344
1345 fn visit_tracked_files(&self, file_states: FileStates<'_>) -> Result<(), SnapshotError> {
1347 for (tracked_path, current_file_state) in file_states {
1348 if current_file_state.file_type == FileType::GitSubmodule {
1349 continue;
1350 }
1351 if !self.matcher.matches(tracked_path) {
1352 continue;
1353 }
1354 let disk_path = tracked_path.to_fs_path(&self.tree_state.working_copy_path)?;
1355 let metadata = match disk_path.symlink_metadata() {
1356 Ok(metadata) => Some(metadata),
1357 Err(err) if err.kind() == io::ErrorKind::NotFound => None,
1358 Err(err) => {
1359 return Err(SnapshotError::Other {
1360 message: format!("Failed to stat file {}", disk_path.display()),
1361 err: err.into(),
1362 });
1363 }
1364 };
1365 if let Some(new_file_state) = metadata.as_ref().and_then(file_state) {
1366 self.process_present_file(
1367 tracked_path.to_owned(),
1368 &disk_path,
1369 Some(¤t_file_state),
1370 new_file_state,
1371 )?;
1372 } else {
1373 self.deleted_files_tx.send(tracked_path.to_owned()).ok();
1374 }
1375 }
1376 Ok(())
1377 }
1378
1379 fn process_present_file(
1380 &self,
1381 path: RepoPathBuf,
1382 disk_path: &Path,
1383 maybe_current_file_state: Option<&FileState>,
1384 mut new_file_state: FileState,
1385 ) -> Result<(), SnapshotError> {
1386 let update = self.get_updated_tree_value(
1387 &path,
1388 disk_path,
1389 maybe_current_file_state,
1390 &new_file_state,
1391 )?;
1392 if matches!(new_file_state.file_type, FileType::Normal { .. })
1394 && !update.as_ref().is_some_and(|update| update.is_resolved())
1395 {
1396 new_file_state.materialized_conflict_data =
1397 maybe_current_file_state.and_then(|state| state.materialized_conflict_data);
1398 }
1399 if let Some(tree_value) = update {
1400 self.tree_entries_tx.send((path.clone(), tree_value)).ok();
1401 }
1402 if Some(&new_file_state) != maybe_current_file_state {
1403 self.file_states_tx.send((path, new_file_state)).ok();
1404 }
1405 Ok(())
1406 }
1407
1408 fn emit_deleted_files(
1410 &self,
1411 dir: &RepoPath,
1412 file_states: FileStates<'_>,
1413 present_entries: &PresentDirEntries,
1414 ) {
1415 let file_state_chunks = file_states.iter().chunk_by(|(path, _state)| {
1416 debug_assert!(path.starts_with(dir));
1419 let slash = !dir.is_root() as usize;
1420 let len = dir.as_internal_file_string().len() + slash;
1421 let tail = path.as_internal_file_string().get(len..).unwrap_or("");
1422 match tail.split_once('/') {
1423 Some((name, _)) => (PresentDirEntryKind::Dir, name),
1424 None => (PresentDirEntryKind::File, tail),
1425 }
1426 });
1427 file_state_chunks
1428 .into_iter()
1429 .filter(|&((kind, name), _)| match kind {
1430 PresentDirEntryKind::Dir => !present_entries.dirs.contains(name),
1431 PresentDirEntryKind::File => !present_entries.files.contains(name),
1432 })
1433 .flat_map(|(_, chunk)| chunk)
1434 .filter(|(_, state)| state.file_type != FileType::GitSubmodule)
1436 .filter(|(path, _)| self.matcher.matches(path))
1437 .try_for_each(|(path, _)| self.deleted_files_tx.send(path.to_owned()))
1438 .ok();
1439 }
1440
1441 fn get_updated_tree_value(
1442 &self,
1443 repo_path: &RepoPath,
1444 disk_path: &Path,
1445 maybe_current_file_state: Option<&FileState>,
1446 new_file_state: &FileState,
1447 ) -> Result<Option<MergedTreeValue>, SnapshotError> {
1448 let clean = match maybe_current_file_state {
1449 None => {
1450 false
1452 }
1453 Some(current_file_state) => {
1454 new_file_state.is_clean(current_file_state)
1457 && current_file_state.mtime < self.tree_state.own_mtime
1458 }
1459 };
1460 if clean {
1461 Ok(None)
1462 } else {
1463 let current_tree_values = self.current_tree.path_value(repo_path)?;
1464 let new_file_type = if !self.tree_state.symlink_support {
1465 let mut new_file_type = new_file_state.file_type.clone();
1466 if matches!(new_file_type, FileType::Normal { .. })
1467 && matches!(current_tree_values.as_normal(), Some(TreeValue::Symlink(_)))
1468 {
1469 new_file_type = FileType::Symlink;
1470 }
1471 new_file_type
1472 } else {
1473 new_file_state.file_type.clone()
1474 };
1475 let new_tree_values = match new_file_type {
1476 FileType::Normal { executable } => self
1477 .write_path_to_store(
1478 repo_path,
1479 disk_path,
1480 ¤t_tree_values,
1481 executable,
1482 maybe_current_file_state.and_then(|state| state.materialized_conflict_data),
1483 )
1484 .block_on()?,
1485 FileType::Symlink => {
1486 let id = self
1487 .write_symlink_to_store(repo_path, disk_path)
1488 .block_on()?;
1489 Merge::normal(TreeValue::Symlink(id))
1490 }
1491 FileType::GitSubmodule => panic!("git submodule cannot be written to store"),
1492 };
1493 if new_tree_values != current_tree_values {
1494 Ok(Some(new_tree_values))
1495 } else {
1496 Ok(None)
1497 }
1498 }
1499 }
1500
1501 fn store(&self) -> &Store {
1502 &self.tree_state.store
1503 }
1504
1505 async fn write_path_to_store(
1506 &self,
1507 repo_path: &RepoPath,
1508 disk_path: &Path,
1509 current_tree_values: &MergedTreeValue,
1510 executable: FileExecutableFlag,
1511 materialized_conflict_data: Option<MaterializedConflictData>,
1512 ) -> Result<MergedTreeValue, SnapshotError> {
1513 if let Some(current_tree_value) = current_tree_values.as_resolved() {
1514 let id = self.write_file_to_store(repo_path, disk_path).await?;
1515 let executable = executable.unwrap_or_else(|| {
1517 if let Some(TreeValue::File {
1518 id: _,
1519 executable,
1520 copy_id: _,
1521 }) = current_tree_value
1522 {
1523 *executable
1524 } else {
1525 false
1526 }
1527 });
1528 let copy_id = {
1530 if let Some(TreeValue::File {
1531 id: _,
1532 executable: _,
1533 copy_id,
1534 }) = current_tree_value
1535 {
1536 copy_id.clone()
1537 } else {
1538 CopyId::placeholder()
1539 }
1540 };
1541 Ok(Merge::normal(TreeValue::File {
1542 id,
1543 executable,
1544 copy_id,
1545 }))
1546 } else if let Some(old_file_ids) = current_tree_values.to_file_merge() {
1547 let copy_id_merge = current_tree_values.to_copy_id_merge().unwrap();
1549 let copy_id = copy_id_merge
1550 .resolve_trivial()
1551 .cloned()
1552 .flatten()
1553 .unwrap_or_else(CopyId::placeholder);
1554 let content = fs::read(disk_path).map_err(|err| SnapshotError::Other {
1558 message: format!("Failed to open file {}", disk_path.display()),
1559 err: err.into(),
1560 })?;
1561 let new_file_ids = conflicts::update_from_content(
1562 &old_file_ids,
1563 self.store(),
1564 repo_path,
1565 &content,
1566 self.conflict_marker_style,
1567 materialized_conflict_data.map_or(MIN_CONFLICT_MARKER_LEN, |data| {
1568 data.conflict_marker_len as usize
1569 }),
1570 )
1571 .await?;
1572 match new_file_ids.into_resolved() {
1573 Ok(file_id) => {
1574 let executable = executable.unwrap_or_else(|| {
1576 if let Some(merge) = current_tree_values.to_executable_merge() {
1577 conflicts::resolve_file_executable(&merge).unwrap_or(false)
1578 } else {
1579 false
1580 }
1581 });
1582 Ok(Merge::normal(TreeValue::File {
1583 id: file_id.unwrap(),
1584 executable,
1585 copy_id,
1586 }))
1587 }
1588 Err(new_file_ids) => {
1589 if new_file_ids != old_file_ids {
1590 Ok(current_tree_values.with_new_file_ids(&new_file_ids))
1591 } else {
1592 Ok(current_tree_values.clone())
1593 }
1594 }
1595 }
1596 } else {
1597 Ok(current_tree_values.clone())
1598 }
1599 }
1600
1601 async fn write_file_to_store(
1602 &self,
1603 path: &RepoPath,
1604 disk_path: &Path,
1605 ) -> Result<FileId, SnapshotError> {
1606 let file = File::open(disk_path).map_err(|err| SnapshotError::Other {
1607 message: format!("Failed to open file {}", disk_path.display()),
1608 err: err.into(),
1609 })?;
1610 Ok(self
1611 .store()
1612 .write_file(path, &mut BlockingAsyncReader::new(file))
1613 .await?)
1614 }
1615
1616 async fn write_symlink_to_store(
1617 &self,
1618 path: &RepoPath,
1619 disk_path: &Path,
1620 ) -> Result<SymlinkId, SnapshotError> {
1621 if self.tree_state.symlink_support {
1622 let target = disk_path.read_link().map_err(|err| SnapshotError::Other {
1623 message: format!("Failed to read symlink {}", disk_path.display()),
1624 err: err.into(),
1625 })?;
1626 let str_target =
1627 target
1628 .to_str()
1629 .ok_or_else(|| SnapshotError::InvalidUtf8SymlinkTarget {
1630 path: disk_path.to_path_buf(),
1631 })?;
1632 Ok(self.store().write_symlink(path, str_target).await?)
1633 } else {
1634 let target = fs::read(disk_path).map_err(|err| SnapshotError::Other {
1635 message: format!("Failed to read file {}", disk_path.display()),
1636 err: err.into(),
1637 })?;
1638 let string_target =
1639 String::from_utf8(target).map_err(|_| SnapshotError::InvalidUtf8SymlinkTarget {
1640 path: disk_path.to_path_buf(),
1641 })?;
1642 Ok(self.store().write_symlink(path, &string_target).await?)
1643 }
1644 }
1645}
1646
1647impl TreeState {
1649 fn write_file(
1650 &self,
1651 disk_path: &Path,
1652 contents: impl AsyncRead,
1653 executable: bool,
1654 ) -> Result<FileState, CheckoutError> {
1655 let mut file = OpenOptions::new()
1656 .write(true)
1657 .create_new(true) .open(disk_path)
1659 .map_err(|err| CheckoutError::Other {
1660 message: format!("Failed to open file {} for writing", disk_path.display()),
1661 err: err.into(),
1662 })?;
1663 let size = copy_async_to_sync(contents, &mut file)
1664 .block_on()
1665 .map_err(|err| CheckoutError::Other {
1666 message: format!("Failed to write file {}", disk_path.display()),
1667 err: err.into(),
1668 })?;
1669 self.set_executable(disk_path, executable)?;
1670 let metadata = file
1675 .metadata()
1676 .map_err(|err| checkout_error_for_stat_error(err, disk_path))?;
1677 Ok(FileState::for_file(
1678 executable,
1679 size as u64,
1680 &metadata,
1681 None,
1682 ))
1683 }
1684
1685 fn write_symlink(&self, disk_path: &Path, target: String) -> Result<FileState, CheckoutError> {
1686 let target = PathBuf::from(&target);
1687 try_symlink(&target, disk_path).map_err(|err| CheckoutError::Other {
1688 message: format!(
1689 "Failed to create symlink from {} to {}",
1690 disk_path.display(),
1691 target.display()
1692 ),
1693 err: err.into(),
1694 })?;
1695 let metadata = disk_path
1696 .symlink_metadata()
1697 .map_err(|err| checkout_error_for_stat_error(err, disk_path))?;
1698 Ok(FileState::for_symlink(&metadata))
1699 }
1700
1701 fn write_conflict(
1702 &self,
1703 disk_path: &Path,
1704 conflict_data: Vec<u8>,
1705 executable: bool,
1706 materialized_conflict_data: Option<MaterializedConflictData>,
1707 ) -> Result<FileState, CheckoutError> {
1708 let mut file = OpenOptions::new()
1709 .write(true)
1710 .create_new(true) .open(disk_path)
1712 .map_err(|err| CheckoutError::Other {
1713 message: format!("Failed to open file {} for writing", disk_path.display()),
1714 err: err.into(),
1715 })?;
1716 file.write_all(&conflict_data)
1717 .map_err(|err| CheckoutError::Other {
1718 message: format!("Failed to write conflict to file {}", disk_path.display()),
1719 err: err.into(),
1720 })?;
1721 let size = conflict_data.len() as u64;
1722 self.set_executable(disk_path, executable)?;
1723 let metadata = file
1724 .metadata()
1725 .map_err(|err| checkout_error_for_stat_error(err, disk_path))?;
1726 Ok(FileState::for_file(
1727 executable,
1728 size,
1729 &metadata,
1730 materialized_conflict_data,
1731 ))
1732 }
1733
1734 #[cfg_attr(windows, allow(unused_variables))]
1735 fn set_executable(&self, disk_path: &Path, executable: bool) -> Result<(), CheckoutError> {
1736 #[cfg(unix)]
1737 {
1738 let mode = if executable { 0o755 } else { 0o644 };
1739 fs::set_permissions(disk_path, fs::Permissions::from_mode(mode))
1740 .map_err(|err| checkout_error_for_stat_error(err, disk_path))?;
1741 }
1742 Ok(())
1743 }
1744
1745 pub fn check_out(
1746 &mut self,
1747 new_tree: &MergedTree,
1748 options: &CheckoutOptions,
1749 ) -> Result<CheckoutStats, CheckoutError> {
1750 let old_tree = self.current_tree().map_err(|err| match err {
1751 err @ BackendError::ObjectNotFound { .. } => CheckoutError::SourceNotFound {
1752 source: Box::new(err),
1753 },
1754 other => CheckoutError::InternalBackendError(other),
1755 })?;
1756 let stats = self
1757 .update(
1758 &old_tree,
1759 new_tree,
1760 self.sparse_matcher().as_ref(),
1761 options.conflict_marker_style,
1762 )
1763 .block_on()?;
1764 self.tree_id = new_tree.id();
1765 Ok(stats)
1766 }
1767
1768 pub fn set_sparse_patterns(
1769 &mut self,
1770 sparse_patterns: Vec<RepoPathBuf>,
1771 options: &CheckoutOptions,
1772 ) -> Result<CheckoutStats, CheckoutError> {
1773 let tree = self.current_tree().map_err(|err| match err {
1774 err @ BackendError::ObjectNotFound { .. } => CheckoutError::SourceNotFound {
1775 source: Box::new(err),
1776 },
1777 other => CheckoutError::InternalBackendError(other),
1778 })?;
1779 let old_matcher = PrefixMatcher::new(&self.sparse_patterns);
1780 let new_matcher = PrefixMatcher::new(&sparse_patterns);
1781 let added_matcher = DifferenceMatcher::new(&new_matcher, &old_matcher);
1782 let removed_matcher = DifferenceMatcher::new(&old_matcher, &new_matcher);
1783 let empty_tree = MergedTree::resolved(Tree::empty(self.store.clone(), RepoPathBuf::root()));
1784 let added_stats = self
1785 .update(
1786 &empty_tree,
1787 &tree,
1788 &added_matcher,
1789 options.conflict_marker_style,
1790 )
1791 .block_on()?;
1792 let removed_stats = self
1793 .update(
1794 &tree,
1795 &empty_tree,
1796 &removed_matcher,
1797 options.conflict_marker_style,
1798 )
1799 .block_on()?;
1800 self.sparse_patterns = sparse_patterns;
1801 assert_eq!(added_stats.updated_files, 0);
1802 assert_eq!(added_stats.removed_files, 0);
1803 assert_eq!(removed_stats.updated_files, 0);
1804 assert_eq!(removed_stats.added_files, 0);
1805 assert_eq!(removed_stats.skipped_files, 0);
1806 Ok(CheckoutStats {
1807 updated_files: 0,
1808 added_files: added_stats.added_files,
1809 removed_files: removed_stats.removed_files,
1810 skipped_files: added_stats.skipped_files,
1811 })
1812 }
1813
1814 async fn update(
1815 &mut self,
1816 old_tree: &MergedTree,
1817 new_tree: &MergedTree,
1818 matcher: &dyn Matcher,
1819 conflict_marker_style: ConflictMarkerStyle,
1820 ) -> Result<CheckoutStats, CheckoutError> {
1821 let mut stats = CheckoutStats {
1824 updated_files: 0,
1825 added_files: 0,
1826 removed_files: 0,
1827 skipped_files: 0,
1828 };
1829 let mut changed_file_states = Vec::new();
1830 let mut deleted_files = HashSet::new();
1831 let mut diff_stream = old_tree
1832 .diff_stream(new_tree, matcher)
1833 .map(|TreeDiffEntry { path, values }| async {
1834 match values {
1835 Ok((before, after)) => {
1836 let result = materialize_tree_value(&self.store, &path, after).await;
1837 (path, result.map(|value| (before, value)))
1838 }
1839 Err(err) => (path, Err(err)),
1840 }
1841 })
1842 .buffered(self.store.concurrency().max(1));
1843 while let Some((path, data)) = diff_stream.next().await {
1844 let (before, after) = data?;
1845 if after.is_absent() {
1846 stats.removed_files += 1;
1847 } else if before.is_absent() {
1848 stats.added_files += 1;
1849 } else {
1850 stats.updated_files += 1;
1851 }
1852
1853 if matches!(before.as_normal(), Some(TreeValue::GitSubmodule(_)))
1861 && matches!(after, MaterializedTreeValue::GitSubmodule(_))
1862 {
1863 eprintln!("ignoring git submodule at {path:?}");
1864 continue;
1867 }
1868
1869 let Some(disk_path) = create_parent_dirs(&self.working_copy_path, &path)? else {
1872 changed_file_states.push((path, FileState::placeholder()));
1873 stats.skipped_files += 1;
1874 continue;
1875 };
1876 let present_file_deleted = before.is_present() && remove_old_file(&disk_path)?;
1878 if !present_file_deleted && !can_create_new_file(&disk_path)? {
1880 changed_file_states.push((path, FileState::placeholder()));
1881 stats.skipped_files += 1;
1882 continue;
1883 }
1884
1885 let file_state = match after {
1887 MaterializedTreeValue::Absent | MaterializedTreeValue::AccessDenied(_) => {
1888 let mut parent_dir = disk_path.parent().unwrap();
1889 loop {
1890 if fs::remove_dir(parent_dir).is_err() {
1891 break;
1892 }
1893 parent_dir = parent_dir.parent().unwrap();
1894 }
1895 deleted_files.insert(path);
1896 continue;
1897 }
1898 MaterializedTreeValue::File(file) => {
1899 self.write_file(&disk_path, file.reader, file.executable)?
1900 }
1901 MaterializedTreeValue::Symlink { id: _, target } => {
1902 if self.symlink_support {
1903 self.write_symlink(&disk_path, target)?
1904 } else {
1905 self.write_file(&disk_path, target.as_bytes(), false)?
1906 }
1907 }
1908 MaterializedTreeValue::GitSubmodule(_) => {
1909 eprintln!("ignoring git submodule at {path:?}");
1910 FileState::for_gitsubmodule()
1911 }
1912 MaterializedTreeValue::Tree(_) => {
1913 panic!("unexpected tree entry in diff at {path:?}");
1914 }
1915 MaterializedTreeValue::FileConflict(file) => {
1916 let conflict_marker_len =
1917 choose_materialized_conflict_marker_len(&file.contents);
1918 let data = materialize_merge_result_to_bytes_with_marker_len(
1919 &file.contents,
1920 conflict_marker_style,
1921 conflict_marker_len,
1922 )
1923 .into();
1924 let materialized_conflict_data = MaterializedConflictData {
1925 conflict_marker_len: conflict_marker_len.try_into().unwrap_or(u32::MAX),
1926 };
1927 self.write_conflict(
1928 &disk_path,
1929 data,
1930 file.executable.unwrap_or(false),
1931 Some(materialized_conflict_data),
1932 )?
1933 }
1934 MaterializedTreeValue::OtherConflict { id } => {
1935 let data = id.describe().into_bytes();
1938 let executable = false;
1939 self.write_conflict(&disk_path, data, executable, None)?
1940 }
1941 };
1942 changed_file_states.push((path, file_state));
1943 }
1944 self.file_states
1945 .merge_in(changed_file_states, &deleted_files);
1946 Ok(stats)
1947 }
1948
1949 pub async fn reset(&mut self, new_tree: &MergedTree) -> Result<(), ResetError> {
1950 let old_tree = self.current_tree().map_err(|err| match err {
1951 err @ BackendError::ObjectNotFound { .. } => ResetError::SourceNotFound {
1952 source: Box::new(err),
1953 },
1954 other => ResetError::InternalBackendError(other),
1955 })?;
1956
1957 let matcher = self.sparse_matcher();
1958 let mut changed_file_states = Vec::new();
1959 let mut deleted_files = HashSet::new();
1960 let mut diff_stream = old_tree.diff_stream(new_tree, matcher.as_ref());
1961 while let Some(TreeDiffEntry { path, values }) = diff_stream.next().await {
1962 let (_before, after) = values?;
1963 if after.is_absent() {
1964 deleted_files.insert(path);
1965 } else {
1966 let file_type = match after.into_resolved() {
1967 Ok(value) => match value.unwrap() {
1968 TreeValue::File {
1969 id: _,
1970 executable,
1971 copy_id: _,
1972 } => FileType::Normal {
1973 executable: FileExecutableFlag::from_bool_lossy(executable),
1974 },
1975 TreeValue::Symlink(_id) => FileType::Symlink,
1976 TreeValue::Conflict(_id) => {
1977 panic!("unexpected conflict entry in diff at {path:?}");
1978 }
1979 TreeValue::GitSubmodule(_id) => {
1980 eprintln!("ignoring git submodule at {path:?}");
1981 FileType::GitSubmodule
1982 }
1983 TreeValue::Tree(_id) => {
1984 panic!("unexpected tree entry in diff at {path:?}");
1985 }
1986 },
1987 Err(_values) => {
1988 FileType::Normal {
1990 executable: FileExecutableFlag::from_bool_lossy(false),
1991 }
1992 }
1993 };
1994 let file_state = FileState {
1995 file_type,
1996 mtime: MillisSinceEpoch(0),
1997 size: 0,
1998 materialized_conflict_data: None,
1999 };
2000 changed_file_states.push((path, file_state));
2001 }
2002 }
2003 self.file_states
2004 .merge_in(changed_file_states, &deleted_files);
2005 self.tree_id = new_tree.id();
2006 Ok(())
2007 }
2008
2009 pub async fn recover(&mut self, new_tree: &MergedTree) -> Result<(), ResetError> {
2010 self.file_states.clear();
2011 self.tree_id = self.store.empty_merged_tree_id();
2012 self.reset(new_tree).await
2013 }
2014}
2015
2016fn checkout_error_for_stat_error(err: io::Error, path: &Path) -> CheckoutError {
2017 CheckoutError::Other {
2018 message: format!("Failed to stat file {}", path.display()),
2019 err: err.into(),
2020 }
2021}
2022
2023#[derive(Clone, Debug)]
2025struct CheckoutState {
2026 operation_id: OperationId,
2027 workspace_name: WorkspaceNameBuf,
2028}
2029
2030pub struct LocalWorkingCopy {
2031 store: Arc<Store>,
2032 working_copy_path: PathBuf,
2033 state_path: PathBuf,
2034 checkout_state: OnceCell<CheckoutState>,
2035 tree_state: OnceCell<TreeState>,
2036}
2037
2038impl WorkingCopy for LocalWorkingCopy {
2039 fn as_any(&self) -> &dyn Any {
2040 self
2041 }
2042
2043 fn name(&self) -> &str {
2044 Self::name()
2045 }
2046
2047 fn workspace_name(&self) -> &WorkspaceName {
2048 &self.checkout_state().workspace_name
2049 }
2050
2051 fn operation_id(&self) -> &OperationId {
2052 &self.checkout_state().operation_id
2053 }
2054
2055 fn tree_id(&self) -> Result<&MergedTreeId, WorkingCopyStateError> {
2056 Ok(self.tree_state()?.current_tree_id())
2057 }
2058
2059 fn sparse_patterns(&self) -> Result<&[RepoPathBuf], WorkingCopyStateError> {
2060 Ok(self.tree_state()?.sparse_patterns())
2061 }
2062
2063 fn start_mutation(&self) -> Result<Box<dyn LockedWorkingCopy>, WorkingCopyStateError> {
2064 let lock_path = self.state_path.join("working_copy.lock");
2065 let lock = FileLock::lock(lock_path).map_err(|err| WorkingCopyStateError {
2066 message: "Failed to lock working copy".to_owned(),
2067 err: err.into(),
2068 })?;
2069
2070 let wc = LocalWorkingCopy {
2071 store: self.store.clone(),
2072 working_copy_path: self.working_copy_path.clone(),
2073 state_path: self.state_path.clone(),
2074 checkout_state: OnceCell::new(),
2076 tree_state: OnceCell::new(),
2079 };
2080 let old_operation_id = wc.operation_id().clone();
2081 let old_tree_id = wc.tree_id()?.clone();
2082 Ok(Box::new(LockedLocalWorkingCopy {
2083 wc,
2084 lock,
2085 old_operation_id,
2086 old_tree_id,
2087 tree_state_dirty: false,
2088 new_workspace_name: None,
2089 }))
2090 }
2091}
2092
2093impl LocalWorkingCopy {
2094 pub fn name() -> &'static str {
2095 "local"
2096 }
2097
2098 pub fn init(
2102 store: Arc<Store>,
2103 working_copy_path: PathBuf,
2104 state_path: PathBuf,
2105 operation_id: OperationId,
2106 workspace_name: WorkspaceNameBuf,
2107 ) -> Result<LocalWorkingCopy, WorkingCopyStateError> {
2108 let proto = crate::protos::working_copy::Checkout {
2109 operation_id: operation_id.to_bytes(),
2110 workspace_name: workspace_name.into(),
2111 };
2112 let mut file = OpenOptions::new()
2113 .create_new(true)
2114 .write(true)
2115 .open(state_path.join("checkout"))
2116 .unwrap();
2117 file.write_all(&proto.encode_to_vec()).unwrap();
2118 let tree_state =
2119 TreeState::init(store.clone(), working_copy_path.clone(), state_path.clone()).map_err(
2120 |err| WorkingCopyStateError {
2121 message: "Failed to initialize working copy state".to_string(),
2122 err: err.into(),
2123 },
2124 )?;
2125 Ok(LocalWorkingCopy {
2126 store,
2127 working_copy_path,
2128 state_path,
2129 checkout_state: OnceCell::new(),
2130 tree_state: OnceCell::with_value(tree_state),
2131 })
2132 }
2133
2134 pub fn load(
2135 store: Arc<Store>,
2136 working_copy_path: PathBuf,
2137 state_path: PathBuf,
2138 ) -> LocalWorkingCopy {
2139 LocalWorkingCopy {
2140 store,
2141 working_copy_path,
2142 state_path,
2143 checkout_state: OnceCell::new(),
2144 tree_state: OnceCell::new(),
2145 }
2146 }
2147
2148 pub fn state_path(&self) -> &Path {
2149 &self.state_path
2150 }
2151
2152 fn write_proto(&self, proto: crate::protos::working_copy::Checkout) {
2153 let mut temp_file = NamedTempFile::new_in(&self.state_path).unwrap();
2154 temp_file
2155 .as_file_mut()
2156 .write_all(&proto.encode_to_vec())
2157 .unwrap();
2158 temp_file.persist(self.state_path.join("checkout")).unwrap();
2161 }
2162
2163 fn checkout_state(&self) -> &CheckoutState {
2164 self.checkout_state.get_or_init(|| {
2165 let buf = fs::read(self.state_path.join("checkout")).unwrap();
2166 let proto = crate::protos::working_copy::Checkout::decode(&*buf).unwrap();
2167 CheckoutState {
2168 operation_id: OperationId::new(proto.operation_id),
2169 workspace_name: if proto.workspace_name.is_empty() {
2170 WorkspaceName::DEFAULT.to_owned()
2173 } else {
2174 proto.workspace_name.into()
2175 },
2176 }
2177 })
2178 }
2179
2180 fn checkout_state_mut(&mut self) -> &mut CheckoutState {
2181 self.checkout_state(); self.checkout_state.get_mut().unwrap()
2183 }
2184
2185 #[instrument(skip_all)]
2186 fn tree_state(&self) -> Result<&TreeState, WorkingCopyStateError> {
2187 self.tree_state
2188 .get_or_try_init(|| {
2189 TreeState::load(
2190 self.store.clone(),
2191 self.working_copy_path.clone(),
2192 self.state_path.clone(),
2193 )
2194 })
2195 .map_err(|err| WorkingCopyStateError {
2196 message: "Failed to read working copy state".to_string(),
2197 err: err.into(),
2198 })
2199 }
2200
2201 fn tree_state_mut(&mut self) -> Result<&mut TreeState, WorkingCopyStateError> {
2202 self.tree_state()?; Ok(self.tree_state.get_mut().unwrap())
2204 }
2205
2206 pub fn file_states(&self) -> Result<FileStates<'_>, WorkingCopyStateError> {
2207 Ok(self.tree_state()?.file_states())
2208 }
2209
2210 #[instrument(skip_all)]
2211 fn save(&mut self) {
2212 self.write_proto(crate::protos::working_copy::Checkout {
2213 operation_id: self.operation_id().to_bytes(),
2214 workspace_name: self.workspace_name().into(),
2215 });
2216 }
2217
2218 #[cfg(feature = "watchman")]
2219 pub fn query_watchman(
2220 &self,
2221 config: &WatchmanConfig,
2222 ) -> Result<(watchman::Clock, Option<Vec<PathBuf>>), WorkingCopyStateError> {
2223 self.tree_state()?
2224 .query_watchman(config)
2225 .map_err(|err| WorkingCopyStateError {
2226 message: "Failed to query watchman".to_string(),
2227 err: err.into(),
2228 })
2229 }
2230
2231 #[cfg(feature = "watchman")]
2232 pub fn is_watchman_trigger_registered(
2233 &self,
2234 config: &WatchmanConfig,
2235 ) -> Result<bool, WorkingCopyStateError> {
2236 self.tree_state()?
2237 .is_watchman_trigger_registered(config)
2238 .map_err(|err| WorkingCopyStateError {
2239 message: "Failed to query watchman".to_string(),
2240 err: err.into(),
2241 })
2242 }
2243}
2244
2245pub struct LocalWorkingCopyFactory {}
2246
2247impl WorkingCopyFactory for LocalWorkingCopyFactory {
2248 fn init_working_copy(
2249 &self,
2250 store: Arc<Store>,
2251 working_copy_path: PathBuf,
2252 state_path: PathBuf,
2253 operation_id: OperationId,
2254 workspace_name: WorkspaceNameBuf,
2255 ) -> Result<Box<dyn WorkingCopy>, WorkingCopyStateError> {
2256 Ok(Box::new(LocalWorkingCopy::init(
2257 store,
2258 working_copy_path,
2259 state_path,
2260 operation_id,
2261 workspace_name,
2262 )?))
2263 }
2264
2265 fn load_working_copy(
2266 &self,
2267 store: Arc<Store>,
2268 working_copy_path: PathBuf,
2269 state_path: PathBuf,
2270 ) -> Result<Box<dyn WorkingCopy>, WorkingCopyStateError> {
2271 Ok(Box::new(LocalWorkingCopy::load(
2272 store,
2273 working_copy_path,
2274 state_path,
2275 )))
2276 }
2277}
2278
2279pub struct LockedLocalWorkingCopy {
2282 wc: LocalWorkingCopy,
2283 #[expect(dead_code)]
2284 lock: FileLock,
2285 old_operation_id: OperationId,
2286 old_tree_id: MergedTreeId,
2287 tree_state_dirty: bool,
2288 new_workspace_name: Option<WorkspaceNameBuf>,
2289}
2290
2291impl LockedWorkingCopy for LockedLocalWorkingCopy {
2292 fn as_any(&self) -> &dyn Any {
2293 self
2294 }
2295
2296 fn as_any_mut(&mut self) -> &mut dyn Any {
2297 self
2298 }
2299
2300 fn old_operation_id(&self) -> &OperationId {
2301 &self.old_operation_id
2302 }
2303
2304 fn old_tree_id(&self) -> &MergedTreeId {
2305 &self.old_tree_id
2306 }
2307
2308 fn snapshot(
2309 &mut self,
2310 options: &SnapshotOptions,
2311 ) -> Result<(MergedTreeId, SnapshotStats), SnapshotError> {
2312 let tree_state = self
2313 .wc
2314 .tree_state_mut()
2315 .map_err(|err| SnapshotError::Other {
2316 message: "Failed to read the working copy state".to_string(),
2317 err: err.into(),
2318 })?;
2319 let (is_dirty, stats) = tree_state.snapshot(options)?;
2320 self.tree_state_dirty |= is_dirty;
2321 Ok((tree_state.current_tree_id().clone(), stats))
2322 }
2323
2324 fn check_out(
2325 &mut self,
2326 commit: &Commit,
2327 options: &CheckoutOptions,
2328 ) -> Result<CheckoutStats, CheckoutError> {
2329 let new_tree = commit.tree()?;
2332 let tree_state = self
2333 .wc
2334 .tree_state_mut()
2335 .map_err(|err| CheckoutError::Other {
2336 message: "Failed to load the working copy state".to_string(),
2337 err: err.into(),
2338 })?;
2339 if tree_state.tree_id != *commit.tree_id() {
2340 let stats = tree_state.check_out(&new_tree, options)?;
2341 self.tree_state_dirty = true;
2342 Ok(stats)
2343 } else {
2344 Ok(CheckoutStats::default())
2345 }
2346 }
2347
2348 fn rename_workspace(&mut self, new_name: WorkspaceNameBuf) {
2349 self.new_workspace_name = Some(new_name);
2350 }
2351
2352 fn reset(&mut self, commit: &Commit) -> Result<(), ResetError> {
2353 let new_tree = commit.tree()?;
2354 self.wc
2355 .tree_state_mut()
2356 .map_err(|err| ResetError::Other {
2357 message: "Failed to read the working copy state".to_string(),
2358 err: err.into(),
2359 })?
2360 .reset(&new_tree)
2361 .block_on()?;
2362 self.tree_state_dirty = true;
2363 Ok(())
2364 }
2365
2366 fn recover(&mut self, commit: &Commit) -> Result<(), ResetError> {
2367 let new_tree = commit.tree()?;
2368 self.wc
2369 .tree_state_mut()
2370 .map_err(|err| ResetError::Other {
2371 message: "Failed to read the working copy state".to_string(),
2372 err: err.into(),
2373 })?
2374 .recover(&new_tree)
2375 .block_on()?;
2376 self.tree_state_dirty = true;
2377 Ok(())
2378 }
2379
2380 fn sparse_patterns(&self) -> Result<&[RepoPathBuf], WorkingCopyStateError> {
2381 self.wc.sparse_patterns()
2382 }
2383
2384 fn set_sparse_patterns(
2385 &mut self,
2386 new_sparse_patterns: Vec<RepoPathBuf>,
2387 options: &CheckoutOptions,
2388 ) -> Result<CheckoutStats, CheckoutError> {
2389 let stats = self
2392 .wc
2393 .tree_state_mut()
2394 .map_err(|err| CheckoutError::Other {
2395 message: "Failed to load the working copy state".to_string(),
2396 err: err.into(),
2397 })?
2398 .set_sparse_patterns(new_sparse_patterns, options)?;
2399 self.tree_state_dirty = true;
2400 Ok(stats)
2401 }
2402
2403 #[instrument(skip_all)]
2404 fn finish(
2405 mut self: Box<Self>,
2406 operation_id: OperationId,
2407 ) -> Result<Box<dyn WorkingCopy>, WorkingCopyStateError> {
2408 assert!(self.tree_state_dirty || &self.old_tree_id == self.wc.tree_id()?);
2409 if self.tree_state_dirty {
2410 self.wc
2411 .tree_state_mut()?
2412 .save()
2413 .map_err(|err| WorkingCopyStateError {
2414 message: "Failed to write working copy state".to_string(),
2415 err: Box::new(err),
2416 })?;
2417 }
2418 if self.old_operation_id != operation_id || self.new_workspace_name.is_some() {
2419 if let Some(new_name) = self.new_workspace_name {
2420 self.wc.checkout_state_mut().workspace_name = new_name;
2421 }
2422 self.wc.checkout_state_mut().operation_id = operation_id;
2423 self.wc.save();
2424 }
2425 Ok(Box::new(self.wc))
2427 }
2428}
2429
2430impl LockedLocalWorkingCopy {
2431 pub fn reset_watchman(&mut self) -> Result<(), SnapshotError> {
2432 self.wc
2433 .tree_state_mut()
2434 .map_err(|err| SnapshotError::Other {
2435 message: "Failed to read the working copy state".to_string(),
2436 err: err.into(),
2437 })?
2438 .reset_watchman();
2439 self.tree_state_dirty = true;
2440 Ok(())
2441 }
2442}
2443
2444#[cfg(test)]
2445mod tests {
2446 use maplit::hashset;
2447
2448 use super::*;
2449
2450 fn repo_path(value: &str) -> &RepoPath {
2451 RepoPath::from_internal_string(value).unwrap()
2452 }
2453
2454 fn repo_path_component(value: &str) -> &RepoPathComponent {
2455 RepoPathComponent::new(value).unwrap()
2456 }
2457
2458 #[test]
2459 fn test_file_states_merge() {
2460 let new_state = |size| FileState {
2461 file_type: FileType::Normal {
2462 executable: FileExecutableFlag::from_bool_lossy(false),
2463 },
2464 mtime: MillisSinceEpoch(0),
2465 size,
2466 materialized_conflict_data: None,
2467 };
2468 let new_static_entry = |path: &'static str, size| (repo_path(path), new_state(size));
2469 let new_owned_entry = |path: &str, size| (repo_path(path).to_owned(), new_state(size));
2470 let new_proto_entry = |path: &str, size| {
2471 file_state_entry_to_proto(repo_path(path).to_owned(), &new_state(size))
2472 };
2473 let data = vec![
2474 new_proto_entry("aa", 0),
2475 new_proto_entry("b#", 4), new_proto_entry("b/c", 1),
2477 new_proto_entry("b/d/e", 2),
2478 new_proto_entry("b/e", 3),
2479 new_proto_entry("bc", 5),
2480 ];
2481 let mut file_states = FileStatesMap::from_proto(data, false);
2482
2483 let changed_file_states = vec![
2484 new_owned_entry("aa", 10), new_owned_entry("b/d/f", 11), new_owned_entry("b/e", 12), new_owned_entry("c", 13), ];
2489 let deleted_files = hashset! {
2490 repo_path("b/c").to_owned(),
2491 repo_path("b#").to_owned(),
2492 };
2493 file_states.merge_in(changed_file_states, &deleted_files);
2494 assert_eq!(
2495 file_states.all().iter().collect_vec(),
2496 vec![
2497 new_static_entry("aa", 10),
2498 new_static_entry("b/d/e", 2),
2499 new_static_entry("b/d/f", 11),
2500 new_static_entry("b/e", 12),
2501 new_static_entry("bc", 5),
2502 new_static_entry("c", 13),
2503 ],
2504 );
2505 }
2506
2507 #[test]
2508 fn test_file_states_lookup() {
2509 let new_state = |size| FileState {
2510 file_type: FileType::Normal {
2511 executable: FileExecutableFlag::from_bool_lossy(false),
2512 },
2513 mtime: MillisSinceEpoch(0),
2514 size,
2515 materialized_conflict_data: None,
2516 };
2517 let new_proto_entry = |path: &str, size| {
2518 file_state_entry_to_proto(repo_path(path).to_owned(), &new_state(size))
2519 };
2520 let data = vec![
2521 new_proto_entry("aa", 0),
2522 new_proto_entry("b/c", 1),
2523 new_proto_entry("b/d/e", 2),
2524 new_proto_entry("b/e", 3),
2525 new_proto_entry("b#", 4), new_proto_entry("bc", 5),
2527 ];
2528 let file_states = FileStates::from_sorted(&data);
2529
2530 assert_eq!(
2531 file_states.prefixed(repo_path("")).paths().collect_vec(),
2532 ["aa", "b/c", "b/d/e", "b/e", "b#", "bc"].map(repo_path)
2533 );
2534 assert!(file_states.prefixed(repo_path("a")).is_empty());
2535 assert_eq!(
2536 file_states.prefixed(repo_path("aa")).paths().collect_vec(),
2537 ["aa"].map(repo_path)
2538 );
2539 assert_eq!(
2540 file_states.prefixed(repo_path("b")).paths().collect_vec(),
2541 ["b/c", "b/d/e", "b/e"].map(repo_path)
2542 );
2543 assert_eq!(
2544 file_states.prefixed(repo_path("b/d")).paths().collect_vec(),
2545 ["b/d/e"].map(repo_path)
2546 );
2547 assert_eq!(
2548 file_states.prefixed(repo_path("b#")).paths().collect_vec(),
2549 ["b#"].map(repo_path)
2550 );
2551 assert_eq!(
2552 file_states.prefixed(repo_path("bc")).paths().collect_vec(),
2553 ["bc"].map(repo_path)
2554 );
2555 assert!(file_states.prefixed(repo_path("z")).is_empty());
2556
2557 assert!(!file_states.contains_path(repo_path("a")));
2558 assert!(file_states.contains_path(repo_path("aa")));
2559 assert!(file_states.contains_path(repo_path("b/d/e")));
2560 assert!(!file_states.contains_path(repo_path("b/d")));
2561 assert!(file_states.contains_path(repo_path("b#")));
2562 assert!(file_states.contains_path(repo_path("bc")));
2563 assert!(!file_states.contains_path(repo_path("z")));
2564
2565 assert_eq!(file_states.get(repo_path("a")), None);
2566 assert_eq!(file_states.get(repo_path("aa")), Some(new_state(0)));
2567 assert_eq!(file_states.get(repo_path("b/d/e")), Some(new_state(2)));
2568 assert_eq!(file_states.get(repo_path("bc")), Some(new_state(5)));
2569 assert_eq!(file_states.get(repo_path("z")), None);
2570 }
2571
2572 #[test]
2573 fn test_file_states_lookup_at() {
2574 let new_state = |size| FileState {
2575 file_type: FileType::Normal {
2576 executable: FileExecutableFlag::from_bool_lossy(false),
2577 },
2578 mtime: MillisSinceEpoch(0),
2579 size,
2580 materialized_conflict_data: None,
2581 };
2582 let new_proto_entry = |path: &str, size| {
2583 file_state_entry_to_proto(repo_path(path).to_owned(), &new_state(size))
2584 };
2585 let data = vec![
2586 new_proto_entry("b/c", 0),
2587 new_proto_entry("b/d/e", 1),
2588 new_proto_entry("b/d#", 2), new_proto_entry("b/e", 3),
2590 new_proto_entry("b#", 4), ];
2592 let file_states = FileStates::from_sorted(&data);
2593
2594 assert_eq!(
2596 file_states.get_at(RepoPath::root(), repo_path_component("b")),
2597 None
2598 );
2599 assert_eq!(
2600 file_states.get_at(RepoPath::root(), repo_path_component("b#")),
2601 Some(new_state(4))
2602 );
2603
2604 let prefixed_states = file_states.prefixed_at(RepoPath::root(), repo_path_component("b"));
2606 assert_eq!(
2607 prefixed_states.paths().collect_vec(),
2608 ["b/c", "b/d/e", "b/d#", "b/e"].map(repo_path)
2609 );
2610 assert_eq!(
2611 prefixed_states.get_at(repo_path("b"), repo_path_component("c")),
2612 Some(new_state(0))
2613 );
2614 assert_eq!(
2615 prefixed_states.get_at(repo_path("b"), repo_path_component("d")),
2616 None
2617 );
2618 assert_eq!(
2619 prefixed_states.get_at(repo_path("b"), repo_path_component("d#")),
2620 Some(new_state(2))
2621 );
2622
2623 let prefixed_states = prefixed_states.prefixed_at(repo_path("b"), repo_path_component("d"));
2625 assert_eq!(
2626 prefixed_states.paths().collect_vec(),
2627 ["b/d/e"].map(repo_path)
2628 );
2629 assert_eq!(
2630 prefixed_states.get_at(repo_path("b/d"), repo_path_component("e")),
2631 Some(new_state(1))
2632 );
2633 assert_eq!(
2634 prefixed_states.get_at(repo_path("b/d"), repo_path_component("#")),
2635 None
2636 );
2637
2638 let prefixed_states = file_states.prefixed_at(RepoPath::root(), repo_path_component("b#"));
2640 assert_eq!(prefixed_states.paths().collect_vec(), ["b#"].map(repo_path));
2641 assert_eq!(
2642 prefixed_states.get_at(repo_path("b#"), repo_path_component("#")),
2643 None
2644 );
2645 }
2646}