Skip to main content

jj_lib/
local_working_copy.rs

1// Copyright 2020 The Jujutsu Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15#![expect(missing_docs)]
16
17use std::borrow::Cow;
18use std::cmp::Ordering;
19use std::collections::HashMap;
20use std::collections::HashSet;
21use std::error::Error;
22use std::ffi::OsString;
23use std::fs;
24use std::fs::DirEntry;
25use std::fs::File;
26use std::fs::Metadata;
27use std::fs::OpenOptions;
28use std::io;
29use std::io::Read as _;
30use std::io::Write as _;
31use std::iter;
32use std::mem;
33use std::ops::Range;
34#[cfg(unix)]
35use std::os::unix::fs::PermissionsExt as _;
36use std::path::Path;
37use std::path::PathBuf;
38use std::slice;
39use std::sync::Arc;
40use std::sync::OnceLock;
41use std::sync::mpsc::Sender;
42use std::sync::mpsc::channel;
43use std::time::SystemTime;
44
45use async_trait::async_trait;
46use either::Either;
47use futures::AsyncRead;
48use futures::AsyncReadExt as _;
49use futures::StreamExt as _;
50use futures::io::AllowStdIo;
51use itertools::EitherOrBoth;
52use itertools::Itertools as _;
53use once_cell::unsync::OnceCell;
54use pollster::FutureExt as _;
55use prost::Message as _;
56use rayon::iter::IntoParallelIterator as _;
57use rayon::prelude::IndexedParallelIterator as _;
58use rayon::prelude::ParallelIterator as _;
59use tempfile::NamedTempFile;
60use thiserror::Error;
61use tracing::instrument;
62use tracing::trace_span;
63
64use crate::backend::BackendError;
65use crate::backend::CopyId;
66use crate::backend::FileId;
67use crate::backend::MillisSinceEpoch;
68use crate::backend::SymlinkId;
69use crate::backend::TreeId;
70use crate::backend::TreeValue;
71use crate::commit::Commit;
72use crate::config::ConfigGetError;
73use crate::conflict_labels::ConflictLabels;
74use crate::conflicts;
75use crate::conflicts::ConflictMarkerStyle;
76use crate::conflicts::ConflictMaterializeOptions;
77use crate::conflicts::MIN_CONFLICT_MARKER_LEN;
78use crate::conflicts::MaterializedTreeValue;
79use crate::conflicts::choose_materialized_conflict_marker_len;
80use crate::conflicts::materialize_merge_result_to_bytes;
81use crate::conflicts::materialize_tree_value;
82pub use crate::eol::EolConversionMode;
83use crate::eol::TargetEolStrategy;
84use crate::file_util::FileIdentity;
85use crate::file_util::check_symlink_support;
86use crate::file_util::copy_async_to_sync;
87use crate::file_util::persist_temp_file;
88use crate::file_util::symlink_file;
89use crate::fsmonitor::FsmonitorSettings;
90#[cfg(feature = "watchman")]
91use crate::fsmonitor::WatchmanConfig;
92#[cfg(feature = "watchman")]
93use crate::fsmonitor::watchman;
94use crate::gitignore::GitIgnoreFile;
95use crate::lock::FileLock;
96use crate::matchers::DifferenceMatcher;
97use crate::matchers::EverythingMatcher;
98use crate::matchers::FilesMatcher;
99use crate::matchers::IntersectionMatcher;
100use crate::matchers::Matcher;
101use crate::matchers::PrefixMatcher;
102use crate::matchers::UnionMatcher;
103use crate::merge::Merge;
104use crate::merge::MergeBuilder;
105use crate::merge::MergedTreeValue;
106use crate::merge::SameChange;
107use crate::merged_tree::MergedTree;
108use crate::merged_tree::TreeDiffEntry;
109use crate::merged_tree_builder::MergedTreeBuilder;
110use crate::object_id::ObjectId as _;
111use crate::op_store::OperationId;
112use crate::ref_name::WorkspaceName;
113use crate::ref_name::WorkspaceNameBuf;
114use crate::repo_path::RepoPath;
115use crate::repo_path::RepoPathBuf;
116use crate::repo_path::RepoPathComponent;
117use crate::settings::UserSettings;
118use crate::store::Store;
119use crate::working_copy::CheckoutError;
120use crate::working_copy::CheckoutStats;
121use crate::working_copy::LockedWorkingCopy;
122use crate::working_copy::ResetError;
123use crate::working_copy::SnapshotError;
124use crate::working_copy::SnapshotOptions;
125use crate::working_copy::SnapshotProgress;
126use crate::working_copy::SnapshotStats;
127use crate::working_copy::UntrackedReason;
128use crate::working_copy::WorkingCopy;
129use crate::working_copy::WorkingCopyFactory;
130use crate::working_copy::WorkingCopyStateError;
131
132fn symlink_target_convert_to_store(path: &Path) -> Option<Cow<'_, str>> {
133    let path = path.to_str()?;
134    if std::path::MAIN_SEPARATOR == '/' {
135        Some(Cow::Borrowed(path))
136    } else {
137        // When storing the symlink target on Windows, convert "\" to "/", so that the
138        // symlink remains valid on Unix.
139        //
140        // Note that we don't use std::path to handle the conversion, because it
141        // performs poorly with Windows verbatim paths like \\?\Global\C:\file.txt.
142        Some(Cow::Owned(path.replace(std::path::MAIN_SEPARATOR_STR, "/")))
143    }
144}
145
146fn symlink_target_convert_to_disk(path: &str) -> PathBuf {
147    let path = if std::path::MAIN_SEPARATOR == '/' {
148        Cow::Borrowed(path)
149    } else {
150        // Use the main separator to reformat the input path to avoid creating a broken
151        // symlink with the incorrect separator "/".
152        //
153        // See https://github.com/jj-vcs/jj/issues/6934 for the relevant bug.
154        Cow::Owned(path.replace('/', std::path::MAIN_SEPARATOR_STR))
155    };
156    PathBuf::from(path.as_ref())
157}
158
159/// How to propagate executable bit changes in file metadata to/from the repo.
160///
161/// On Windows, executable bits are always ignored, but on Unix they are
162/// respected by default, but may be ignored by user settings or if we find
163/// that the filesystem of the working copy doesn't support executable bits.
164#[derive(Clone, Copy, Debug)]
165enum ExecChangePolicy {
166    Ignore,
167    #[cfg_attr(windows, expect(dead_code))]
168    Respect,
169}
170
171/// The executable bit change setting as exposed to the user.
172#[derive(Clone, Copy, Debug, Default, serde::Deserialize)]
173#[serde(rename_all = "kebab-case")]
174pub enum ExecChangeSetting {
175    Ignore,
176    Respect,
177    #[default]
178    Auto,
179}
180
181impl ExecChangePolicy {
182    /// Get the executable bit policy based on user settings and executable bit
183    /// support in the working copy's state path.
184    ///
185    /// On Unix we check whether executable bits are supported in the working
186    /// copy to determine respect/ignorance, but we default to respect.
187    #[cfg_attr(windows, expect(unused_variables))]
188    fn new(exec_change_setting: ExecChangeSetting, state_path: &Path) -> Self {
189        #[cfg(windows)]
190        return Self::Ignore;
191        #[cfg(unix)]
192        return match exec_change_setting {
193            ExecChangeSetting::Ignore => Self::Ignore,
194            ExecChangeSetting::Respect => Self::Respect,
195            ExecChangeSetting::Auto => {
196                match crate::file_util::check_executable_bit_support(state_path) {
197                    Ok(false) => Self::Ignore,
198                    Ok(true) => Self::Respect,
199                    Err(err) => {
200                        tracing::warn!(?err, "Error when checking for executable bit support");
201                        Self::Respect
202                    }
203                }
204            }
205        };
206    }
207}
208
209/// On-disk state of file executable as cached in the file states. This does
210/// *not* necessarily equal the `executable` field of [`TreeValue::File`]: the
211/// two are allowed to diverge if and only if we're ignoring executable bit
212/// changes.
213///
214/// This will only ever be true on Windows if the repo is also being accessed
215/// from a Unix version of jj, such as when accessed from WSL.
216#[derive(Clone, Copy, Debug, Eq, PartialEq)]
217pub struct ExecBit(bool);
218
219impl ExecBit {
220    /// Get the executable bit for a tree value to write to the repo store.
221    ///
222    /// If we're ignoring the executable bit, then we fallback to the previous
223    /// in-repo executable bit if present.
224    fn for_tree_value(
225        self,
226        exec_policy: ExecChangePolicy,
227        prev_in_repo: impl FnOnce() -> Option<bool>,
228    ) -> bool {
229        match exec_policy {
230            ExecChangePolicy::Ignore => prev_in_repo().unwrap_or(false),
231            ExecChangePolicy::Respect => self.0,
232        }
233    }
234
235    /// Set the on-disk executable bit to be written based on the in-repo bit or
236    /// the previous on-disk executable bit.
237    ///
238    /// On Windows, we return `false` because when we later write files, we
239    /// always create them anew, and the executable bit will be `false` even if
240    /// shared with a Unix machine.
241    ///
242    /// `prev_on_disk` is a closure because it is somewhat expensive and is only
243    /// used if ignoring the executable bit on Unix.
244    fn new_from_repo(
245        in_repo: bool,
246        exec_policy: ExecChangePolicy,
247        prev_on_disk: impl FnOnce() -> Option<Self>,
248    ) -> Self {
249        match exec_policy {
250            _ if cfg!(windows) => Self(false),
251            ExecChangePolicy::Ignore => prev_on_disk().unwrap_or(Self(false)),
252            ExecChangePolicy::Respect => Self(in_repo),
253        }
254    }
255
256    /// Load the on-disk executable bit from file metadata.
257    #[cfg_attr(windows, expect(unused_variables))]
258    fn new_from_disk(metadata: &Metadata) -> Self {
259        #[cfg(unix)]
260        return Self(metadata.permissions().mode() & 0o111 != 0);
261        #[cfg(windows)]
262        return Self(false);
263    }
264}
265
266/// Set the executable bit of a file on-disk. This is a no-op on Windows.
267///
268/// On Unix, we manually set the executable bit to the previous value on-disk.
269/// This is necessary because we write all files by creating them new, so files
270/// won't preserve their permissions naturally.
271#[cfg_attr(windows, expect(unused_variables))]
272fn set_executable(exec_bit: ExecBit, disk_path: &Path) -> Result<(), io::Error> {
273    #[cfg(unix)]
274    {
275        let mode = if exec_bit.0 { 0o755 } else { 0o644 };
276        fs::set_permissions(disk_path, fs::Permissions::from_mode(mode))?;
277    }
278    Ok(())
279}
280
281#[derive(Debug, PartialEq, Eq, Clone)]
282pub enum FileType {
283    Normal { exec_bit: ExecBit },
284    Symlink,
285    GitSubmodule,
286}
287
288#[derive(Debug, PartialEq, Eq, Clone, Copy)]
289pub struct MaterializedConflictData {
290    pub conflict_marker_len: u32,
291}
292
293#[derive(Debug, PartialEq, Eq, Clone)]
294pub struct FileState {
295    pub file_type: FileType,
296    pub mtime: MillisSinceEpoch,
297    pub size: u64,
298    pub materialized_conflict_data: Option<MaterializedConflictData>,
299    /* TODO: What else do we need here? Git stores a lot of fields.
300     * TODO: Could possibly handle case-insensitive file systems keeping an
301     *       Option<PathBuf> with the actual path here. */
302}
303
304impl FileState {
305    /// Check whether a file state appears clean compared to a previous file
306    /// state, ignoring materialized conflict data.
307    pub fn is_clean(&self, old_file_state: &Self) -> bool {
308        self.file_type == old_file_state.file_type
309            && self.mtime == old_file_state.mtime
310            && self.size == old_file_state.size
311    }
312
313    /// Indicates that a file exists in the tree but that it needs to be
314    /// re-stat'ed on the next snapshot.
315    fn placeholder() -> Self {
316        Self {
317            file_type: FileType::Normal {
318                exec_bit: ExecBit(false),
319            },
320            mtime: MillisSinceEpoch(0),
321            size: 0,
322            materialized_conflict_data: None,
323        }
324    }
325
326    fn for_file(
327        exec_bit: ExecBit,
328        size: u64,
329        metadata: &Metadata,
330    ) -> Result<Self, MtimeOutOfRange> {
331        Ok(Self {
332            file_type: FileType::Normal { exec_bit },
333            mtime: mtime_from_metadata(metadata)?,
334            size,
335            materialized_conflict_data: None,
336        })
337    }
338
339    fn for_symlink(metadata: &Metadata) -> Result<Self, MtimeOutOfRange> {
340        // When using fscrypt, the reported size is not the content size. So if
341        // we were to record the content size here (like we do for regular files), we
342        // would end up thinking the file has changed every time we snapshot.
343        Ok(Self {
344            file_type: FileType::Symlink,
345            mtime: mtime_from_metadata(metadata)?,
346            size: metadata.len(),
347            materialized_conflict_data: None,
348        })
349    }
350
351    fn for_gitsubmodule() -> Self {
352        Self {
353            file_type: FileType::GitSubmodule,
354            mtime: MillisSinceEpoch(0),
355            size: 0,
356            materialized_conflict_data: None,
357        }
358    }
359}
360
361/// Owned map of path to file states, backed by proto data.
362#[derive(Clone, Debug)]
363struct FileStatesMap {
364    data: Vec<crate::protos::local_working_copy::FileStateEntry>,
365}
366
367impl FileStatesMap {
368    fn new() -> Self {
369        Self { data: Vec::new() }
370    }
371
372    fn from_proto(
373        mut data: Vec<crate::protos::local_working_copy::FileStateEntry>,
374        is_sorted: bool,
375    ) -> Self {
376        if !is_sorted {
377            data.sort_unstable_by(|entry1, entry2| {
378                let path1 = RepoPath::from_internal_string(&entry1.path).unwrap();
379                let path2 = RepoPath::from_internal_string(&entry2.path).unwrap();
380                path1.cmp(path2)
381            });
382        }
383        debug_assert!(is_file_state_entries_proto_unique_and_sorted(&data));
384        Self { data }
385    }
386
387    /// Merges changed and deleted entries into this map. The changed entries
388    /// must be sorted by path.
389    fn merge_in(
390        &mut self,
391        changed_file_states: Vec<(RepoPathBuf, FileState)>,
392        deleted_files: &HashSet<RepoPathBuf>,
393    ) {
394        if changed_file_states.is_empty() && deleted_files.is_empty() {
395            return;
396        }
397        debug_assert!(
398            changed_file_states.is_sorted_by(|(path1, _), (path2, _)| path1 < path2),
399            "changed_file_states must be sorted and have no duplicates"
400        );
401        self.data = itertools::merge_join_by(
402            mem::take(&mut self.data),
403            changed_file_states,
404            |old_entry, (changed_path, _)| {
405                RepoPath::from_internal_string(&old_entry.path)
406                    .unwrap()
407                    .cmp(changed_path)
408            },
409        )
410        .filter_map(|diff| match diff {
411            EitherOrBoth::Both(_, (path, state)) | EitherOrBoth::Right((path, state)) => {
412                debug_assert!(!deleted_files.contains(&path));
413                Some(file_state_entry_to_proto(path, &state))
414            }
415            EitherOrBoth::Left(entry) => {
416                let present =
417                    !deleted_files.contains(RepoPath::from_internal_string(&entry.path).unwrap());
418                present.then_some(entry)
419            }
420        })
421        .collect();
422    }
423
424    fn clear(&mut self) {
425        self.data.clear();
426    }
427
428    /// Returns read-only map containing all file states.
429    fn all(&self) -> FileStates<'_> {
430        FileStates::from_sorted(&self.data)
431    }
432}
433
434/// Read-only map of path to file states, possibly filtered by path prefix.
435#[derive(Clone, Copy, Debug)]
436pub struct FileStates<'a> {
437    data: &'a [crate::protos::local_working_copy::FileStateEntry],
438}
439
440impl<'a> FileStates<'a> {
441    fn from_sorted(data: &'a [crate::protos::local_working_copy::FileStateEntry]) -> Self {
442        debug_assert!(is_file_state_entries_proto_unique_and_sorted(data));
443        Self { data }
444    }
445
446    /// Returns file states under the given directory path.
447    pub fn prefixed(&self, base: &RepoPath) -> Self {
448        let range = self.prefixed_range(base);
449        Self::from_sorted(&self.data[range])
450    }
451
452    /// Faster version of `prefixed("<dir>/<base>")`. Requires that all entries
453    /// share the same prefix `dir`.
454    fn prefixed_at(&self, dir: &RepoPath, base: &RepoPathComponent) -> Self {
455        let range = self.prefixed_range_at(dir, base);
456        Self::from_sorted(&self.data[range])
457    }
458
459    /// Returns true if this contains no entries.
460    pub fn is_empty(&self) -> bool {
461        self.data.is_empty()
462    }
463
464    /// Returns true if the given `path` exists.
465    pub fn contains_path(&self, path: &RepoPath) -> bool {
466        self.exact_position(path).is_some()
467    }
468
469    /// Returns file state for the given `path`.
470    pub fn get(&self, path: &RepoPath) -> Option<FileState> {
471        let pos = self.exact_position(path)?;
472        let (_, state) = file_state_entry_from_proto(&self.data[pos]);
473        Some(state)
474    }
475
476    /// Returns the executable bit state if `path` is a normal file.
477    pub fn get_exec_bit(&self, path: &RepoPath) -> Option<ExecBit> {
478        match self.get(path)?.file_type {
479            FileType::Normal { exec_bit } => Some(exec_bit),
480            FileType::Symlink | FileType::GitSubmodule => None,
481        }
482    }
483
484    /// Faster version of `get("<dir>/<name>")`. Requires that all entries share
485    /// the same prefix `dir`.
486    fn get_at(&self, dir: &RepoPath, name: &RepoPathComponent) -> Option<FileState> {
487        let pos = self.exact_position_at(dir, name)?;
488        let (_, state) = file_state_entry_from_proto(&self.data[pos]);
489        Some(state)
490    }
491
492    fn exact_position(&self, path: &RepoPath) -> Option<usize> {
493        self.data
494            .binary_search_by(|entry| {
495                RepoPath::from_internal_string(&entry.path)
496                    .unwrap()
497                    .cmp(path)
498            })
499            .ok()
500    }
501
502    fn exact_position_at(&self, dir: &RepoPath, name: &RepoPathComponent) -> Option<usize> {
503        debug_assert!(self.paths().all(|path| path.starts_with(dir)));
504        let slash_len = usize::from(!dir.is_root());
505        let prefix_len = dir.as_internal_file_string().len() + slash_len;
506        self.data
507            .binary_search_by(|entry| {
508                let tail = entry.path.get(prefix_len..).unwrap_or("");
509                match tail.split_once('/') {
510                    // "<name>/*" > "<name>"
511                    Some((pre, _)) => pre.cmp(name.as_internal_str()).then(Ordering::Greater),
512                    None => tail.cmp(name.as_internal_str()),
513                }
514            })
515            .ok()
516    }
517
518    fn prefixed_range(&self, base: &RepoPath) -> Range<usize> {
519        let start = self
520            .data
521            .partition_point(|entry| RepoPath::from_internal_string(&entry.path).unwrap() < base);
522        let len = self.data[start..].partition_point(|entry| {
523            RepoPath::from_internal_string(&entry.path)
524                .unwrap()
525                .starts_with(base)
526        });
527        start..(start + len)
528    }
529
530    fn prefixed_range_at(&self, dir: &RepoPath, base: &RepoPathComponent) -> Range<usize> {
531        debug_assert!(self.paths().all(|path| path.starts_with(dir)));
532        let slash_len = usize::from(!dir.is_root());
533        let prefix_len = dir.as_internal_file_string().len() + slash_len;
534        let start = self.data.partition_point(|entry| {
535            let tail = entry.path.get(prefix_len..).unwrap_or("");
536            let entry_name = tail.split_once('/').map_or(tail, |(name, _)| name);
537            entry_name < base.as_internal_str()
538        });
539        let len = self.data[start..].partition_point(|entry| {
540            let tail = entry.path.get(prefix_len..).unwrap_or("");
541            let entry_name = tail.split_once('/').map_or(tail, |(name, _)| name);
542            entry_name == base.as_internal_str()
543        });
544        start..(start + len)
545    }
546
547    /// Iterates file state entries sorted by path.
548    pub fn iter(&self) -> FileStatesIter<'a> {
549        self.data.iter().map(file_state_entry_from_proto)
550    }
551
552    /// Iterates sorted file paths.
553    pub fn paths(&self) -> impl ExactSizeIterator<Item = &'a RepoPath> + use<'a> {
554        self.data
555            .iter()
556            .map(|entry| RepoPath::from_internal_string(&entry.path).unwrap())
557    }
558}
559
560type FileStatesIter<'a> = iter::Map<
561    slice::Iter<'a, crate::protos::local_working_copy::FileStateEntry>,
562    fn(&crate::protos::local_working_copy::FileStateEntry) -> (&RepoPath, FileState),
563>;
564
565impl<'a> IntoIterator for FileStates<'a> {
566    type Item = (&'a RepoPath, FileState);
567    type IntoIter = FileStatesIter<'a>;
568
569    fn into_iter(self) -> Self::IntoIter {
570        self.iter()
571    }
572}
573
574fn file_state_from_proto(proto: &crate::protos::local_working_copy::FileState) -> FileState {
575    let file_type = match proto.file_type() {
576        crate::protos::local_working_copy::FileType::Normal => FileType::Normal {
577            exec_bit: ExecBit(false),
578        },
579        // On Windows, `FileType::Executable` can exist if the repo is being
580        // shared with a Unix version of jj, such as when accessed from WSL.
581        crate::protos::local_working_copy::FileType::Executable => FileType::Normal {
582            exec_bit: ExecBit(true),
583        },
584        crate::protos::local_working_copy::FileType::Symlink => FileType::Symlink,
585        #[expect(deprecated)]
586        crate::protos::local_working_copy::FileType::Conflict => FileType::Normal {
587            exec_bit: ExecBit(false),
588        },
589        crate::protos::local_working_copy::FileType::GitSubmodule => FileType::GitSubmodule,
590    };
591    FileState {
592        file_type,
593        mtime: MillisSinceEpoch(proto.mtime_millis_since_epoch),
594        size: proto.size,
595        materialized_conflict_data: proto.materialized_conflict_data.as_ref().map(|data| {
596            MaterializedConflictData {
597                conflict_marker_len: data.conflict_marker_len,
598            }
599        }),
600    }
601}
602
603fn file_state_to_proto(file_state: &FileState) -> crate::protos::local_working_copy::FileState {
604    let mut proto = crate::protos::local_working_copy::FileState::default();
605    let file_type = match &file_state.file_type {
606        FileType::Normal { exec_bit } => {
607            if exec_bit.0 {
608                crate::protos::local_working_copy::FileType::Executable
609            } else {
610                crate::protos::local_working_copy::FileType::Normal
611            }
612        }
613        FileType::Symlink => crate::protos::local_working_copy::FileType::Symlink,
614        FileType::GitSubmodule => crate::protos::local_working_copy::FileType::GitSubmodule,
615    };
616    proto.file_type = file_type as i32;
617    proto.mtime_millis_since_epoch = file_state.mtime.0;
618    proto.size = file_state.size;
619    proto.materialized_conflict_data = file_state.materialized_conflict_data.map(|data| {
620        crate::protos::local_working_copy::MaterializedConflictData {
621            conflict_marker_len: data.conflict_marker_len,
622        }
623    });
624    proto
625}
626
627fn file_state_entry_from_proto(
628    proto: &crate::protos::local_working_copy::FileStateEntry,
629) -> (&RepoPath, FileState) {
630    let path = RepoPath::from_internal_string(&proto.path).unwrap();
631    (path, file_state_from_proto(proto.state.as_ref().unwrap()))
632}
633
634fn file_state_entry_to_proto(
635    path: RepoPathBuf,
636    state: &FileState,
637) -> crate::protos::local_working_copy::FileStateEntry {
638    crate::protos::local_working_copy::FileStateEntry {
639        path: path.into_internal_string(),
640        state: Some(file_state_to_proto(state)),
641    }
642}
643
644fn is_file_state_entries_proto_unique_and_sorted(
645    data: &[crate::protos::local_working_copy::FileStateEntry],
646) -> bool {
647    data.iter()
648        .map(|entry| RepoPath::from_internal_string(&entry.path).unwrap())
649        .is_sorted_by(|path1, path2| path1 < path2)
650}
651
652fn sparse_patterns_from_proto(
653    proto: Option<&crate::protos::local_working_copy::SparsePatterns>,
654) -> Vec<RepoPathBuf> {
655    let mut sparse_patterns = vec![];
656    if let Some(proto_sparse_patterns) = proto {
657        for prefix in &proto_sparse_patterns.prefixes {
658            sparse_patterns.push(RepoPathBuf::from_internal_string(prefix).unwrap());
659        }
660    } else {
661        // For compatibility with old working copies.
662        // TODO: Delete this is late 2022 or so.
663        sparse_patterns.push(RepoPathBuf::root());
664    }
665    sparse_patterns
666}
667
668/// Creates intermediate directories from the `working_copy_path` to the
669/// `repo_path` parent. Returns disk path for the `repo_path` file.
670///
671/// If an intermediate directory exists and if it is a file or symlink, this
672/// function returns `Ok(None)` to signal that the path should be skipped.
673/// The `working_copy_path` directory may be a symlink.
674///
675/// If an existing or newly-created sub directory points to ".git" or ".jj",
676/// this function returns an error.
677///
678/// Note that this does not prevent TOCTOU bugs caused by concurrent checkouts.
679/// Another process may remove the directory created by this function and put a
680/// symlink there.
681fn create_parent_dirs(
682    working_copy_path: &Path,
683    repo_path: &RepoPath,
684) -> Result<Option<PathBuf>, CheckoutError> {
685    let (parent_path, basename) = repo_path.split().expect("repo path shouldn't be root");
686    let mut dir_path = working_copy_path.to_owned();
687    for c in parent_path.components() {
688        // Ensure that the name is a normal entry of the current dir_path.
689        dir_path.push(c.to_fs_name().map_err(|err| err.with_path(repo_path))?);
690        // A directory named ".git" or ".jj" can be temporarily created. It
691        // might trick workspace path discovery, but is harmless so long as the
692        // directory is empty.
693        let (new_dir_created, is_dir) = match fs::create_dir(&dir_path) {
694            Ok(()) => (true, true), // New directory
695            Err(err) => match dir_path.symlink_metadata() {
696                Ok(m) => (false, m.is_dir()), // Existing file or directory
697                Err(_) => {
698                    return Err(CheckoutError::Other {
699                        message: format!(
700                            "Failed to create parent directories for {}",
701                            repo_path.to_fs_path_unchecked(working_copy_path).display(),
702                        ),
703                        err: err.into(),
704                    });
705                }
706            },
707        };
708        // Invalid component (e.g. "..") should have been rejected.
709        // The current dir_path should be an entry of dir_path.parent().
710        reject_reserved_existing_path(&dir_path).inspect_err(|_| {
711            if new_dir_created {
712                fs::remove_dir(&dir_path).ok();
713            }
714        })?;
715        if !is_dir {
716            return Ok(None); // Skip existing file or symlink
717        }
718    }
719
720    let mut file_path = dir_path;
721    file_path.push(
722        basename
723            .to_fs_name()
724            .map_err(|err| err.with_path(repo_path))?,
725    );
726    Ok(Some(file_path))
727}
728
729/// Removes existing file named `disk_path` if any. Returns `Ok(true)` if the
730/// file was there and got removed, meaning that new file can be safely created.
731///
732/// If the existing file points to ".git" or ".jj", this function returns an
733/// error.
734fn remove_old_file(disk_path: &Path) -> Result<bool, CheckoutError> {
735    reject_reserved_existing_path(disk_path)?;
736    match fs::remove_file(disk_path) {
737        Ok(()) => Ok(true),
738        Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(false),
739        // TODO: Use io::ErrorKind::IsADirectory if it gets stabilized
740        Err(_) if disk_path.symlink_metadata().is_ok_and(|m| m.is_dir()) => Ok(false),
741        Err(err) => Err(CheckoutError::Other {
742            message: format!("Failed to remove file {}", disk_path.display()),
743            err: err.into(),
744        }),
745    }
746}
747
748/// Removes existing submodule directory named `disk_path` if any. Returns
749/// `Ok(true)` if the directory was there and got removed, meaning that new file
750/// can be safely created.
751///
752/// The directory will not be removed if it is not empty, as it could contain
753/// untracked or modified files. This is in line with Git's behavior.
754fn remove_old_submodule_dir(disk_path: &Path) -> Result<bool, CheckoutError> {
755    match fs::remove_dir(disk_path) {
756        Ok(()) => Ok(true),
757        Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(false),
758        Err(err) if err.kind() == io::ErrorKind::DirectoryNotEmpty => Ok(false),
759        Err(err) => Err(CheckoutError::Other {
760            message: format!(
761                "Failed to remove submodule directory {}",
762                disk_path.display()
763            ),
764            err: err.into(),
765        }),
766    }
767}
768
769/// Checks if new file or symlink named `disk_path` can be created.
770///
771/// If the file already exists, this function return `Ok(false)` to signal
772/// that the path should be skipped.
773///
774/// If the path may point to ".git" or ".jj" entry, this function returns an
775/// error.
776///
777/// This function can fail if `disk_path.parent()` isn't a directory.
778fn can_create_new_file(disk_path: &Path) -> Result<bool, CheckoutError> {
779    // New file or symlink will be created by caller. If it were pointed to by
780    // name ".git" or ".jj", git/jj CLI could be tricked to load configuration
781    // from an attacker-controlled location. So we first test the path by
782    // creating an empty file.
783    let new_file = match OpenOptions::new()
784        .write(true)
785        .create_new(true) // Don't overwrite, don't follow symlink
786        .open(disk_path)
787    {
788        Ok(file) => Some(file),
789        Err(err) if err.kind() == io::ErrorKind::AlreadyExists => None,
790        // Workaround for "Access is denied. (os error 5)" error on Windows.
791        Err(_) => match disk_path.symlink_metadata() {
792            Ok(_) => None,
793            Err(err) => {
794                return Err(CheckoutError::Other {
795                    message: format!("Failed to stat {}", disk_path.display()),
796                    err: err.into(),
797                });
798            }
799        },
800    };
801
802    let new_file_created = new_file.is_some();
803
804    if let Some(new_file) = new_file {
805        reject_reserved_existing_file(new_file, disk_path).inspect_err(|_| {
806            // We keep the error from `reject_reserved_existing_file`
807            fs::remove_file(disk_path).ok();
808        })?;
809
810        fs::remove_file(disk_path).map_err(|err| CheckoutError::Other {
811            message: format!("Failed to remove temporary file {}", disk_path.display()),
812            err: err.into(),
813        })?;
814    } else {
815        reject_reserved_existing_path(disk_path)?;
816    }
817    Ok(new_file_created)
818}
819
820const RESERVED_DIR_NAMES: &[&str] = &[".git", ".jj"];
821
822fn file_identity_from_symlink_path(disk_path: &Path) -> io::Result<Option<FileIdentity>> {
823    match FileIdentity::from_symlink_path(disk_path) {
824        Ok(identity) => Ok(Some(identity)),
825        Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(None),
826        Err(err) => Err(err),
827    }
828}
829
830/// Wrapper for [`reject_reserved_existing_file_identity`] which avoids a
831/// syscall by converting the provided `file` to a `FileIdentity` via its
832/// file descriptor.
833///
834/// See [`reject_reserved_existing_file_identity`] for more info.
835fn reject_reserved_existing_file(file: File, disk_path: &Path) -> Result<(), CheckoutError> {
836    // Note: since the file is open, we don't expect that it's possible for
837    // `io::ErrorKind::NotFound` to be a possible error returned here.
838    let file_identity = FileIdentity::from_file(file).map_err(|err| CheckoutError::Other {
839        message: format!("Failed to validate path {}", disk_path.display()),
840        err: err.into(),
841    })?;
842
843    reject_reserved_existing_file_identity(file_identity, disk_path)
844}
845
846/// Wrapper for [`reject_reserved_existing_file_identity`] which converts
847/// the provided `disk_path` to a `FileIdentity`.
848///
849/// See [`reject_reserved_existing_file_identity`] for more info.
850///
851/// # Remarks
852///
853/// On Windows, this incurs an additional syscall cost to open and close the
854/// file `HANDLE` for `disk_path`. On Unix, `lstat()` is used.
855fn reject_reserved_existing_path(disk_path: &Path) -> Result<(), CheckoutError> {
856    let Some(disk_identity) =
857        file_identity_from_symlink_path(disk_path).map_err(|err| CheckoutError::Other {
858            message: format!("Failed to validate path {}", disk_path.display()),
859            err: err.into(),
860        })?
861    else {
862        // If the existing disk_path pointed to the reserved path, we would have
863        // gotten an identity back. Since we got nothing, the file does not exist
864        // and cannot be a reserved path name.
865        return Ok(());
866    };
867
868    reject_reserved_existing_file_identity(disk_identity, disk_path)
869}
870
871/// Suppose the `disk_path` exists, checks if the last component points to
872/// ".git" or ".jj" in the same parent directory.
873///
874/// `disk_identity` is expected to be an identity of the file described by
875/// `disk_path`.
876///
877/// # Remarks
878///
879/// On Windows, this incurs a syscall cost to open and close a file `HANDLE` for
880/// each filename in `RESERVED_DIR_NAMES`. On Unix, `lstat()` is used.
881fn reject_reserved_existing_file_identity(
882    disk_identity: FileIdentity,
883    disk_path: &Path,
884) -> Result<(), CheckoutError> {
885    let parent_dir_path = disk_path.parent().expect("content path shouldn't be root");
886    for name in RESERVED_DIR_NAMES {
887        let reserved_path = parent_dir_path.join(name);
888
889        let Some(reserved_identity) =
890            file_identity_from_symlink_path(&reserved_path).map_err(|err| {
891                CheckoutError::Other {
892                    message: format!("Failed to validate path {}", disk_path.display()),
893                    err: err.into(),
894                }
895            })?
896        else {
897            // If the existing disk_path pointed to the reserved path, we would have
898            // gotten an identity back. Since we got nothing, the file does not exist
899            // and cannot be a reserved path name.
900            continue;
901        };
902
903        if disk_identity == reserved_identity {
904            return Err(CheckoutError::ReservedPathComponent {
905                path: disk_path.to_owned(),
906                name,
907            });
908        }
909    }
910
911    Ok(())
912}
913
914#[derive(Debug, Error)]
915#[error("Out-of-range file modification time")]
916struct MtimeOutOfRange;
917
918fn mtime_from_metadata(metadata: &Metadata) -> Result<MillisSinceEpoch, MtimeOutOfRange> {
919    let time = metadata
920        .modified()
921        .expect("File mtime not supported on this platform?");
922    system_time_to_millis(time).ok_or(MtimeOutOfRange)
923}
924
925fn system_time_to_millis(time: SystemTime) -> Option<MillisSinceEpoch> {
926    let millis = match time.duration_since(SystemTime::UNIX_EPOCH) {
927        Ok(duration) => i64::try_from(duration.as_millis()).ok()?,
928        Err(err) => -i64::try_from(err.duration().as_millis()).ok()?,
929    };
930    Some(MillisSinceEpoch(millis))
931}
932
933/// Create a new [`FileState`] from metadata.
934fn file_state(metadata: &Metadata) -> Result<Option<FileState>, MtimeOutOfRange> {
935    let metadata_file_type = metadata.file_type();
936    let file_type = if metadata_file_type.is_dir() {
937        None
938    } else if metadata_file_type.is_symlink() {
939        Some(FileType::Symlink)
940    } else if metadata_file_type.is_file() {
941        let exec_bit = ExecBit::new_from_disk(metadata);
942        Some(FileType::Normal { exec_bit })
943    } else {
944        None
945    };
946    if let Some(file_type) = file_type {
947        Ok(Some(FileState {
948            file_type,
949            mtime: mtime_from_metadata(metadata)?,
950            size: metadata.len(),
951            materialized_conflict_data: None,
952        }))
953    } else {
954        Ok(None)
955    }
956}
957
958struct FsmonitorMatcher {
959    matcher: Option<Box<dyn Matcher>>,
960    watchman_clock: Option<crate::protos::local_working_copy::WatchmanClock>,
961}
962
963/// Settings specific to the tree state of the [`LocalWorkingCopy`] backend.
964#[derive(Clone, Debug)]
965pub struct TreeStateSettings {
966    /// Conflict marker style to use when materializing files or when checking
967    /// changed files.
968    pub conflict_marker_style: ConflictMarkerStyle,
969    /// Configuring auto-converting CRLF line endings into LF when you add a
970    /// file to the backend, and vice versa when it checks out code onto your
971    /// filesystem.
972    pub eol_conversion_mode: EolConversionMode,
973    /// Whether to ignore changes to the executable bit for files on Unix.
974    pub exec_change_setting: ExecChangeSetting,
975    /// The fsmonitor (e.g. Watchman) to use, if any.
976    pub fsmonitor_settings: FsmonitorSettings,
977}
978
979impl TreeStateSettings {
980    /// Create [`TreeStateSettings`] from [`UserSettings`].
981    pub fn try_from_user_settings(user_settings: &UserSettings) -> Result<Self, ConfigGetError> {
982        Ok(Self {
983            conflict_marker_style: user_settings.get("ui.conflict-marker-style")?,
984            eol_conversion_mode: EolConversionMode::try_from_settings(user_settings)?,
985            exec_change_setting: user_settings.get("working-copy.exec-bit-change")?,
986            fsmonitor_settings: FsmonitorSettings::from_settings(user_settings)?,
987        })
988    }
989}
990
991pub struct TreeState {
992    store: Arc<Store>,
993    working_copy_path: PathBuf,
994    state_path: PathBuf,
995    tree: MergedTree,
996    file_states: FileStatesMap,
997    // Currently only path prefixes
998    sparse_patterns: Vec<RepoPathBuf>,
999    own_mtime: MillisSinceEpoch,
1000    symlink_support: bool,
1001
1002    /// The most recent clock value returned by Watchman. Will only be set if
1003    /// the repo is configured to use the Watchman filesystem monitor and
1004    /// Watchman has been queried at least once.
1005    watchman_clock: Option<crate::protos::local_working_copy::WatchmanClock>,
1006
1007    conflict_marker_style: ConflictMarkerStyle,
1008    exec_policy: ExecChangePolicy,
1009    fsmonitor_settings: FsmonitorSettings,
1010    target_eol_strategy: TargetEolStrategy,
1011}
1012
1013#[derive(Debug, Error)]
1014pub enum TreeStateError {
1015    #[error("Reading tree state from {path}")]
1016    ReadTreeState { path: PathBuf, source: io::Error },
1017    #[error("Decoding tree state from {path}")]
1018    DecodeTreeState {
1019        path: PathBuf,
1020        source: prost::DecodeError,
1021    },
1022    #[error("Writing tree state to temporary file {path}")]
1023    WriteTreeState { path: PathBuf, source: io::Error },
1024    #[error("Persisting tree state to file {path}")]
1025    PersistTreeState { path: PathBuf, source: io::Error },
1026    #[error("Filesystem monitor error")]
1027    Fsmonitor(#[source] Box<dyn Error + Send + Sync>),
1028}
1029
1030impl TreeState {
1031    pub fn working_copy_path(&self) -> &Path {
1032        &self.working_copy_path
1033    }
1034
1035    pub fn current_tree(&self) -> &MergedTree {
1036        &self.tree
1037    }
1038
1039    pub fn file_states(&self) -> FileStates<'_> {
1040        self.file_states.all()
1041    }
1042
1043    pub fn sparse_patterns(&self) -> &Vec<RepoPathBuf> {
1044        &self.sparse_patterns
1045    }
1046
1047    fn sparse_matcher(&self) -> Box<dyn Matcher> {
1048        Box::new(PrefixMatcher::new(&self.sparse_patterns))
1049    }
1050
1051    pub fn init(
1052        store: Arc<Store>,
1053        working_copy_path: PathBuf,
1054        state_path: PathBuf,
1055        tree_state_settings: &TreeStateSettings,
1056    ) -> Result<Self, TreeStateError> {
1057        let mut wc = Self::empty(store, working_copy_path, state_path, tree_state_settings);
1058        wc.save()?;
1059        Ok(wc)
1060    }
1061
1062    /// Like `init` but does not persist the initial empty tree state to
1063    /// disk. Use when the caller will save state itself only after a
1064    /// successful operation (e.g. to use `tree_state` file absence as a
1065    /// dirty marker).
1066    pub fn init_without_saving(
1067        store: Arc<Store>,
1068        working_copy_path: PathBuf,
1069        state_path: PathBuf,
1070        tree_state_settings: &TreeStateSettings,
1071    ) -> Self {
1072        Self::empty(store, working_copy_path, state_path, tree_state_settings)
1073    }
1074
1075    fn empty(
1076        store: Arc<Store>,
1077        working_copy_path: PathBuf,
1078        state_path: PathBuf,
1079        TreeStateSettings {
1080            conflict_marker_style,
1081            eol_conversion_mode,
1082            exec_change_setting,
1083            fsmonitor_settings,
1084        }: &TreeStateSettings,
1085    ) -> Self {
1086        let exec_policy = ExecChangePolicy::new(*exec_change_setting, &state_path);
1087        Self {
1088            store: store.clone(),
1089            working_copy_path,
1090            state_path,
1091            tree: store.empty_merged_tree(),
1092            file_states: FileStatesMap::new(),
1093            sparse_patterns: vec![RepoPathBuf::root()],
1094            own_mtime: MillisSinceEpoch(0),
1095            symlink_support: check_symlink_support().unwrap_or(false),
1096            watchman_clock: None,
1097            conflict_marker_style: *conflict_marker_style,
1098            exec_policy,
1099            fsmonitor_settings: fsmonitor_settings.clone(),
1100            target_eol_strategy: TargetEolStrategy::new(*eol_conversion_mode),
1101        }
1102    }
1103
1104    pub fn load(
1105        store: Arc<Store>,
1106        working_copy_path: PathBuf,
1107        state_path: PathBuf,
1108        tree_state_settings: &TreeStateSettings,
1109    ) -> Result<Self, TreeStateError> {
1110        let tree_state_path = state_path.join("tree_state");
1111        let file = match File::open(&tree_state_path) {
1112            Err(err) if err.kind() == io::ErrorKind::NotFound => {
1113                return Self::init(store, working_copy_path, state_path, tree_state_settings);
1114            }
1115            Err(err) => {
1116                return Err(TreeStateError::ReadTreeState {
1117                    path: tree_state_path,
1118                    source: err,
1119                });
1120            }
1121            Ok(file) => file,
1122        };
1123
1124        let mut wc = Self::empty(store, working_copy_path, state_path, tree_state_settings);
1125        wc.read(&tree_state_path, file)?;
1126        Ok(wc)
1127    }
1128
1129    fn update_own_mtime(&mut self) {
1130        if let Ok(metadata) = self.state_path.join("tree_state").symlink_metadata()
1131            && let Ok(mtime) = mtime_from_metadata(&metadata)
1132        {
1133            self.own_mtime = mtime;
1134        } else {
1135            self.own_mtime = MillisSinceEpoch(0);
1136        }
1137    }
1138
1139    fn read(&mut self, tree_state_path: &Path, mut file: File) -> Result<(), TreeStateError> {
1140        self.update_own_mtime();
1141        let mut buf = Vec::new();
1142        file.read_to_end(&mut buf)
1143            .map_err(|err| TreeStateError::ReadTreeState {
1144                path: tree_state_path.to_owned(),
1145                source: err,
1146            })?;
1147        let proto = crate::protos::local_working_copy::TreeState::decode(&*buf).map_err(|err| {
1148            TreeStateError::DecodeTreeState {
1149                path: tree_state_path.to_owned(),
1150                source: err,
1151            }
1152        })?;
1153        #[expect(deprecated)]
1154        if proto.tree_ids.is_empty() {
1155            self.tree = MergedTree::resolved(
1156                self.store.clone(),
1157                TreeId::new(proto.legacy_tree_id.clone()),
1158            );
1159        } else {
1160            let tree_ids_builder: MergeBuilder<TreeId> = proto
1161                .tree_ids
1162                .iter()
1163                .map(|id| TreeId::new(id.clone()))
1164                .collect();
1165            self.tree = MergedTree::new(
1166                self.store.clone(),
1167                tree_ids_builder.build(),
1168                ConflictLabels::from_vec(proto.conflict_labels),
1169            );
1170        }
1171        self.file_states =
1172            FileStatesMap::from_proto(proto.file_states, proto.is_file_states_sorted);
1173        self.sparse_patterns = sparse_patterns_from_proto(proto.sparse_patterns.as_ref());
1174        self.watchman_clock = proto.watchman_clock;
1175        Ok(())
1176    }
1177
1178    #[expect(clippy::assigning_clones, clippy::field_reassign_with_default)]
1179    pub fn save(&mut self) -> Result<(), TreeStateError> {
1180        let mut proto: crate::protos::local_working_copy::TreeState = Default::default();
1181        proto.tree_ids = self
1182            .tree
1183            .tree_ids()
1184            .iter()
1185            .map(|id| id.to_bytes())
1186            .collect();
1187        proto.conflict_labels = self.tree.labels().as_slice().to_owned();
1188        proto.file_states = self.file_states.data.clone();
1189        // `FileStatesMap` is guaranteed to be sorted.
1190        proto.is_file_states_sorted = true;
1191        let mut sparse_patterns = crate::protos::local_working_copy::SparsePatterns::default();
1192        for path in &self.sparse_patterns {
1193            sparse_patterns
1194                .prefixes
1195                .push(path.as_internal_file_string().to_owned());
1196        }
1197        proto.sparse_patterns = Some(sparse_patterns);
1198        proto.watchman_clock = self.watchman_clock.clone();
1199
1200        let wrap_write_err = |source| TreeStateError::WriteTreeState {
1201            path: self.state_path.clone(),
1202            source,
1203        };
1204        let mut temp_file = NamedTempFile::new_in(&self.state_path).map_err(wrap_write_err)?;
1205        temp_file
1206            .as_file_mut()
1207            .write_all(&proto.encode_to_vec())
1208            .map_err(wrap_write_err)?;
1209        // update own write time while we before we rename it, so we know
1210        // there is no unknown data in it
1211        self.update_own_mtime();
1212        // TODO: Retry if persisting fails (it will on Windows if the file happened to
1213        // be open for read).
1214        let target_path = self.state_path.join("tree_state");
1215        persist_temp_file(temp_file, &target_path).map_err(|source| {
1216            TreeStateError::PersistTreeState {
1217                path: target_path.clone(),
1218                source,
1219            }
1220        })?;
1221        Ok(())
1222    }
1223
1224    fn reset_watchman(&mut self) {
1225        self.watchman_clock.take();
1226    }
1227
1228    #[cfg(feature = "watchman")]
1229    #[instrument(skip(self))]
1230    pub async fn query_watchman(
1231        &self,
1232        config: &WatchmanConfig,
1233    ) -> Result<(watchman::Clock, Option<Vec<PathBuf>>), TreeStateError> {
1234        let previous_clock = self.watchman_clock.clone().map(watchman::Clock::from);
1235
1236        let tokio_fn = async || {
1237            let fsmonitor = watchman::Fsmonitor::init(&self.working_copy_path, config)
1238                .await
1239                .map_err(|err| TreeStateError::Fsmonitor(Box::new(err)))?;
1240            fsmonitor
1241                .query_changed_files(previous_clock)
1242                .await
1243                .map_err(|err| TreeStateError::Fsmonitor(Box::new(err)))
1244        };
1245
1246        match tokio::runtime::Handle::try_current() {
1247            Ok(_handle) => tokio_fn().await,
1248            Err(_) => {
1249                let runtime = tokio::runtime::Builder::new_current_thread()
1250                    .enable_all()
1251                    .build()
1252                    .map_err(|err| TreeStateError::Fsmonitor(Box::new(err)))?;
1253                runtime.block_on(tokio_fn())
1254            }
1255        }
1256    }
1257
1258    #[cfg(feature = "watchman")]
1259    #[instrument(skip(self))]
1260    pub async fn is_watchman_trigger_registered(
1261        &self,
1262        config: &WatchmanConfig,
1263    ) -> Result<bool, TreeStateError> {
1264        let tokio_fn = async || {
1265            let fsmonitor = watchman::Fsmonitor::init(&self.working_copy_path, config)
1266                .await
1267                .map_err(|err| TreeStateError::Fsmonitor(Box::new(err)))?;
1268            fsmonitor
1269                .is_trigger_registered()
1270                .await
1271                .map_err(|err| TreeStateError::Fsmonitor(Box::new(err)))
1272        };
1273
1274        match tokio::runtime::Handle::try_current() {
1275            Ok(_handle) => tokio_fn().await,
1276            Err(_) => {
1277                let runtime = tokio::runtime::Builder::new_current_thread()
1278                    .enable_all()
1279                    .build()
1280                    .map_err(|err| TreeStateError::Fsmonitor(Box::new(err)))?;
1281                runtime.block_on(tokio_fn())
1282            }
1283        }
1284    }
1285}
1286
1287/// Functions to snapshot local-disk files to the store.
1288impl TreeState {
1289    /// Look for changes to the working copy. If there are any changes, create
1290    /// a new tree from it.
1291    #[instrument(skip_all)]
1292    pub async fn snapshot(
1293        &mut self,
1294        options: &SnapshotOptions<'_>,
1295    ) -> Result<(bool, SnapshotStats), SnapshotError> {
1296        let SnapshotOptions {
1297            base_ignores,
1298            progress,
1299            start_tracking_matcher,
1300            force_tracking_matcher,
1301            max_new_file_size,
1302        } = options;
1303
1304        let sparse_matcher = self.sparse_matcher();
1305
1306        let fsmonitor_clock_needs_save = self.fsmonitor_settings != FsmonitorSettings::None;
1307        let mut is_dirty = fsmonitor_clock_needs_save;
1308        let FsmonitorMatcher {
1309            matcher: fsmonitor_matcher,
1310            watchman_clock,
1311        } = self
1312            .make_fsmonitor_matcher(&self.fsmonitor_settings)
1313            .await?;
1314        let fsmonitor_matcher = match fsmonitor_matcher.as_ref() {
1315            None => &EverythingMatcher,
1316            Some(fsmonitor_matcher) => fsmonitor_matcher.as_ref(),
1317        };
1318
1319        let matcher = IntersectionMatcher::new(
1320            sparse_matcher.as_ref(),
1321            UnionMatcher::new(fsmonitor_matcher, force_tracking_matcher),
1322        );
1323        if matcher.visit(RepoPath::root()).is_nothing() {
1324            // No need to load the current tree, set up channels, etc.
1325            self.watchman_clock = watchman_clock;
1326            return Ok((is_dirty, SnapshotStats::default()));
1327        }
1328
1329        let (tree_entries_tx, tree_entries_rx) = channel();
1330        let (file_states_tx, file_states_rx) = channel();
1331        let (untracked_paths_tx, untracked_paths_rx) = channel();
1332        let (invalid_utf8_paths_tx, invalid_utf8_paths_rx) = channel();
1333        let (deleted_files_tx, deleted_files_rx) = channel();
1334
1335        trace_span!("traverse filesystem").in_scope(|| -> Result<(), SnapshotError> {
1336            let snapshotter = FileSnapshotter {
1337                tree_state: self,
1338                current_tree: &self.tree,
1339                matcher: &matcher,
1340                start_tracking_matcher,
1341                force_tracking_matcher,
1342                // Move tx sides so they'll be dropped at the end of the scope.
1343                tree_entries_tx,
1344                file_states_tx,
1345                untracked_paths_tx,
1346                invalid_utf8_paths_tx,
1347                deleted_files_tx,
1348                error: OnceLock::new(),
1349                progress: *progress,
1350                max_new_file_size: *max_new_file_size,
1351            };
1352            let directory_to_visit = DirectoryToVisit {
1353                dir: RepoPathBuf::root(),
1354                disk_dir: self.working_copy_path.clone(),
1355                git_ignore: base_ignores.clone(),
1356                file_states: self.file_states.all(),
1357            };
1358            // Here we use scope as a queue of per-directory jobs.
1359            rayon::scope(|scope| {
1360                snapshotter.spawn_ok(scope, |scope| {
1361                    snapshotter.visit_directory(directory_to_visit, scope)
1362                });
1363            });
1364            snapshotter.into_result()
1365        })?;
1366
1367        let stats = SnapshotStats {
1368            untracked_paths: untracked_paths_rx.into_iter().collect(),
1369            invalid_utf8_paths: invalid_utf8_paths_rx.into_iter().collect(),
1370        };
1371        let mut tree_builder = MergedTreeBuilder::new(self.tree.clone());
1372        trace_span!("process tree entries").in_scope(|| {
1373            for (path, tree_values) in &tree_entries_rx {
1374                tree_builder.set_or_remove(path, tree_values);
1375            }
1376        });
1377        let deleted_files = trace_span!("process deleted tree entries").in_scope(|| {
1378            let deleted_files = HashSet::from_iter(deleted_files_rx);
1379            is_dirty |= !deleted_files.is_empty();
1380            for file in &deleted_files {
1381                tree_builder.set_or_remove(file.clone(), Merge::absent());
1382            }
1383            deleted_files
1384        });
1385        trace_span!("process file states").in_scope(|| {
1386            let changed_file_states = file_states_rx
1387                .iter()
1388                .sorted_unstable_by(|(path1, _), (path2, _)| path1.cmp(path2))
1389                .collect_vec();
1390            is_dirty |= !changed_file_states.is_empty();
1391            self.file_states
1392                .merge_in(changed_file_states, &deleted_files);
1393        });
1394        trace_span!("write tree")
1395            .in_scope(async || -> Result<(), BackendError> {
1396                let new_tree = tree_builder.write_tree().await?;
1397                is_dirty |= new_tree.tree_ids_and_labels() != self.tree.tree_ids_and_labels();
1398                self.tree = new_tree.clone();
1399                Ok(())
1400            })
1401            .await?;
1402        if cfg!(debug_assertions) {
1403            let tree_paths: HashSet<_> = self
1404                .tree
1405                .entries_matching(sparse_matcher.as_ref())
1406                .filter_map(|(path, result)| result.is_ok().then_some(path))
1407                .collect();
1408            let file_states = self.file_states.all();
1409            let state_paths: HashSet<_> = file_states.paths().map(|path| path.to_owned()).collect();
1410            assert_eq!(state_paths, tree_paths);
1411        }
1412        // Since untracked paths aren't cached in the tree state, we'll need to
1413        // rescan the working directory changes to report or track them later.
1414        // TODO: store untracked paths and update watchman_clock?
1415        if (stats.untracked_paths.is_empty() && stats.invalid_utf8_paths.is_empty())
1416            || watchman_clock.is_none()
1417        {
1418            self.watchman_clock = watchman_clock;
1419        } else {
1420            tracing::info!("not updating watchman clock because there are untracked files");
1421        }
1422        Ok((is_dirty, stats))
1423    }
1424
1425    #[instrument(skip_all)]
1426    async fn make_fsmonitor_matcher(
1427        &self,
1428        fsmonitor_settings: &FsmonitorSettings,
1429    ) -> Result<FsmonitorMatcher, SnapshotError> {
1430        let (watchman_clock, changed_files) = match fsmonitor_settings {
1431            FsmonitorSettings::None => (None, None),
1432            FsmonitorSettings::Test { changed_files } => (None, Some(changed_files.clone())),
1433            #[cfg(feature = "watchman")]
1434            FsmonitorSettings::Watchman(config) => match self.query_watchman(config).await {
1435                Ok((watchman_clock, changed_files)) => (Some(watchman_clock.into()), changed_files),
1436                Err(err) => {
1437                    tracing::warn!(?err, "Failed to query filesystem monitor");
1438                    (None, None)
1439                }
1440            },
1441            #[cfg(not(feature = "watchman"))]
1442            FsmonitorSettings::Watchman(_) => {
1443                return Err(SnapshotError::Other {
1444                    message: "Failed to query the filesystem monitor".to_string(),
1445                    err: "Cannot query Watchman because jj was not compiled with the `watchman` \
1446                          feature (consider disabling `fsmonitor.backend`)"
1447                        .into(),
1448                });
1449            }
1450        };
1451        let matcher: Option<Box<dyn Matcher>> = match changed_files {
1452            None => None,
1453            Some(changed_files) => {
1454                let (repo_paths, gitignore_prefixes) = trace_span!("processing fsmonitor paths")
1455                    .in_scope(|| {
1456                        let repo_paths = changed_files
1457                            .iter()
1458                            .filter_map(|path| RepoPathBuf::from_relative_path(path).ok())
1459                            .collect_vec();
1460                        // .gitignore changes require rescanning parent directories to pick up newly
1461                        // unignored files.
1462                        let gitignore_prefixes = repo_paths
1463                            .iter()
1464                            .filter_map(|repo_path| {
1465                                let (parent, basename) = repo_path.split()?;
1466                                (basename.as_internal_str() == ".gitignore")
1467                                    .then(|| parent.to_owned())
1468                            })
1469                            .collect_vec();
1470                        (repo_paths, gitignore_prefixes)
1471                    });
1472
1473                let matcher: Box<dyn Matcher> = if gitignore_prefixes.is_empty() {
1474                    Box::new(FilesMatcher::new(repo_paths))
1475                } else {
1476                    Box::new(UnionMatcher::new(
1477                        FilesMatcher::new(repo_paths),
1478                        PrefixMatcher::new(gitignore_prefixes),
1479                    ))
1480                };
1481
1482                Some(matcher)
1483            }
1484        };
1485        Ok(FsmonitorMatcher {
1486            matcher,
1487            watchman_clock,
1488        })
1489    }
1490}
1491
1492struct DirectoryToVisit<'a> {
1493    dir: RepoPathBuf,
1494    disk_dir: PathBuf,
1495    git_ignore: Arc<GitIgnoreFile>,
1496    file_states: FileStates<'a>,
1497}
1498
1499#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1500enum PresentDirEntryKind {
1501    Dir,
1502    File,
1503}
1504
1505#[derive(Clone, Debug)]
1506struct PresentDirEntries {
1507    dirs: HashSet<String>,
1508    files: HashSet<String>,
1509}
1510
1511/// Helper to scan local-disk directories and files in parallel.
1512struct FileSnapshotter<'a> {
1513    tree_state: &'a TreeState,
1514    current_tree: &'a MergedTree,
1515    matcher: &'a dyn Matcher,
1516    start_tracking_matcher: &'a dyn Matcher,
1517    force_tracking_matcher: &'a dyn Matcher,
1518    tree_entries_tx: Sender<(RepoPathBuf, MergedTreeValue)>,
1519    file_states_tx: Sender<(RepoPathBuf, FileState)>,
1520    untracked_paths_tx: Sender<(RepoPathBuf, UntrackedReason)>,
1521    invalid_utf8_paths_tx: Sender<(RepoPathBuf, OsString)>,
1522    deleted_files_tx: Sender<RepoPathBuf>,
1523    error: OnceLock<SnapshotError>,
1524    progress: Option<&'a SnapshotProgress<'a>>,
1525    max_new_file_size: u64,
1526}
1527
1528impl FileSnapshotter<'_> {
1529    fn spawn_ok<'scope, F>(&'scope self, scope: &rayon::Scope<'scope>, body: F)
1530    where
1531        F: FnOnce(&rayon::Scope<'scope>) -> Result<(), SnapshotError> + Send + 'scope,
1532    {
1533        scope.spawn(|scope| {
1534            if self.error.get().is_some() {
1535                return;
1536            }
1537            match body(scope) {
1538                Ok(()) => {}
1539                Err(err) => self.error.set(err).unwrap_or(()),
1540            }
1541        });
1542    }
1543
1544    /// Extracts the result of the snapshot.
1545    fn into_result(self) -> Result<(), SnapshotError> {
1546        match self.error.into_inner() {
1547            Some(err) => Err(err),
1548            None => Ok(()),
1549        }
1550    }
1551
1552    /// Visits the directory entries, spawns jobs to recurse into sub
1553    /// directories.
1554    fn visit_directory<'scope>(
1555        &'scope self,
1556        directory_to_visit: DirectoryToVisit<'scope>,
1557        scope: &rayon::Scope<'scope>,
1558    ) -> Result<(), SnapshotError> {
1559        let DirectoryToVisit {
1560            dir,
1561            disk_dir,
1562            git_ignore,
1563            file_states,
1564        } = directory_to_visit;
1565
1566        let git_ignore = git_ignore.chain_with_file(&dir, disk_dir.join(".gitignore"))?;
1567        let dir_entries: Vec<_> = disk_dir
1568            .read_dir()
1569            .and_then(|entries| entries.try_collect())
1570            .map_err(|err| SnapshotError::Other {
1571                message: format!("Failed to read directory {}", disk_dir.display()),
1572                err: err.into(),
1573            })?;
1574        let (dirs, files) = dir_entries
1575            .into_par_iter()
1576            // Don't split into too many small jobs. For a small directory,
1577            // sequential scan should be fast enough.
1578            .with_min_len(100)
1579            .filter_map(|entry| {
1580                self.process_dir_entry(&dir, &git_ignore, file_states, &entry, scope)
1581                    .block_on()
1582                    .transpose()
1583            })
1584            .map(|item| match item {
1585                Ok((PresentDirEntryKind::Dir, name)) => Ok(Either::Left(name)),
1586                Ok((PresentDirEntryKind::File, name)) => Ok(Either::Right(name)),
1587                Err(err) => Err(err),
1588            })
1589            .collect::<Result<_, _>>()?;
1590        let present_entries = PresentDirEntries { dirs, files };
1591        self.emit_deleted_files(&dir, file_states, &present_entries);
1592        Ok(())
1593    }
1594
1595    async fn process_dir_entry<'scope>(
1596        &'scope self,
1597        dir: &RepoPath,
1598        git_ignore: &Arc<GitIgnoreFile>,
1599        file_states: FileStates<'scope>,
1600        entry: &DirEntry,
1601        scope: &rayon::Scope<'scope>,
1602    ) -> Result<Option<(PresentDirEntryKind, String)>, SnapshotError> {
1603        let file_type = entry.file_type().unwrap();
1604        let file_name = entry.file_name();
1605        let name_string = match file_name.into_string() {
1606            Ok(name_string) => name_string,
1607            Err(name) => {
1608                // A path that isn't valid UTF-8 can't be represented as a
1609                // RepoPath, so it can never be tracked. Skip it instead of
1610                // failing the whole snapshot, and let the caller report it.
1611                self.invalid_utf8_paths_tx.send((dir.to_owned(), name)).ok();
1612                return Ok(None);
1613            }
1614        };
1615
1616        if RESERVED_DIR_NAMES.contains(&name_string.as_str()) {
1617            return Ok(None);
1618        }
1619        let name = RepoPathComponent::new(&name_string).unwrap();
1620        let path = dir.join(name);
1621        let maybe_current_file_state = file_states.get_at(dir, name);
1622        if let Some(file_state) = &maybe_current_file_state
1623            && file_state.file_type == FileType::GitSubmodule
1624        {
1625            return Ok(None);
1626        }
1627
1628        if file_type.is_dir() {
1629            let file_states = file_states.prefixed_at(dir, name);
1630            // If a submodule was added in commit C, and a user decides to run
1631            // `jj new <something before C>` from after C, then the submodule
1632            // files stick around but it is no longer seen as a submodule.
1633            // We need to ensure that it is not tracked as if it was added to
1634            // the main repo.
1635            // See https://github.com/jj-vcs/jj/issues/4349.
1636            // To solve this, we ignore all nested repos entirely.
1637            let disk_dir = entry.path();
1638            for &name in RESERVED_DIR_NAMES {
1639                if disk_dir.join(name).symlink_metadata().is_ok() {
1640                    return Ok(None);
1641                }
1642            }
1643
1644            if git_ignore.matches_dir(&path)
1645                && self.force_tracking_matcher.visit(&path).is_nothing()
1646            {
1647                // If the whole directory is ignored by .gitignore, visit only
1648                // paths we're already tracking. This is because .gitignore in
1649                // ignored directory must be ignored. It's also more efficient.
1650                // start_tracking_matcher is NOT tested here because we need to
1651                // scan directory entries to report untracked paths.
1652                self.spawn_ok(scope, move |_| {
1653                    self.visit_tracked_files(file_states).block_on()
1654                });
1655            } else if !self.matcher.visit(&path).is_nothing() {
1656                let directory_to_visit = DirectoryToVisit {
1657                    dir: path,
1658                    disk_dir,
1659                    git_ignore: git_ignore.clone(),
1660                    file_states,
1661                };
1662                self.spawn_ok(scope, |scope| {
1663                    self.visit_directory(directory_to_visit, scope)
1664                });
1665            }
1666            // Whether or not the directory path matches, any child file entries
1667            // shouldn't be touched within the current recursion step.
1668            Ok(Some((PresentDirEntryKind::Dir, name_string)))
1669        } else if self.matcher.matches(&path) {
1670            if let Some(progress) = self.progress {
1671                progress(&path);
1672            }
1673            if maybe_current_file_state.is_none()
1674                && (git_ignore.matches_file(&path) && !self.force_tracking_matcher.matches(&path))
1675            {
1676                // If it wasn't already tracked and it matches
1677                // the ignored paths, then ignore it.
1678                Ok(None)
1679            } else if maybe_current_file_state.is_none()
1680                && !self.start_tracking_matcher.matches(&path)
1681            {
1682                // Leave the file untracked
1683                self.untracked_paths_tx
1684                    .send((path, UntrackedReason::FileNotAutoTracked))
1685                    .ok();
1686                Ok(None)
1687            } else {
1688                let metadata = entry.metadata().map_err(|err| SnapshotError::Other {
1689                    message: format!("Failed to stat file {}", entry.path().display()),
1690                    err: err.into(),
1691                })?;
1692                if maybe_current_file_state.is_none()
1693                    && (metadata.len() > self.max_new_file_size
1694                        && !self.force_tracking_matcher.matches(&path))
1695                {
1696                    // Leave the large file untracked
1697                    let reason = UntrackedReason::FileTooLarge {
1698                        size: metadata.len(),
1699                        max_size: self.max_new_file_size,
1700                    };
1701                    self.untracked_paths_tx.send((path, reason)).ok();
1702                    Ok(None)
1703                } else if let Some(new_file_state) = file_state(&metadata)
1704                    .map_err(|err| snapshot_error_for_mtime_out_of_range(err, &entry.path()))?
1705                {
1706                    self.process_present_file(
1707                        path,
1708                        &entry.path(),
1709                        maybe_current_file_state.as_ref(),
1710                        new_file_state,
1711                    )
1712                    .await?;
1713                    Ok(Some((PresentDirEntryKind::File, name_string)))
1714                } else {
1715                    // Special file is not considered present
1716                    Ok(None)
1717                }
1718            }
1719        } else {
1720            Ok(None)
1721        }
1722    }
1723
1724    /// Visits only paths we're already tracking.
1725    async fn visit_tracked_files(&self, file_states: FileStates<'_>) -> Result<(), SnapshotError> {
1726        for (tracked_path, current_file_state) in file_states {
1727            if current_file_state.file_type == FileType::GitSubmodule {
1728                continue;
1729            }
1730            if !self.matcher.matches(tracked_path) {
1731                continue;
1732            }
1733            let disk_path = tracked_path.to_fs_path(&self.tree_state.working_copy_path)?;
1734            let metadata = match disk_path.symlink_metadata() {
1735                Ok(metadata) => Some(metadata),
1736                Err(err) if err.kind() == io::ErrorKind::NotFound => None,
1737                Err(err) => {
1738                    return Err(SnapshotError::Other {
1739                        message: format!("Failed to stat file {}", disk_path.display()),
1740                        err: err.into(),
1741                    });
1742                }
1743            };
1744            if let Some(metadata) = &metadata
1745                && let Some(new_file_state) = file_state(metadata)
1746                    .map_err(|err| snapshot_error_for_mtime_out_of_range(err, &disk_path))?
1747            {
1748                self.process_present_file(
1749                    tracked_path.to_owned(),
1750                    &disk_path,
1751                    Some(&current_file_state),
1752                    new_file_state,
1753                )
1754                .await?;
1755            } else {
1756                self.deleted_files_tx.send(tracked_path.to_owned()).ok();
1757            }
1758        }
1759        Ok(())
1760    }
1761
1762    async fn process_present_file(
1763        &self,
1764        path: RepoPathBuf,
1765        disk_path: &Path,
1766        maybe_current_file_state: Option<&FileState>,
1767        mut new_file_state: FileState,
1768    ) -> Result<(), SnapshotError> {
1769        let update = self
1770            .get_updated_tree_value(&path, disk_path, maybe_current_file_state, &new_file_state)
1771            .await?;
1772        // Preserve materialized conflict data for normal, non-resolved files
1773        if matches!(new_file_state.file_type, FileType::Normal { .. })
1774            && !update.as_ref().is_some_and(|update| update.is_resolved())
1775        {
1776            new_file_state.materialized_conflict_data =
1777                maybe_current_file_state.and_then(|state| state.materialized_conflict_data);
1778        }
1779        if let Some(tree_value) = update {
1780            self.tree_entries_tx.send((path.clone(), tree_value)).ok();
1781        }
1782        if Some(&new_file_state) != maybe_current_file_state {
1783            self.file_states_tx.send((path, new_file_state)).ok();
1784        }
1785        Ok(())
1786    }
1787
1788    /// Emits file paths that don't exist in the `present_entries`.
1789    fn emit_deleted_files(
1790        &self,
1791        dir: &RepoPath,
1792        file_states: FileStates<'_>,
1793        present_entries: &PresentDirEntries,
1794    ) {
1795        let file_state_chunks = file_states.iter().chunk_by(|(path, _state)| {
1796            // Extract <name> from <dir>, <dir>/<name>, or <dir>/<name>/**.
1797            // (file_states may contain <dir> file on file->dir transition.)
1798            debug_assert!(path.starts_with(dir));
1799            let slash = usize::from(!dir.is_root());
1800            let len = dir.as_internal_file_string().len() + slash;
1801            let tail = path.as_internal_file_string().get(len..).unwrap_or("");
1802            match tail.split_once('/') {
1803                Some((name, _)) => (PresentDirEntryKind::Dir, name),
1804                None => (PresentDirEntryKind::File, tail),
1805            }
1806        });
1807        file_state_chunks
1808            .into_iter()
1809            .filter(|&((kind, name), _)| match kind {
1810                PresentDirEntryKind::Dir => !present_entries.dirs.contains(name),
1811                PresentDirEntryKind::File => !present_entries.files.contains(name),
1812            })
1813            .flat_map(|(_, chunk)| chunk)
1814            // Whether or not the entry exists, submodule should be ignored
1815            .filter(|(_, state)| state.file_type != FileType::GitSubmodule)
1816            .filter(|(path, _)| self.matcher.matches(path))
1817            .try_for_each(|(path, _)| self.deleted_files_tx.send(path.to_owned()))
1818            .ok();
1819    }
1820
1821    async fn get_updated_tree_value(
1822        &self,
1823        repo_path: &RepoPath,
1824        disk_path: &Path,
1825        maybe_current_file_state: Option<&FileState>,
1826        new_file_state: &FileState,
1827    ) -> Result<Option<MergedTreeValue>, SnapshotError> {
1828        let clean = match maybe_current_file_state {
1829            None => {
1830                // untracked
1831                false
1832            }
1833            Some(current_file_state) => {
1834                // If the file's mtime was set at the same time as this state file's own mtime,
1835                // then we don't know if the file was modified before or after this state file.
1836                new_file_state.is_clean(current_file_state)
1837                    && current_file_state.mtime < self.tree_state.own_mtime
1838            }
1839        };
1840        if clean {
1841            Ok(None)
1842        } else {
1843            let current_tree_values = self.current_tree.path_value(repo_path).await?;
1844            let new_file_type = if !self.tree_state.symlink_support {
1845                let mut new_file_type = new_file_state.file_type.clone();
1846                if matches!(new_file_type, FileType::Normal { .. })
1847                    && matches!(current_tree_values.as_normal(), Some(TreeValue::Symlink(_)))
1848                {
1849                    new_file_type = FileType::Symlink;
1850                }
1851                new_file_type
1852            } else {
1853                new_file_state.file_type.clone()
1854            };
1855            let new_tree_values = match new_file_type {
1856                FileType::Normal { exec_bit } => {
1857                    self.write_path_to_store(
1858                        repo_path,
1859                        disk_path,
1860                        &current_tree_values,
1861                        exec_bit,
1862                        maybe_current_file_state.and_then(|state| state.materialized_conflict_data),
1863                    )
1864                    .await?
1865                }
1866                FileType::Symlink => {
1867                    let id = self.write_symlink_to_store(repo_path, disk_path).await?;
1868                    Merge::normal(TreeValue::Symlink(id))
1869                }
1870                FileType::GitSubmodule => panic!("git submodule cannot be written to store"),
1871            };
1872            if new_tree_values != current_tree_values {
1873                Ok(Some(new_tree_values))
1874            } else {
1875                Ok(None)
1876            }
1877        }
1878    }
1879
1880    fn store(&self) -> &Store {
1881        &self.tree_state.store
1882    }
1883
1884    async fn write_path_to_store(
1885        &self,
1886        repo_path: &RepoPath,
1887        disk_path: &Path,
1888        current_tree_values: &MergedTreeValue,
1889        exec_bit: ExecBit,
1890        materialized_conflict_data: Option<MaterializedConflictData>,
1891    ) -> Result<MergedTreeValue, SnapshotError> {
1892        if let Some(current_tree_value) = current_tree_values.as_resolved() {
1893            let id = self.write_file_to_store(repo_path, disk_path).await?;
1894            // On Windows, we preserve the executable bit from the current tree.
1895            let executable = exec_bit.for_tree_value(self.tree_state.exec_policy, || {
1896                if let Some(TreeValue::File {
1897                    id: _,
1898                    executable,
1899                    copy_id: _,
1900                }) = current_tree_value
1901                {
1902                    Some(*executable)
1903                } else {
1904                    None
1905                }
1906            });
1907            // Preserve the copy id from the current tree
1908            let copy_id = {
1909                if let Some(TreeValue::File {
1910                    id: _,
1911                    executable: _,
1912                    copy_id,
1913                }) = current_tree_value
1914                {
1915                    copy_id.clone()
1916                } else {
1917                    CopyId::placeholder()
1918                }
1919            };
1920            Ok(Merge::normal(TreeValue::File {
1921                id,
1922                executable,
1923                copy_id,
1924            }))
1925        } else if let Some(old_file_ids) = current_tree_values.to_file_merge() {
1926            // Safe to unwrap because the copy id exists exactly on the file variant
1927            let copy_id_merge = current_tree_values.to_copy_id_merge().unwrap();
1928            let copy_id = copy_id_merge
1929                .resolve_trivial(SameChange::Accept)
1930                .cloned()
1931                .flatten()
1932                .unwrap_or_else(CopyId::placeholder);
1933            let mut contents = vec![];
1934            let file = File::open(disk_path).map_err(|err| SnapshotError::Other {
1935                message: format!("Failed to open file {}", disk_path.display()),
1936                err: err.into(),
1937            })?;
1938            self.tree_state
1939                .target_eol_strategy
1940                .convert_eol_for_snapshot(AllowStdIo::new(file))
1941                .await
1942                .map_err(|err| SnapshotError::Other {
1943                    message: "Failed to convert the EOL".to_string(),
1944                    err: err.into(),
1945                })?
1946                .read_to_end(&mut contents)
1947                .await
1948                .map_err(|err| SnapshotError::Other {
1949                    message: "Failed to read the EOL converted contents".to_string(),
1950                    err: err.into(),
1951                })?;
1952            // If the file contained a conflict before and is a normal file on
1953            // disk, we try to parse any conflict markers in the file into a
1954            // conflict.
1955            let new_file_ids = conflicts::update_from_content(
1956                &old_file_ids,
1957                self.store(),
1958                repo_path,
1959                &contents,
1960                materialized_conflict_data.map_or(MIN_CONFLICT_MARKER_LEN, |data| {
1961                    data.conflict_marker_len as usize
1962                }),
1963            )
1964            .await?;
1965            match new_file_ids.into_resolved() {
1966                Ok(file_id) => {
1967                    // On Windows, we preserve the executable bit from the merged trees.
1968                    let executable = exec_bit.for_tree_value(self.tree_state.exec_policy, || {
1969                        current_tree_values
1970                            .to_executable_merge()
1971                            .as_ref()
1972                            .and_then(conflicts::resolve_file_executable)
1973                    });
1974                    Ok(Merge::normal(TreeValue::File {
1975                        id: file_id.unwrap(),
1976                        executable,
1977                        copy_id,
1978                    }))
1979                }
1980                Err(new_file_ids) => {
1981                    if new_file_ids != old_file_ids {
1982                        Ok(current_tree_values.with_new_file_ids(&new_file_ids))
1983                    } else {
1984                        Ok(current_tree_values.clone())
1985                    }
1986                }
1987            }
1988        } else {
1989            Ok(current_tree_values.clone())
1990        }
1991    }
1992
1993    async fn write_file_to_store(
1994        &self,
1995        path: &RepoPath,
1996        disk_path: &Path,
1997    ) -> Result<FileId, SnapshotError> {
1998        let file = File::open(disk_path).map_err(|err| SnapshotError::Other {
1999            message: format!("Failed to open file {}", disk_path.display()),
2000            err: err.into(),
2001        })?;
2002        let mut contents = self
2003            .tree_state
2004            .target_eol_strategy
2005            .convert_eol_for_snapshot(AllowStdIo::new(file))
2006            .await
2007            .map_err(|err| SnapshotError::Other {
2008                message: "Failed to convert the EOL".to_string(),
2009                err: err.into(),
2010            })?;
2011        Ok(self.store().write_file(path, &mut contents).await?)
2012    }
2013
2014    async fn write_symlink_to_store(
2015        &self,
2016        path: &RepoPath,
2017        disk_path: &Path,
2018    ) -> Result<SymlinkId, SnapshotError> {
2019        if self.tree_state.symlink_support {
2020            let target = disk_path.read_link().map_err(|err| SnapshotError::Other {
2021                message: format!("Failed to read symlink {}", disk_path.display()),
2022                err: err.into(),
2023            })?;
2024            let str_target = symlink_target_convert_to_store(&target).ok_or_else(|| {
2025                SnapshotError::InvalidUtf8SymlinkTarget {
2026                    path: disk_path.to_path_buf(),
2027                }
2028            })?;
2029            Ok(self.store().write_symlink(path, &str_target).await?)
2030        } else {
2031            let target = fs::read(disk_path).map_err(|err| SnapshotError::Other {
2032                message: format!("Failed to read file {}", disk_path.display()),
2033                err: err.into(),
2034            })?;
2035            let string_target =
2036                String::from_utf8(target).map_err(|_| SnapshotError::InvalidUtf8SymlinkTarget {
2037                    path: disk_path.to_path_buf(),
2038                })?;
2039            Ok(self.store().write_symlink(path, &string_target).await?)
2040        }
2041    }
2042}
2043
2044fn snapshot_error_for_mtime_out_of_range(err: MtimeOutOfRange, path: &Path) -> SnapshotError {
2045    SnapshotError::Other {
2046        message: format!("Failed to process file metadata {}", path.display()),
2047        err: err.into(),
2048    }
2049}
2050
2051/// Functions to update local-disk files from the store.
2052impl TreeState {
2053    async fn write_file(
2054        &self,
2055        disk_path: &Path,
2056        contents: impl AsyncRead + Send + Unpin,
2057        exec_bit: ExecBit,
2058        apply_eol_conversion: bool,
2059    ) -> Result<FileState, CheckoutError> {
2060        let mut file = File::options()
2061            .write(true)
2062            .create_new(true) // Don't overwrite un-ignored file. Don't follow symlink.
2063            .open(disk_path)
2064            .map_err(|err| CheckoutError::Other {
2065                message: format!("Failed to open file {} for writing", disk_path.display()),
2066                err: err.into(),
2067            })?;
2068        let contents = if apply_eol_conversion {
2069            self.target_eol_strategy
2070                .convert_eol_for_update(contents)
2071                .await
2072                .map_err(|err| CheckoutError::Other {
2073                    message: "Failed to convert the EOL for the content".to_string(),
2074                    err: err.into(),
2075                })?
2076        } else {
2077            Box::new(contents)
2078        };
2079        let size = copy_async_to_sync(contents, &mut file)
2080            .await
2081            .map_err(|err| CheckoutError::Other {
2082                message: format!(
2083                    "Failed to write the content to the file {}",
2084                    disk_path.display()
2085                ),
2086                err: err.into(),
2087            })?;
2088        set_executable(exec_bit, disk_path)
2089            .map_err(|err| checkout_error_for_stat_error(err, disk_path))?;
2090        // Read the file state from the file descriptor. That way, know that the file
2091        // exists and is of the expected type, and the stat information is most likely
2092        // accurate, except for other processes modifying the file concurrently (The
2093        // mtime is set at write time and won't change when we close the file.)
2094        let metadata = file
2095            .metadata()
2096            .map_err(|err| checkout_error_for_stat_error(err, disk_path))?;
2097        FileState::for_file(exec_bit, size as u64, &metadata)
2098            .map_err(|err| checkout_error_for_mtime_out_of_range(err, disk_path))
2099    }
2100
2101    fn write_symlink(&self, disk_path: &Path, target: String) -> Result<FileState, CheckoutError> {
2102        let target = symlink_target_convert_to_disk(&target);
2103
2104        if cfg!(windows) {
2105            // On Windows, "/" can't be part of valid file name, and "/" is also not a valid
2106            // separator for the symlink target. See an example of this issue in
2107            // https://github.com/jj-vcs/jj/issues/6934.
2108            //
2109            // We use debug_assert_* instead of assert_* because we want to avoid panic in
2110            // release build, and we are sure that we shouldn't create invalid symlinks in
2111            // tests.
2112            debug_assert_ne!(
2113                target.as_os_str().to_str().map(|path| path.contains('/')),
2114                Some(true),
2115                r#"Expect the symlink target doesn't contain "/", but got invalid symlink target: {}."#,
2116                target.display()
2117            );
2118        }
2119
2120        // On Windows, this will create a nonfunctional link for directories,
2121        // but at the moment we don't have enough information in the tree to
2122        // determine whether the symlink target is a file or a directory.
2123        symlink_file(&target, disk_path).map_err(|err| CheckoutError::Other {
2124            message: format!(
2125                "Failed to create symlink from {} to {}",
2126                disk_path.display(),
2127                target.display()
2128            ),
2129            err: err.into(),
2130        })?;
2131        let metadata = disk_path
2132            .symlink_metadata()
2133            .map_err(|err| checkout_error_for_stat_error(err, disk_path))?;
2134        FileState::for_symlink(&metadata)
2135            .map_err(|err| checkout_error_for_mtime_out_of_range(err, disk_path))
2136    }
2137
2138    async fn write_conflict(
2139        &self,
2140        disk_path: &Path,
2141        contents: &[u8],
2142        exec_bit: ExecBit,
2143    ) -> Result<FileState, CheckoutError> {
2144        let contents = self
2145            .target_eol_strategy
2146            .convert_eol_for_update(contents)
2147            .await
2148            .map_err(|err| CheckoutError::Other {
2149                message: "Failed to convert the EOL when writing a merge conflict".to_string(),
2150                err: err.into(),
2151            })?;
2152        let mut file = OpenOptions::new()
2153            .write(true)
2154            .create_new(true) // Don't overwrite un-ignored file. Don't follow symlink.
2155            .open(disk_path)
2156            .map_err(|err| CheckoutError::Other {
2157                message: format!("Failed to open file {} for writing", disk_path.display()),
2158                err: err.into(),
2159            })?;
2160        let size = copy_async_to_sync(contents, &mut file)
2161            .await
2162            .map_err(|err| CheckoutError::Other {
2163                message: format!("Failed to write conflict to file {}", disk_path.display()),
2164                err: err.into(),
2165            })? as u64;
2166        set_executable(exec_bit, disk_path)
2167            .map_err(|err| checkout_error_for_stat_error(err, disk_path))?;
2168        let metadata = file
2169            .metadata()
2170            .map_err(|err| checkout_error_for_stat_error(err, disk_path))?;
2171        FileState::for_file(exec_bit, size, &metadata)
2172            .map_err(|err| checkout_error_for_mtime_out_of_range(err, disk_path))
2173    }
2174
2175    pub fn check_out(&mut self, new_tree: &MergedTree) -> Result<CheckoutStats, CheckoutError> {
2176        let old_tree = self.tree.clone();
2177        let stats = self
2178            .update(&old_tree, new_tree, self.sparse_matcher().as_ref())
2179            .block_on()?;
2180        self.tree = new_tree.clone();
2181        Ok(stats)
2182    }
2183
2184    pub fn set_sparse_patterns(
2185        &mut self,
2186        sparse_patterns: Vec<RepoPathBuf>,
2187    ) -> Result<CheckoutStats, CheckoutError> {
2188        let tree = self.tree.clone();
2189        let old_matcher = PrefixMatcher::new(&self.sparse_patterns);
2190        let new_matcher = PrefixMatcher::new(&sparse_patterns);
2191        let added_matcher = DifferenceMatcher::new(&new_matcher, &old_matcher);
2192        let removed_matcher = DifferenceMatcher::new(&old_matcher, &new_matcher);
2193        let empty_tree = self.store.empty_merged_tree();
2194        let added_stats = self.update(&empty_tree, &tree, &added_matcher).block_on()?;
2195        let removed_stats = self
2196            .update(&tree, &empty_tree, &removed_matcher)
2197            .block_on()?;
2198        self.sparse_patterns = sparse_patterns;
2199        assert_eq!(added_stats.updated_files, 0);
2200        assert_eq!(added_stats.removed_files, 0);
2201        assert_eq!(removed_stats.updated_files, 0);
2202        assert_eq!(removed_stats.added_files, 0);
2203        assert_eq!(removed_stats.skipped_files, 0);
2204        Ok(CheckoutStats {
2205            updated_files: 0,
2206            added_files: added_stats.added_files,
2207            removed_files: removed_stats.removed_files,
2208            skipped_files: added_stats.skipped_files,
2209        })
2210    }
2211
2212    async fn update(
2213        &mut self,
2214        old_tree: &MergedTree,
2215        new_tree: &MergedTree,
2216        matcher: &dyn Matcher,
2217    ) -> Result<CheckoutStats, CheckoutError> {
2218        // TODO: maybe it's better not include the skipped counts in the "intended"
2219        // counts
2220        let mut stats = CheckoutStats {
2221            updated_files: 0,
2222            added_files: 0,
2223            removed_files: 0,
2224            skipped_files: 0,
2225        };
2226        let mut changed_file_states = Vec::new();
2227        let mut deleted_files = HashSet::new();
2228        let mut prev_created_path: RepoPathBuf = RepoPathBuf::root();
2229
2230        let mut process_diff_entry = async |path: RepoPathBuf,
2231                                            before: MergedTreeValue,
2232                                            after: MaterializedTreeValue|
2233               -> Result<(), CheckoutError> {
2234            if after.is_absent() {
2235                stats.removed_files += 1;
2236            } else if before.is_absent() {
2237                stats.added_files += 1;
2238            } else {
2239                stats.updated_files += 1;
2240            }
2241
2242            // Existing Git submodule can be a non-empty directory on disk. We
2243            // shouldn't attempt to manage it as a tracked path.
2244            //
2245            // TODO: It might be better to add general support for paths not
2246            // tracked by jj than processing submodules specially. For example,
2247            // paths excluded by .gitignore can be marked as such so that
2248            // newly-"unignored" paths won't be snapshotted automatically.
2249            if matches!(before.as_normal(), Some(TreeValue::GitSubmodule(_)))
2250                && matches!(after, MaterializedTreeValue::GitSubmodule(_))
2251            {
2252                eprintln!("ignoring git submodule at {path:?}");
2253                // Not updating the file state as if there were no diffs. Leave
2254                // the state type as FileType::GitSubmodule if it was before.
2255                return Ok(());
2256            }
2257
2258            // This path and the previous one we did work for may have a common prefix. We
2259            // can adjust the "working copy" path to the parent directory which we know
2260            // is already created. If there is no common prefix, this will by default use
2261            // RepoPath::root() as the common prefix.
2262            let (common_prefix, adjusted_diff_file_path) =
2263                path.split_common_prefix(&prev_created_path);
2264
2265            let disk_path = if adjusted_diff_file_path.is_root() {
2266                // The path being "root" here implies that the entire path has already been
2267                // created.
2268                //
2269                // e.g we may have have already processed a path like: "foo/bar/baz" and this is
2270                // our `prev_created_path`.
2271                //
2272                // and the current path is:
2273                // "foo/bar"
2274                //
2275                // This results in a common prefix of "foo/bar" with empty string for the
2276                // remainder since its entire prefix has already been created.
2277                // This means that we _dont_ need to create its parent dirs
2278                // either.
2279
2280                path.to_fs_path(self.working_copy_path())?
2281            } else {
2282                let adjusted_working_copy_path =
2283                    common_prefix.to_fs_path(self.working_copy_path())?;
2284
2285                // Create parent directories no matter if after.is_present(). This
2286                // ensures that the path never traverses symlinks.
2287                let Some(disk_path) =
2288                    create_parent_dirs(&adjusted_working_copy_path, adjusted_diff_file_path)?
2289                else {
2290                    changed_file_states.push((path, FileState::placeholder()));
2291                    stats.skipped_files += 1;
2292                    return Ok(());
2293                };
2294
2295                // Cache this path for the next iteration. This must occur after
2296                // `create_parent_dirs` to ensure that the path is only set when
2297                // no symlinks are encountered. Otherwise there could be
2298                // opportunity for a filesystem write-what-where attack.
2299                prev_created_path = path
2300                    .parent()
2301                    .map(RepoPath::to_owned)
2302                    .expect("diff path has no parent");
2303
2304                disk_path
2305            };
2306
2307            // If the path was present, check reserved path first and delete it.
2308            let present_file_deleted = before.is_present()
2309                && if matches!(before.as_normal(), Some(TreeValue::GitSubmodule(_))) {
2310                    remove_old_submodule_dir(&disk_path)?
2311                } else {
2312                    remove_old_file(&disk_path)?
2313                };
2314
2315            // If not, create temporary file to test the path validity.
2316            if !present_file_deleted && !can_create_new_file(&disk_path)? {
2317                if matches!(after, MaterializedTreeValue::GitSubmodule(_)) && disk_path.is_dir() {
2318                    // Failing to materialize submodule, over a directory which
2319                    // is presumably the submodule before it was added in a
2320                    // commit, is not an error.
2321                    // Falling through to the "after" state code, to set the
2322                    // correct file state.
2323                } else if matches!(before.as_normal(), Some(TreeValue::GitSubmodule(_)))
2324                    && after.is_absent()
2325                {
2326                    // Failing to delete un-tracked submodule directory is not
2327                    // an error, as the, possibly untracked, contents would
2328                    // otherwise be lost.
2329                    // Falling through to the "after" state code in case there
2330                    // are parents to be deleted.
2331                } else {
2332                    changed_file_states.push((path, FileState::placeholder()));
2333                    stats.skipped_files += 1;
2334                    return Ok(());
2335                }
2336            }
2337
2338            // We get the previous executable bit from the file states and not
2339            // the tree value because only the file states store the on-disk
2340            // executable bit.
2341            let get_prev_exec = || self.file_states().get_exec_bit(&path);
2342
2343            // TODO: Check that the file has not changed before overwriting/removing it.
2344            let file_state = match after {
2345                MaterializedTreeValue::Absent | MaterializedTreeValue::AccessDenied(_) => {
2346                    // Reset the previous path to avoid scenarios where this path is deleted,
2347                    // then on the next iteration recreation is skipped because of this
2348                    // optimization.
2349                    prev_created_path = RepoPathBuf::root();
2350
2351                    let mut parent_dir = disk_path.parent().unwrap();
2352                    loop {
2353                        if fs::remove_dir(parent_dir).is_err() {
2354                            break;
2355                        }
2356
2357                        parent_dir = parent_dir.parent().unwrap();
2358                    }
2359                    deleted_files.insert(path);
2360                    return Ok(());
2361                }
2362                MaterializedTreeValue::File(file) => {
2363                    let exec_bit =
2364                        ExecBit::new_from_repo(file.executable, self.exec_policy, get_prev_exec);
2365                    self.write_file(&disk_path, file.reader, exec_bit, true)
2366                        .await?
2367                }
2368                MaterializedTreeValue::Symlink { id: _, target } => {
2369                    if self.symlink_support {
2370                        self.write_symlink(&disk_path, target)?
2371                    } else {
2372                        // The fake symlink file shouldn't be executable.
2373                        self.write_file(&disk_path, target.as_bytes(), ExecBit(false), false)
2374                            .await?
2375                    }
2376                }
2377                MaterializedTreeValue::GitSubmodule(_) => {
2378                    eprintln!("ignoring git submodule at {path:?}");
2379                    // Git behavior: Create the submodule directory but don't
2380                    // populate/overwrite the contents.
2381                    match fs::create_dir(&disk_path) {
2382                        Ok(()) => {}
2383                        Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
2384                        Err(err) => eprintln!(
2385                            "warning: failed to create submodule directory {path:?}: {err}"
2386                        ),
2387                    }
2388                    FileState::for_gitsubmodule()
2389                }
2390                MaterializedTreeValue::Tree(_) => {
2391                    panic!("unexpected tree entry in diff at {path:?}");
2392                }
2393                MaterializedTreeValue::FileConflict(file) => {
2394                    let conflict_marker_len =
2395                        choose_materialized_conflict_marker_len(&file.contents);
2396                    let options = ConflictMaterializeOptions {
2397                        marker_style: self.conflict_marker_style,
2398                        marker_len: Some(conflict_marker_len),
2399                        merge: self.store.merge_options().clone(),
2400                    };
2401                    let exec_bit = ExecBit::new_from_repo(
2402                        file.executable.unwrap_or(false),
2403                        self.exec_policy,
2404                        get_prev_exec,
2405                    );
2406                    let contents =
2407                        materialize_merge_result_to_bytes(&file.contents, &file.labels, &options);
2408                    let mut file_state =
2409                        self.write_conflict(&disk_path, &contents, exec_bit).await?;
2410                    file_state.materialized_conflict_data = Some(MaterializedConflictData {
2411                        conflict_marker_len: conflict_marker_len.try_into().unwrap_or(u32::MAX),
2412                    });
2413                    file_state
2414                }
2415                MaterializedTreeValue::OtherConflict { id, labels } => {
2416                    // Unless all terms are regular files, we can't do much
2417                    // better than trying to describe the merge.
2418                    let contents = id.describe(&labels);
2419                    // Since this is a dummy file, it shouldn't be executable.
2420                    self.write_conflict(&disk_path, contents.as_bytes(), ExecBit(false))
2421                        .await?
2422                }
2423            };
2424            changed_file_states.push((path, file_state));
2425            Ok(())
2426        };
2427
2428        let mut diff_stream = old_tree
2429            .diff_stream_for_file_system(new_tree, matcher)
2430            .map(async |TreeDiffEntry { path, values }| match values {
2431                Ok(diff) => {
2432                    let result =
2433                        materialize_tree_value(&self.store, &path, diff.after, new_tree.labels())
2434                            .await;
2435                    (path, result.map(|value| (diff.before, value)))
2436                }
2437                Err(err) => (path, Err(err)),
2438            })
2439            .buffered(self.store.concurrency());
2440
2441        // If a conflicted file didn't change between the two trees, but the conflict
2442        // labels did, we still need to re-materialize it in the working copy. We don't
2443        // need to do this if the conflicts have different numbers of sides though since
2444        // these conflicts are considered different, so they will be materialized by
2445        // `MergedTree::diff_stream_for_file_system` already.
2446        let mut conflicts_to_rematerialize: HashMap<RepoPathBuf, MergedTreeValue> =
2447            if old_tree.tree_ids().num_sides() == new_tree.tree_ids().num_sides()
2448                && old_tree.labels() != new_tree.labels()
2449            {
2450                // TODO: it might be better to use an async stream here and merge it with the
2451                // other diff stream, but it could be difficult since the diff stream is not
2452                // sorted in the same order as the conflicts iterator.
2453                new_tree
2454                    .conflicts_matching(matcher)
2455                    .map(|(path, value)| value.map(|value| (path, value)))
2456                    .try_collect()?
2457            } else {
2458                HashMap::new()
2459            };
2460
2461        while let Some((path, data)) = diff_stream.next().await {
2462            let (before, after) = data?;
2463            conflicts_to_rematerialize.remove(&path);
2464            process_diff_entry(path, before, after).await?;
2465        }
2466
2467        if !conflicts_to_rematerialize.is_empty() {
2468            for (path, conflict) in conflicts_to_rematerialize {
2469                let materialized =
2470                    materialize_tree_value(&self.store, &path, conflict.clone(), new_tree.labels())
2471                        .await?;
2472                process_diff_entry(path, conflict, materialized).await?;
2473            }
2474
2475            // We need to re-sort the changed file states since we may have inserted a
2476            // conflicted file out of order.
2477            changed_file_states.sort_unstable_by(|(path1, _), (path2, _)| path1.cmp(path2));
2478        }
2479
2480        self.file_states
2481            .merge_in(changed_file_states, &deleted_files);
2482        Ok(stats)
2483    }
2484
2485    pub async fn reset(&mut self, new_tree: &MergedTree) -> Result<(), ResetError> {
2486        let matcher = self.sparse_matcher();
2487        let mut changed_file_states = Vec::new();
2488        let mut deleted_files = HashSet::new();
2489        let mut diff_stream = self
2490            .tree
2491            .diff_stream_for_file_system(new_tree, matcher.as_ref());
2492        while let Some(TreeDiffEntry { path, values }) = diff_stream.next().await {
2493            let after = values?.after;
2494            if after.is_absent() {
2495                deleted_files.insert(path);
2496            } else {
2497                let file_type = match after.into_resolved() {
2498                    Ok(value) => match value.unwrap() {
2499                        TreeValue::File {
2500                            id: _,
2501                            executable,
2502                            copy_id: _,
2503                        } => {
2504                            let get_prev_exec = || self.file_states().get_exec_bit(&path);
2505                            let exec_bit =
2506                                ExecBit::new_from_repo(executable, self.exec_policy, get_prev_exec);
2507                            FileType::Normal { exec_bit }
2508                        }
2509                        TreeValue::Symlink(_id) => FileType::Symlink,
2510                        TreeValue::GitSubmodule(_id) => {
2511                            eprintln!("ignoring git submodule at {path:?}");
2512                            FileType::GitSubmodule
2513                        }
2514                        TreeValue::Tree(_id) => {
2515                            panic!("unexpected tree entry in diff at {path:?}");
2516                        }
2517                    },
2518                    Err(_values) => {
2519                        // TODO: Try to set the executable bit based on the conflict
2520                        FileType::Normal {
2521                            exec_bit: ExecBit(false),
2522                        }
2523                    }
2524                };
2525                let file_state = FileState {
2526                    file_type,
2527                    mtime: MillisSinceEpoch(0),
2528                    size: 0,
2529                    materialized_conflict_data: None,
2530                };
2531                changed_file_states.push((path, file_state));
2532            }
2533        }
2534        self.file_states
2535            .merge_in(changed_file_states, &deleted_files);
2536        self.tree = new_tree.clone();
2537        Ok(())
2538    }
2539
2540    pub async fn recover(&mut self, new_tree: &MergedTree) -> Result<(), ResetError> {
2541        self.file_states.clear();
2542        self.tree = self.store.empty_merged_tree();
2543        self.reset(new_tree).await
2544    }
2545}
2546
2547fn checkout_error_for_stat_error(err: io::Error, path: &Path) -> CheckoutError {
2548    CheckoutError::Other {
2549        message: format!("Failed to stat file {}", path.display()),
2550        err: err.into(),
2551    }
2552}
2553
2554fn checkout_error_for_mtime_out_of_range(err: MtimeOutOfRange, path: &Path) -> CheckoutError {
2555    CheckoutError::Other {
2556        message: format!("Failed to process file metadata {}", path.display()),
2557        err: err.into(),
2558    }
2559}
2560
2561/// Working copy state stored in "checkout" file.
2562#[derive(Clone, Debug)]
2563struct CheckoutState {
2564    operation_id: OperationId,
2565    workspace_name: WorkspaceNameBuf,
2566}
2567
2568impl CheckoutState {
2569    fn load(state_path: &Path) -> Result<Self, WorkingCopyStateError> {
2570        let wrap_err = |err| WorkingCopyStateError {
2571            message: "Failed to read checkout state".to_owned(),
2572            err,
2573        };
2574        let buf = fs::read(state_path.join("checkout")).map_err(|err| wrap_err(err.into()))?;
2575        let proto = crate::protos::local_working_copy::Checkout::decode(&*buf)
2576            .map_err(|err| wrap_err(err.into()))?;
2577        Ok(Self {
2578            operation_id: OperationId::new(proto.operation_id),
2579            workspace_name: if proto.workspace_name.is_empty() {
2580                // For compatibility with old working copies.
2581                // TODO: Delete in mid 2022 or so
2582                WorkspaceName::DEFAULT.to_owned()
2583            } else {
2584                proto.workspace_name.into()
2585            },
2586        })
2587    }
2588
2589    #[instrument(skip_all)]
2590    fn save(&self, state_path: &Path) -> Result<(), WorkingCopyStateError> {
2591        let wrap_err = |err| WorkingCopyStateError {
2592            message: "Failed to write checkout state".to_owned(),
2593            err,
2594        };
2595        let proto = crate::protos::local_working_copy::Checkout {
2596            operation_id: self.operation_id.to_bytes(),
2597            workspace_name: (*self.workspace_name).into(),
2598        };
2599        let mut temp_file =
2600            NamedTempFile::new_in(state_path).map_err(|err| wrap_err(err.into()))?;
2601        temp_file
2602            .as_file_mut()
2603            .write_all(&proto.encode_to_vec())
2604            .map_err(|err| wrap_err(err.into()))?;
2605        // TODO: Retry if persisting fails (it will on Windows if the file happened to
2606        // be open for read).
2607        persist_temp_file(temp_file, state_path.join("checkout"))
2608            .map_err(|err| wrap_err(err.into()))?;
2609        Ok(())
2610    }
2611}
2612
2613pub struct LocalWorkingCopy {
2614    store: Arc<Store>,
2615    working_copy_path: PathBuf,
2616    state_path: PathBuf,
2617    checkout_state: CheckoutState,
2618    tree_state: OnceCell<TreeState>,
2619    tree_state_settings: TreeStateSettings,
2620}
2621
2622#[async_trait(?Send)]
2623impl WorkingCopy for LocalWorkingCopy {
2624    fn name(&self) -> &str {
2625        Self::name()
2626    }
2627
2628    fn workspace_name(&self) -> &WorkspaceName {
2629        &self.checkout_state.workspace_name
2630    }
2631
2632    fn operation_id(&self) -> &OperationId {
2633        &self.checkout_state.operation_id
2634    }
2635
2636    fn tree(&self) -> Result<&MergedTree, WorkingCopyStateError> {
2637        Ok(self.tree_state()?.current_tree())
2638    }
2639
2640    fn sparse_patterns(&self) -> Result<&[RepoPathBuf], WorkingCopyStateError> {
2641        Ok(self.tree_state()?.sparse_patterns())
2642    }
2643
2644    async fn start_mutation(&self) -> Result<Box<dyn LockedWorkingCopy>, WorkingCopyStateError> {
2645        let lock_path = self.state_path.join("working_copy.lock");
2646        let lock = FileLock::lock(lock_path).map_err(|err| WorkingCopyStateError {
2647            message: "Failed to lock working copy".to_owned(),
2648            err: err.into(),
2649        })?;
2650
2651        let wc = Self {
2652            store: self.store.clone(),
2653            working_copy_path: self.working_copy_path.clone(),
2654            state_path: self.state_path.clone(),
2655            // Re-read the state after taking the lock
2656            checkout_state: CheckoutState::load(&self.state_path)?,
2657            // Empty so we re-read the state after taking the lock
2658            // TODO: It's expensive to reload the whole tree. We should copy it from `self` if it
2659            // hasn't changed.
2660            tree_state: OnceCell::new(),
2661            tree_state_settings: self.tree_state_settings.clone(),
2662        };
2663        let old_operation_id = wc.operation_id().clone();
2664        let old_tree = wc.tree()?.clone();
2665        Ok(Box::new(LockedLocalWorkingCopy {
2666            wc,
2667            old_operation_id,
2668            old_tree,
2669            tree_state_dirty: false,
2670            new_workspace_name: None,
2671            _lock: lock,
2672        }))
2673    }
2674}
2675
2676impl LocalWorkingCopy {
2677    pub fn name() -> &'static str {
2678        "local"
2679    }
2680
2681    /// Initializes a new working copy at `working_copy_path`. The working
2682    /// copy's state will be stored in the `state_path` directory. The working
2683    /// copy will have the empty tree checked out.
2684    pub fn init(
2685        store: Arc<Store>,
2686        working_copy_path: PathBuf,
2687        state_path: PathBuf,
2688        operation_id: OperationId,
2689        workspace_name: WorkspaceNameBuf,
2690        user_settings: &UserSettings,
2691    ) -> Result<Self, WorkingCopyStateError> {
2692        let checkout_state = CheckoutState {
2693            operation_id,
2694            workspace_name,
2695        };
2696        checkout_state.save(&state_path)?;
2697        let tree_state_settings = TreeStateSettings::try_from_user_settings(user_settings)
2698            .map_err(|err| WorkingCopyStateError {
2699                message: "Failed to read the tree state settings".to_string(),
2700                err: err.into(),
2701            })?;
2702        let tree_state = TreeState::init(
2703            store.clone(),
2704            working_copy_path.clone(),
2705            state_path.clone(),
2706            &tree_state_settings,
2707        )
2708        .map_err(|err| WorkingCopyStateError {
2709            message: "Failed to initialize working copy state".to_string(),
2710            err: err.into(),
2711        })?;
2712        Ok(Self {
2713            store,
2714            working_copy_path,
2715            state_path,
2716            checkout_state,
2717            tree_state: OnceCell::with_value(tree_state),
2718            tree_state_settings,
2719        })
2720    }
2721
2722    pub fn load(
2723        store: Arc<Store>,
2724        working_copy_path: PathBuf,
2725        state_path: PathBuf,
2726        user_settings: &UserSettings,
2727    ) -> Result<Self, WorkingCopyStateError> {
2728        let checkout_state = CheckoutState::load(&state_path)?;
2729        let tree_state_settings = TreeStateSettings::try_from_user_settings(user_settings)
2730            .map_err(|err| WorkingCopyStateError {
2731                message: "Failed to read the tree state settings".to_string(),
2732                err: err.into(),
2733            })?;
2734        Ok(Self {
2735            store,
2736            working_copy_path,
2737            state_path,
2738            checkout_state,
2739            tree_state: OnceCell::new(),
2740            tree_state_settings,
2741        })
2742    }
2743
2744    pub fn state_path(&self) -> &Path {
2745        &self.state_path
2746    }
2747
2748    #[instrument(skip_all)]
2749    fn tree_state(&self) -> Result<&TreeState, WorkingCopyStateError> {
2750        self.tree_state.get_or_try_init(|| {
2751            TreeState::load(
2752                self.store.clone(),
2753                self.working_copy_path.clone(),
2754                self.state_path.clone(),
2755                &self.tree_state_settings,
2756            )
2757            .map_err(|err| WorkingCopyStateError {
2758                message: "Failed to read working copy state".to_string(),
2759                err: err.into(),
2760            })
2761        })
2762    }
2763
2764    fn tree_state_mut(&mut self) -> Result<&mut TreeState, WorkingCopyStateError> {
2765        self.tree_state()?; // ensure loaded
2766        Ok(self.tree_state.get_mut().unwrap())
2767    }
2768
2769    pub fn file_states(&self) -> Result<FileStates<'_>, WorkingCopyStateError> {
2770        Ok(self.tree_state()?.file_states())
2771    }
2772
2773    #[cfg(feature = "watchman")]
2774    pub async fn query_watchman(
2775        &self,
2776        config: &WatchmanConfig,
2777    ) -> Result<(watchman::Clock, Option<Vec<PathBuf>>), WorkingCopyStateError> {
2778        self.tree_state()?
2779            .query_watchman(config)
2780            .await
2781            .map_err(|err| WorkingCopyStateError {
2782                message: "Failed to query watchman".to_string(),
2783                err: err.into(),
2784            })
2785    }
2786
2787    #[cfg(feature = "watchman")]
2788    pub async fn is_watchman_trigger_registered(
2789        &self,
2790        config: &WatchmanConfig,
2791    ) -> Result<bool, WorkingCopyStateError> {
2792        self.tree_state()?
2793            .is_watchman_trigger_registered(config)
2794            .await
2795            .map_err(|err| WorkingCopyStateError {
2796                message: "Failed to query watchman".to_string(),
2797                err: err.into(),
2798            })
2799    }
2800}
2801
2802pub struct LocalWorkingCopyFactory {}
2803
2804impl WorkingCopyFactory for LocalWorkingCopyFactory {
2805    fn init_working_copy(
2806        &self,
2807        store: Arc<Store>,
2808        working_copy_path: PathBuf,
2809        state_path: PathBuf,
2810        operation_id: OperationId,
2811        workspace_name: WorkspaceNameBuf,
2812        settings: &UserSettings,
2813    ) -> Result<Box<dyn WorkingCopy>, WorkingCopyStateError> {
2814        Ok(Box::new(LocalWorkingCopy::init(
2815            store,
2816            working_copy_path,
2817            state_path,
2818            operation_id,
2819            workspace_name,
2820            settings,
2821        )?))
2822    }
2823
2824    fn load_working_copy(
2825        &self,
2826        store: Arc<Store>,
2827        working_copy_path: PathBuf,
2828        state_path: PathBuf,
2829        settings: &UserSettings,
2830    ) -> Result<Box<dyn WorkingCopy>, WorkingCopyStateError> {
2831        Ok(Box::new(LocalWorkingCopy::load(
2832            store,
2833            working_copy_path,
2834            state_path,
2835            settings,
2836        )?))
2837    }
2838}
2839
2840/// A working copy that's locked on disk. The lock is held until you call
2841/// `finish()` or `discard()`.
2842pub struct LockedLocalWorkingCopy {
2843    wc: LocalWorkingCopy,
2844    old_operation_id: OperationId,
2845    old_tree: MergedTree,
2846    tree_state_dirty: bool,
2847    new_workspace_name: Option<WorkspaceNameBuf>,
2848    _lock: FileLock,
2849}
2850
2851#[async_trait]
2852impl LockedWorkingCopy for LockedLocalWorkingCopy {
2853    fn old_operation_id(&self) -> &OperationId {
2854        &self.old_operation_id
2855    }
2856
2857    fn old_tree(&self) -> &MergedTree {
2858        &self.old_tree
2859    }
2860
2861    async fn snapshot(
2862        &mut self,
2863        options: &SnapshotOptions,
2864    ) -> Result<(MergedTree, SnapshotStats), SnapshotError> {
2865        let tree_state = self.wc.tree_state_mut()?;
2866        let (is_dirty, stats) = tree_state.snapshot(options).await?;
2867        self.tree_state_dirty |= is_dirty;
2868        Ok((tree_state.current_tree().clone(), stats))
2869    }
2870
2871    async fn check_out(&mut self, commit: &Commit) -> Result<CheckoutStats, CheckoutError> {
2872        // TODO: Write a "pending_checkout" file with the new TreeId so we can
2873        // continue an interrupted update if we find such a file.
2874        let new_tree = commit.tree();
2875        let tree_state = self.wc.tree_state_mut()?;
2876        if tree_state.tree.tree_ids_and_labels() != new_tree.tree_ids_and_labels() {
2877            let stats = tree_state.check_out(&new_tree)?;
2878            self.tree_state_dirty = true;
2879            Ok(stats)
2880        } else {
2881            Ok(CheckoutStats::default())
2882        }
2883    }
2884
2885    fn rename_workspace(&mut self, new_name: WorkspaceNameBuf) {
2886        self.new_workspace_name = Some(new_name);
2887    }
2888
2889    async fn reset(&mut self, commit: &Commit) -> Result<(), ResetError> {
2890        let new_tree = commit.tree();
2891        self.wc.tree_state_mut()?.reset(&new_tree).await?;
2892        self.tree_state_dirty = true;
2893        Ok(())
2894    }
2895
2896    async fn recover(&mut self, commit: &Commit) -> Result<(), ResetError> {
2897        let new_tree = commit.tree();
2898        self.wc.tree_state_mut()?.recover(&new_tree).await?;
2899        self.tree_state_dirty = true;
2900        Ok(())
2901    }
2902
2903    fn sparse_patterns(&self) -> Result<&[RepoPathBuf], WorkingCopyStateError> {
2904        self.wc.sparse_patterns()
2905    }
2906
2907    async fn set_sparse_patterns(
2908        &mut self,
2909        new_sparse_patterns: Vec<RepoPathBuf>,
2910    ) -> Result<CheckoutStats, CheckoutError> {
2911        // TODO: Write a "pending_checkout" file with new sparse patterns so we can
2912        // continue an interrupted update if we find such a file.
2913        let stats = self
2914            .wc
2915            .tree_state_mut()?
2916            .set_sparse_patterns(new_sparse_patterns)?;
2917        self.tree_state_dirty = true;
2918        Ok(stats)
2919    }
2920
2921    #[instrument(skip_all)]
2922    async fn finish(
2923        mut self: Box<Self>,
2924        operation_id: OperationId,
2925    ) -> Result<Box<dyn WorkingCopy>, WorkingCopyStateError> {
2926        assert!(
2927            self.tree_state_dirty
2928                || self.old_tree.tree_ids_and_labels() == self.wc.tree()?.tree_ids_and_labels()
2929        );
2930        if self.tree_state_dirty {
2931            self.wc
2932                .tree_state_mut()?
2933                .save()
2934                .map_err(|err| WorkingCopyStateError {
2935                    message: "Failed to write working copy state".to_string(),
2936                    err: Box::new(err),
2937                })?;
2938        }
2939        if self.old_operation_id != operation_id || self.new_workspace_name.is_some() {
2940            self.wc.checkout_state.operation_id = operation_id;
2941            if let Some(workspace_name) = self.new_workspace_name {
2942                self.wc.checkout_state.workspace_name = workspace_name;
2943            }
2944            self.wc.checkout_state.save(&self.wc.state_path)?;
2945        }
2946        // TODO: Clear the "pending_checkout" file here.
2947        Ok(Box::new(self.wc))
2948    }
2949}
2950
2951impl LockedLocalWorkingCopy {
2952    pub fn reset_watchman(&mut self) -> Result<(), SnapshotError> {
2953        self.wc.tree_state_mut()?.reset_watchman();
2954        self.tree_state_dirty = true;
2955        Ok(())
2956    }
2957}
2958
2959#[cfg(test)]
2960mod tests {
2961    use std::time::Duration;
2962
2963    use maplit::hashset;
2964
2965    use super::*;
2966
2967    fn repo_path(value: &str) -> &RepoPath {
2968        RepoPath::from_internal_string(value).unwrap()
2969    }
2970
2971    fn repo_path_component(value: &str) -> &RepoPathComponent {
2972        RepoPathComponent::new(value).unwrap()
2973    }
2974
2975    fn new_state(size: u64) -> FileState {
2976        FileState {
2977            file_type: FileType::Normal {
2978                exec_bit: ExecBit(false),
2979            },
2980            mtime: MillisSinceEpoch(0),
2981            size,
2982            materialized_conflict_data: None,
2983        }
2984    }
2985
2986    #[test]
2987    fn test_file_states_merge() {
2988        let new_static_entry = |path: &'static str, size| (repo_path(path), new_state(size));
2989        let new_owned_entry = |path: &str, size| (repo_path(path).to_owned(), new_state(size));
2990        let new_proto_entry = |path: &str, size| {
2991            file_state_entry_to_proto(repo_path(path).to_owned(), &new_state(size))
2992        };
2993        let data = vec![
2994            new_proto_entry("aa", 0),
2995            new_proto_entry("b#", 4), // '#' < '/'
2996            new_proto_entry("b/c", 1),
2997            new_proto_entry("b/d/e", 2),
2998            new_proto_entry("b/e", 3),
2999            new_proto_entry("bc", 5),
3000        ];
3001        let mut file_states = FileStatesMap::from_proto(data, false);
3002
3003        let changed_file_states = vec![
3004            new_owned_entry("aa", 10),    // change
3005            new_owned_entry("b/d/f", 11), // add
3006            new_owned_entry("b/e", 12),   // change
3007            new_owned_entry("c", 13),     // add
3008        ];
3009        let deleted_files = hashset! {
3010            repo_path("b/c").to_owned(),
3011            repo_path("b#").to_owned(),
3012        };
3013        file_states.merge_in(changed_file_states, &deleted_files);
3014        assert_eq!(
3015            file_states.all().iter().collect_vec(),
3016            vec![
3017                new_static_entry("aa", 10),
3018                new_static_entry("b/d/e", 2),
3019                new_static_entry("b/d/f", 11),
3020                new_static_entry("b/e", 12),
3021                new_static_entry("bc", 5),
3022                new_static_entry("c", 13),
3023            ],
3024        );
3025    }
3026
3027    #[test]
3028    fn test_file_states_lookup() {
3029        let new_proto_entry = |path: &str, size| {
3030            file_state_entry_to_proto(repo_path(path).to_owned(), &new_state(size))
3031        };
3032        let data = vec![
3033            new_proto_entry("aa", 0),
3034            new_proto_entry("b/c", 1),
3035            new_proto_entry("b/d/e", 2),
3036            new_proto_entry("b/e", 3),
3037            new_proto_entry("b#", 4), // '#' < '/'
3038            new_proto_entry("bc", 5),
3039        ];
3040        let file_states = FileStates::from_sorted(&data);
3041
3042        assert_eq!(
3043            file_states.prefixed(repo_path("")).paths().collect_vec(),
3044            ["aa", "b/c", "b/d/e", "b/e", "b#", "bc"].map(repo_path)
3045        );
3046        assert!(file_states.prefixed(repo_path("a")).is_empty());
3047        assert_eq!(
3048            file_states.prefixed(repo_path("aa")).paths().collect_vec(),
3049            ["aa"].map(repo_path)
3050        );
3051        assert_eq!(
3052            file_states.prefixed(repo_path("b")).paths().collect_vec(),
3053            ["b/c", "b/d/e", "b/e"].map(repo_path)
3054        );
3055        assert_eq!(
3056            file_states.prefixed(repo_path("b/d")).paths().collect_vec(),
3057            ["b/d/e"].map(repo_path)
3058        );
3059        assert_eq!(
3060            file_states.prefixed(repo_path("b#")).paths().collect_vec(),
3061            ["b#"].map(repo_path)
3062        );
3063        assert_eq!(
3064            file_states.prefixed(repo_path("bc")).paths().collect_vec(),
3065            ["bc"].map(repo_path)
3066        );
3067        assert!(file_states.prefixed(repo_path("z")).is_empty());
3068
3069        assert!(!file_states.contains_path(repo_path("a")));
3070        assert!(file_states.contains_path(repo_path("aa")));
3071        assert!(file_states.contains_path(repo_path("b/d/e")));
3072        assert!(!file_states.contains_path(repo_path("b/d")));
3073        assert!(file_states.contains_path(repo_path("b#")));
3074        assert!(file_states.contains_path(repo_path("bc")));
3075        assert!(!file_states.contains_path(repo_path("z")));
3076
3077        assert_eq!(file_states.get(repo_path("a")), None);
3078        assert_eq!(file_states.get(repo_path("aa")), Some(new_state(0)));
3079        assert_eq!(file_states.get(repo_path("b/d/e")), Some(new_state(2)));
3080        assert_eq!(file_states.get(repo_path("bc")), Some(new_state(5)));
3081        assert_eq!(file_states.get(repo_path("z")), None);
3082    }
3083
3084    #[test]
3085    fn test_file_states_lookup_at() {
3086        let new_proto_entry = |path: &str, size| {
3087            file_state_entry_to_proto(repo_path(path).to_owned(), &new_state(size))
3088        };
3089        let data = vec![
3090            new_proto_entry("b/c", 0),
3091            new_proto_entry("b/d/e", 1),
3092            new_proto_entry("b/d#", 2), // '#' < '/'
3093            new_proto_entry("b/e", 3),
3094            new_proto_entry("b#", 4), // '#' < '/'
3095        ];
3096        let file_states = FileStates::from_sorted(&data);
3097
3098        // At root
3099        assert_eq!(
3100            file_states.get_at(RepoPath::root(), repo_path_component("b")),
3101            None
3102        );
3103        assert_eq!(
3104            file_states.get_at(RepoPath::root(), repo_path_component("b#")),
3105            Some(new_state(4))
3106        );
3107
3108        // At prefixed dir
3109        let prefixed_states = file_states.prefixed_at(RepoPath::root(), repo_path_component("b"));
3110        assert_eq!(
3111            prefixed_states.paths().collect_vec(),
3112            ["b/c", "b/d/e", "b/d#", "b/e"].map(repo_path)
3113        );
3114        assert_eq!(
3115            prefixed_states.get_at(repo_path("b"), repo_path_component("c")),
3116            Some(new_state(0))
3117        );
3118        assert_eq!(
3119            prefixed_states.get_at(repo_path("b"), repo_path_component("d")),
3120            None
3121        );
3122        assert_eq!(
3123            prefixed_states.get_at(repo_path("b"), repo_path_component("d#")),
3124            Some(new_state(2))
3125        );
3126
3127        // At nested prefixed dir
3128        let prefixed_states = prefixed_states.prefixed_at(repo_path("b"), repo_path_component("d"));
3129        assert_eq!(
3130            prefixed_states.paths().collect_vec(),
3131            ["b/d/e"].map(repo_path)
3132        );
3133        assert_eq!(
3134            prefixed_states.get_at(repo_path("b/d"), repo_path_component("e")),
3135            Some(new_state(1))
3136        );
3137        assert_eq!(
3138            prefixed_states.get_at(repo_path("b/d"), repo_path_component("#")),
3139            None
3140        );
3141
3142        // At prefixed file
3143        let prefixed_states = file_states.prefixed_at(RepoPath::root(), repo_path_component("b#"));
3144        assert_eq!(prefixed_states.paths().collect_vec(), ["b#"].map(repo_path));
3145        assert_eq!(
3146            prefixed_states.get_at(repo_path("b#"), repo_path_component("#")),
3147            None
3148        );
3149    }
3150
3151    #[test]
3152    fn test_system_time_to_millis() {
3153        let epoch = SystemTime::UNIX_EPOCH;
3154        assert_eq!(system_time_to_millis(epoch), Some(MillisSinceEpoch(0)));
3155        if let Some(time) = epoch.checked_add(Duration::from_millis(1)) {
3156            assert_eq!(system_time_to_millis(time), Some(MillisSinceEpoch(1)));
3157        }
3158        if let Some(time) = epoch.checked_sub(Duration::from_millis(1)) {
3159            assert_eq!(system_time_to_millis(time), Some(MillisSinceEpoch(-1)));
3160        }
3161        if let Some(time) = epoch.checked_add(Duration::from_millis(i64::MAX as u64)) {
3162            assert_eq!(
3163                system_time_to_millis(time),
3164                Some(MillisSinceEpoch(i64::MAX))
3165            );
3166        }
3167        if let Some(time) = epoch.checked_sub(Duration::from_millis(i64::MAX as u64)) {
3168            assert_eq!(
3169                system_time_to_millis(time),
3170                Some(MillisSinceEpoch(-i64::MAX))
3171            );
3172        }
3173        if let Some(time) = epoch.checked_sub(Duration::from_millis(i64::MAX as u64 + 1)) {
3174            // i64::MIN could be returned, but we don't care such old timestamp
3175            assert_eq!(system_time_to_millis(time), None);
3176        }
3177    }
3178}