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