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