2642 lines · 96.1 KB · Rust
RawDownload
// Copyright 2020 The Jujutsu Authors//// Licensed under the Apache License, Version 2.0 (the "License");// you may not use this file except in compliance with the License.// You may obtain a copy of the License at//// https://www.apache.org/licenses/LICENSE-2.0//// Unless required by applicable law or agreed to in writing, software// distributed under the License is distributed on an "AS IS" BASIS,// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.// See the License for the specific language governing permissions and// limitations under the License. #![allow(missing_docs)]#![allow(clippy::let_unit_value)] use std::any::Any;use std::cmp::Ordering;use std::collections::HashSet;use std::error::Error;use std::fs;use std::fs::DirEntry;use std::fs::File;use std::fs::Metadata;use std::fs::OpenOptions;use std::io;use std::io::Read as _;use std::io::Write as _;use std::iter;use std::mem;use std::ops::Range;#[cfg(unix)]use std::os::unix::fs::PermissionsExt as _;use std::path::Path;use std::path::PathBuf;use std::slice;use std::sync::mpsc::channel;use std::sync::mpsc::Sender;use std::sync::Arc;use std::sync::OnceLock;use std::time::UNIX_EPOCH; use either::Either;use futures::StreamExt as _;use itertools::EitherOrBoth;use itertools::Itertools as _;use once_cell::unsync::OnceCell;use pollster::FutureExt as _;use prost::Message as _;use rayon::iter::IntoParallelIterator as _;use rayon::prelude::IndexedParallelIterator as _;use rayon::prelude::ParallelIterator as _;use tempfile::NamedTempFile;use thiserror::Error;use tokio::io::AsyncRead;use tracing::instrument;use tracing::trace_span; use crate::backend::BackendError;use crate::backend::BackendResult;use crate::backend::CopyId;use crate::backend::FileId;use crate::backend::MergedTreeId;use crate::backend::MillisSinceEpoch;use crate::backend::SymlinkId;use crate::backend::TreeId;use crate::backend::TreeValue;use crate::commit::Commit;use crate::conflicts;use crate::conflicts::choose_materialized_conflict_marker_len;use crate::conflicts::materialize_merge_result_to_bytes_with_marker_len;use crate::conflicts::materialize_tree_value;use crate::conflicts::ConflictMarkerStyle;use crate::conflicts::MaterializedTreeValue;use crate::conflicts::MIN_CONFLICT_MARKER_LEN;use crate::file_util::check_symlink_support;use crate::file_util::copy_async_to_sync;use crate::file_util::try_symlink;use crate::file_util::BlockingAsyncReader;#[cfg(feature = "watchman")]use crate::fsmonitor::watchman;use crate::fsmonitor::FsmonitorSettings;#[cfg(feature = "watchman")]use crate::fsmonitor::WatchmanConfig;use crate::gitignore::GitIgnoreFile;use crate::lock::FileLock;use crate::matchers::DifferenceMatcher;use crate::matchers::EverythingMatcher;use crate::matchers::FilesMatcher;use crate::matchers::IntersectionMatcher;use crate::matchers::Matcher;use crate::matchers::PrefixMatcher;use crate::merge::Merge;use crate::merge::MergeBuilder;use crate::merge::MergedTreeValue;use crate::merged_tree::MergedTree;use crate::merged_tree::MergedTreeBuilder;use crate::merged_tree::TreeDiffEntry;use crate::object_id::ObjectId as _;use crate::op_store::OperationId;use crate::ref_name::WorkspaceName;use crate::ref_name::WorkspaceNameBuf;use crate::repo_path::RepoPath;use crate::repo_path::RepoPathBuf;use crate::repo_path::RepoPathComponent;use crate::store::Store;use crate::tree::Tree;use crate::working_copy::CheckoutError;use crate::working_copy::CheckoutOptions;use crate::working_copy::CheckoutStats;use crate::working_copy::LockedWorkingCopy;use crate::working_copy::ResetError;use crate::working_copy::SnapshotError;use crate::working_copy::SnapshotOptions;use crate::working_copy::SnapshotProgress;use crate::working_copy::SnapshotStats;use crate::working_copy::UntrackedReason;use crate::working_copy::WorkingCopy;use crate::working_copy::WorkingCopyFactory;use crate::working_copy::WorkingCopyStateError; /// On-disk state of file executable bit.// TODO: maybe better to preserve the executable bit on all platforms, and// ignore conditionally? #3949#[derive(Clone, Copy, Debug, Eq, PartialEq)]pub struct FileExecutableFlag(#[cfg(unix)] bool); #[cfg(unix)]impl FileExecutableFlag {    pub const fn from_bool_lossy(executable: bool) -> Self {        FileExecutableFlag(executable)    }     pub fn unwrap_or_else(self, _: impl FnOnce() -> bool) -> bool {        self.0    }} // Windows doesn't support executable bit.#[cfg(windows)]impl FileExecutableFlag {    pub const fn from_bool_lossy(_executable: bool) -> Self {        FileExecutableFlag()    }     pub fn unwrap_or_else(self, f: impl FnOnce() -> bool) -> bool {        f()    }} #[derive(Debug, PartialEq, Eq, Clone)]pub enum FileType {    Normal { executable: FileExecutableFlag },    Symlink,    GitSubmodule,} #[derive(Debug, PartialEq, Eq, Clone, Copy)]pub struct MaterializedConflictData {    pub conflict_marker_len: u32,} #[derive(Debug, PartialEq, Eq, Clone)]pub struct FileState {    pub file_type: FileType,    pub mtime: MillisSinceEpoch,    pub size: u64,    pub materialized_conflict_data: Option<MaterializedConflictData>,    /* TODO: What else do we need here? Git stores a lot of fields.     * TODO: Could possibly handle case-insensitive file systems keeping an     *       Option<PathBuf> with the actual path here. */} impl FileState {    /// Check whether a file state appears clean compared to a previous file    /// state, ignoring materialized conflict data.    pub fn is_clean(&self, old_file_state: &Self) -> bool {        self.file_type == old_file_state.file_type            && self.mtime == old_file_state.mtime            && self.size == old_file_state.size    }     /// Indicates that a file exists in the tree but that it needs to be    /// re-stat'ed on the next snapshot.    fn placeholder() -> Self {        let executable = FileExecutableFlag::from_bool_lossy(false);        FileState {            file_type: FileType::Normal { executable },            mtime: MillisSinceEpoch(0),            size: 0,            materialized_conflict_data: None,        }    }     fn for_file(        executable: bool,        size: u64,        metadata: &Metadata,        materialized_conflict_data: Option<MaterializedConflictData>,    ) -> Self {        let executable = FileExecutableFlag::from_bool_lossy(executable);        FileState {            file_type: FileType::Normal { executable },            mtime: mtime_from_metadata(metadata),            size,            materialized_conflict_data,        }    }     fn for_symlink(metadata: &Metadata) -> Self {        // When using fscrypt, the reported size is not the content size. So if        // we were to record the content size here (like we do for regular files), we        // would end up thinking the file has changed every time we snapshot.        FileState {            file_type: FileType::Symlink,            mtime: mtime_from_metadata(metadata),            size: metadata.len(),            materialized_conflict_data: None,        }    }     fn for_gitsubmodule() -> Self {        FileState {            file_type: FileType::GitSubmodule,            mtime: MillisSinceEpoch(0),            size: 0,            materialized_conflict_data: None,        }    }} /// Owned map of path to file states, backed by proto data.#[derive(Clone, Debug)]struct FileStatesMap {    data: Vec<crate::protos::working_copy::FileStateEntry>,} impl FileStatesMap {    fn new() -> Self {        FileStatesMap { data: Vec::new() }    }     fn from_proto(        mut data: Vec<crate::protos::working_copy::FileStateEntry>,        is_sorted: bool,    ) -> Self {        if !is_sorted {            data.sort_unstable_by(|entry1, entry2| {                let path1 = RepoPath::from_internal_string(&entry1.path).unwrap();                let path2 = RepoPath::from_internal_string(&entry2.path).unwrap();                path1.cmp(path2)            });        }        debug_assert!(is_file_state_entries_proto_unique_and_sorted(&data));        FileStatesMap { data }    }     /// Merges changed and deleted entries into this map. The changed entries    /// must be sorted by path.    fn merge_in(        &mut self,        changed_file_states: Vec<(RepoPathBuf, FileState)>,        deleted_files: &HashSet<RepoPathBuf>,    ) {        if changed_file_states.is_empty() && deleted_files.is_empty() {            return;        }        debug_assert!(            changed_file_states.is_sorted_by(|(path1, _), (path2, _)| path1 < path2),            "changed_file_states must be sorted and have no duplicates"        );        self.data = itertools::merge_join_by(            mem::take(&mut self.data),            changed_file_states,            |old_entry, (changed_path, _)| {                RepoPath::from_internal_string(&old_entry.path)                    .unwrap()                    .cmp(changed_path)            },        )        .filter_map(|diff| match diff {            EitherOrBoth::Both(_, (path, state)) | EitherOrBoth::Right((path, state)) => {                debug_assert!(!deleted_files.contains(&path));                Some(file_state_entry_to_proto(path, &state))            }            EitherOrBoth::Left(entry) => {                let present =                    !deleted_files.contains(RepoPath::from_internal_string(&entry.path).unwrap());                present.then_some(entry)            }        })        .collect();    }     fn clear(&mut self) {        self.data.clear();    }     /// Returns read-only map containing all file states.    fn all(&self) -> FileStates<'_> {        FileStates::from_sorted(&self.data)    }} /// Read-only map of path to file states, possibly filtered by path prefix.#[derive(Clone, Copy, Debug)]pub struct FileStates<'a> {    data: &'a [crate::protos::working_copy::FileStateEntry],} impl<'a> FileStates<'a> {    fn from_sorted(data: &'a [crate::protos::working_copy::FileStateEntry]) -> Self {        debug_assert!(is_file_state_entries_proto_unique_and_sorted(data));        FileStates { data }    }     /// Returns file states under the given directory path.    pub fn prefixed(&self, base: &RepoPath) -> Self {        let range = self.prefixed_range(base);        Self::from_sorted(&self.data[range])    }     /// Faster version of `prefixed("<dir>/<base>")`. Requires that all entries    /// share the same prefix `dir`.    fn prefixed_at(&self, dir: &RepoPath, base: &RepoPathComponent) -> Self {        let range = self.prefixed_range_at(dir, base);        Self::from_sorted(&self.data[range])    }     /// Returns true if this contains no entries.    pub fn is_empty(&self) -> bool {        self.data.is_empty()    }     /// Returns true if the given `path` exists.    pub fn contains_path(&self, path: &RepoPath) -> bool {        self.exact_position(path).is_some()    }     /// Returns file state for the given `path`.    pub fn get(&self, path: &RepoPath) -> Option<FileState> {        let pos = self.exact_position(path)?;        let (_, state) = file_state_entry_from_proto(&self.data[pos]);        Some(state)    }     /// Faster version of `get("<dir>/<name>")`. Requires that all entries share    /// the same prefix `dir`.    fn get_at(&self, dir: &RepoPath, name: &RepoPathComponent) -> Option<FileState> {        let pos = self.exact_position_at(dir, name)?;        let (_, state) = file_state_entry_from_proto(&self.data[pos]);        Some(state)    }     fn exact_position(&self, path: &RepoPath) -> Option<usize> {        self.data            .binary_search_by(|entry| {                RepoPath::from_internal_string(&entry.path)                    .unwrap()                    .cmp(path)            })            .ok()    }     fn exact_position_at(&self, dir: &RepoPath, name: &RepoPathComponent) -> Option<usize> {        debug_assert!(self.paths().all(|path| path.starts_with(dir)));        let slash_len = !dir.is_root() as usize;        let prefix_len = dir.as_internal_file_string().len() + slash_len;        self.data            .binary_search_by(|entry| {                let tail = entry.path.get(prefix_len..).unwrap_or("");                match tail.split_once('/') {                    // "<name>/*" > "<name>"                    Some((pre, _)) => pre.cmp(name.as_internal_str()).then(Ordering::Greater),                    None => tail.cmp(name.as_internal_str()),                }            })            .ok()    }     fn prefixed_range(&self, base: &RepoPath) -> Range<usize> {        let start = self            .data            .partition_point(|entry| RepoPath::from_internal_string(&entry.path).unwrap() < base);        let len = self.data[start..].partition_point(|entry| {            RepoPath::from_internal_string(&entry.path)                .unwrap()                .starts_with(base)        });        start..(start + len)    }     fn prefixed_range_at(&self, dir: &RepoPath, base: &RepoPathComponent) -> Range<usize> {        debug_assert!(self.paths().all(|path| path.starts_with(dir)));        let slash_len = !dir.is_root() as usize;        let prefix_len = dir.as_internal_file_string().len() + slash_len;        let start = self.data.partition_point(|entry| {            let tail = entry.path.get(prefix_len..).unwrap_or("");            let entry_name = tail.split_once('/').map_or(tail, |(name, _)| name);            entry_name < base.as_internal_str()        });        let len = self.data[start..].partition_point(|entry| {            let tail = entry.path.get(prefix_len..).unwrap_or("");            let entry_name = tail.split_once('/').map_or(tail, |(name, _)| name);            entry_name == base.as_internal_str()        });        start..(start + len)    }     /// Iterates file state entries sorted by path.    pub fn iter(&self) -> FileStatesIter<'a> {        self.data.iter().map(file_state_entry_from_proto)    }     /// Iterates sorted file paths.    pub fn paths(&self) -> impl ExactSizeIterator<Item = &'a RepoPath> + use<'a> {        self.data            .iter()            .map(|entry| RepoPath::from_internal_string(&entry.path).unwrap())    }} type FileStatesIter<'a> = iter::Map<    slice::Iter<'a, crate::protos::working_copy::FileStateEntry>,    fn(&crate::protos::working_copy::FileStateEntry) -> (&RepoPath, FileState),>; impl<'a> IntoIterator for FileStates<'a> {    type Item = (&'a RepoPath, FileState);    type IntoIter = FileStatesIter<'a>;     fn into_iter(self) -> Self::IntoIter {        self.iter()    }} pub struct TreeState {    store: Arc<Store>,    working_copy_path: PathBuf,    state_path: PathBuf,    tree_id: MergedTreeId,    file_states: FileStatesMap,    // Currently only path prefixes    sparse_patterns: Vec<RepoPathBuf>,    own_mtime: MillisSinceEpoch,    symlink_support: bool,     /// The most recent clock value returned by Watchman. Will only be set if    /// the repo is configured to use the Watchman filesystem monitor and    /// Watchman has been queried at least once.    watchman_clock: Option<crate::protos::working_copy::WatchmanClock>,} fn file_state_from_proto(proto: &crate::protos::working_copy::FileState) -> FileState {    let file_type = match proto.file_type() {        crate::protos::working_copy::FileType::Normal => FileType::Normal {            executable: FileExecutableFlag::from_bool_lossy(false),        },        // On Windows, FileType::Executable can exist in files written by older        // versions of jj        crate::protos::working_copy::FileType::Executable => FileType::Normal {            executable: FileExecutableFlag::from_bool_lossy(true),        },        crate::protos::working_copy::FileType::Symlink => FileType::Symlink,        crate::protos::working_copy::FileType::Conflict => FileType::Normal {            executable: FileExecutableFlag::from_bool_lossy(false),        },        crate::protos::working_copy::FileType::GitSubmodule => FileType::GitSubmodule,    };    FileState {        file_type,        mtime: MillisSinceEpoch(proto.mtime_millis_since_epoch),        size: proto.size,        materialized_conflict_data: proto.materialized_conflict_data.as_ref().map(|data| {            MaterializedConflictData {                conflict_marker_len: data.conflict_marker_len,            }        }),    }} fn file_state_to_proto(file_state: &FileState) -> crate::protos::working_copy::FileState {    let mut proto = crate::protos::working_copy::FileState::default();    let file_type = match &file_state.file_type {        FileType::Normal { executable } => {            if executable.unwrap_or_else(Default::default) {                crate::protos::working_copy::FileType::Executable            } else {                crate::protos::working_copy::FileType::Normal            }        }        FileType::Symlink => crate::protos::working_copy::FileType::Symlink,        FileType::GitSubmodule => crate::protos::working_copy::FileType::GitSubmodule,    };    proto.file_type = file_type as i32;    proto.mtime_millis_since_epoch = file_state.mtime.0;    proto.size = file_state.size;    proto.materialized_conflict_data = file_state.materialized_conflict_data.map(|data| {        crate::protos::working_copy::MaterializedConflictData {

Lines 1–500 of 2642.