Skip to main content

jj_lib/
local_working_copy.rs

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