1974 lines · 71.7 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)] use std::any::Any;use std::collections::HashSet;use std::error::Error;use std::fs::{File, Metadata, OpenOptions};use std::io::{Read, Write};use std::ops::Range;#[cfg(unix)]use std::os::unix::fs::PermissionsExt;use std::path::{Path, PathBuf};use std::sync::mpsc::{channel, Sender};use std::sync::Arc;use std::time::UNIX_EPOCH;use std::{fs, iter, mem, slice}; use futures::StreamExt;use itertools::{EitherOrBoth, Itertools};use once_cell::unsync::OnceCell;use pollster::FutureExt;use prost::Message;use rayon::iter::IntoParallelIterator;use rayon::prelude::ParallelIterator;use tempfile::NamedTempFile;use thiserror::Error;use tracing::{instrument, trace_span}; use crate::backend::{    BackendError, FileId, MergedTreeId, MillisSinceEpoch, SymlinkId, TreeId, TreeValue,};use crate::commit::Commit;use crate::conflicts::{self, materialize_tree_value, MaterializedTreeValue};use crate::file_util::{check_symlink_support, try_symlink};#[cfg(feature = "watchman")]use crate::fsmonitor::watchman;use crate::fsmonitor::FsmonitorKind;use crate::gitignore::GitIgnoreFile;use crate::lock::FileLock;use crate::matchers::{    DifferenceMatcher, EverythingMatcher, FilesMatcher, IntersectionMatcher, Matcher, PrefixMatcher,};use crate::merge::{Merge, MergeBuilder, MergedTreeValue};use crate::merged_tree::{MergedTree, MergedTreeBuilder};use crate::object_id::ObjectId;use crate::op_store::{OperationId, WorkspaceId};use crate::repo_path::{RepoPath, RepoPathBuf, RepoPathComponent};use crate::settings::HumanByteSize;use crate::store::Store;use crate::tree::Tree;use crate::working_copy::{    CheckoutError, CheckoutStats, LockedWorkingCopy, ResetError, SnapshotError, SnapshotOptions,    SnapshotProgress, WorkingCopy, WorkingCopyFactory, WorkingCopyStateError,}; #[cfg(unix)]type FileExecutableFlag = bool;#[cfg(windows)]type FileExecutableFlag = (); #[derive(Debug, PartialEq, Eq, Clone)]pub enum FileType {    Normal { executable: FileExecutableFlag },    Symlink,    GitSubmodule,} #[derive(Debug, PartialEq, Eq, Clone)]pub struct FileState {    pub file_type: FileType,    pub mtime: MillisSinceEpoch,    pub size: u64,    /* 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 {    /// Indicates that a file exists in the tree but that it needs to be    /// re-stat'ed on the next snapshot.    fn placeholder() -> Self {        #[cfg(unix)]        let executable = false;        #[cfg(windows)]        let executable = ();        FileState {            file_type: FileType::Normal { executable },            mtime: MillisSinceEpoch(0),            size: 0,        }    }     fn for_file(executable: bool, size: u64, metadata: &Metadata) -> Self {        #[cfg(windows)]        let executable = {            // Windows doesn't support executable bit.            let _ = executable;            ()        };        FileState {            file_type: FileType::Normal { executable },            mtime: mtime_from_metadata(metadata),            size,        }    }     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(),        }    }     fn for_gitsubmodule() -> Self {        FileState {            file_type: FileType::GitSubmodule,            mtime: MillisSinceEpoch(0),            size: 0,        }    }} /// 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() }    }     // TODO: skip sorting if the data is known to be sorted?    fn from_proto_unsorted(mut data: Vec<crate::protos::working_copy::FileStateEntry>) -> Self {        data.sort_unstable_by(|entry1, entry2| {            let path1 = RepoPath::from_internal_string(&entry1.path);            let path2 = RepoPath::from_internal_string(&entry2.path);            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                .iter()                .tuple_windows()                .all(|((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).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));                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])    }     /// 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)    }     fn exact_position(&self, path: &RepoPath) -> Option<usize> {        self.data            .binary_search_by(|entry| RepoPath::from_internal_string(&entry.path).cmp(path))            .ok()    }     fn prefixed_range(&self, base: &RepoPath) -> Range<usize> {        let start = self            .data            .partition_point(|entry| RepoPath::from_internal_string(&entry.path) < base);        let len = self.data[start..]            .partition_point(|entry| RepoPath::from_internal_string(&entry.path).starts_with(base));        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> {        self.data            .iter()            .map(|entry| RepoPath::from_internal_string(&entry.path))    }} 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::default(),        },        #[cfg(unix)]        crate::protos::working_copy::FileType::Executable => FileType::Normal { executable: true },        // can exist in files written by older versions of jj        #[cfg(windows)]        crate::protos::working_copy::FileType::Executable => FileType::Normal { executable: () },        crate::protos::working_copy::FileType::Symlink => FileType::Symlink,        crate::protos::working_copy::FileType::Conflict => FileType::Normal {            executable: FileExecutableFlag::default(),        },        crate::protos::working_copy::FileType::GitSubmodule => FileType::GitSubmodule,    };    FileState {        file_type,        mtime: MillisSinceEpoch(proto.mtime_millis_since_epoch),        size: proto.size,    }} 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 {        #[cfg(unix)]        FileType::Normal { executable: false } => crate::protos::working_copy::FileType::Normal,        #[cfg(unix)]        FileType::Normal { executable: true } => crate::protos::working_copy::FileType::Executable,        #[cfg(windows)]        FileType::Normal { executable: () } => 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} fn file_state_entry_from_proto(    proto: &crate::protos::working_copy::FileStateEntry,) -> (&RepoPath, FileState) {    let path = RepoPath::from_internal_string(&proto.path);    (path, file_state_from_proto(proto.state.as_ref().unwrap()))} fn file_state_entry_to_proto(    path: RepoPathBuf,    state: &FileState,) -> crate::protos::working_copy::FileStateEntry {    crate::protos::working_copy::FileStateEntry {        path: path.into_internal_string(),        state: Some(file_state_to_proto(state)),    }} fn is_file_state_entries_proto_unique_and_sorted(    data: &[crate::protos::working_copy::FileStateEntry],) -> bool {    data.iter()        .map(|entry| RepoPath::from_internal_string(&entry.path))        .tuple_windows()        .all(|(path1, path2)| path1 < path2)} fn sparse_patterns_from_proto(    proto: Option<&crate::protos::working_copy::SparsePatterns>,) -> Vec<RepoPathBuf> {    let mut sparse_patterns = vec![];    if let Some(proto_sparse_patterns) = proto {        for prefix in &proto_sparse_patterns.prefixes {            sparse_patterns.push(RepoPathBuf::from_internal_string(prefix));        }    } else {        // For compatibility with old working copies.        // TODO: Delete this is late 2022 or so.        sparse_patterns.push(RepoPathBuf::root());    }    sparse_patterns} /// Creates intermediate directories from the `working_copy_path` to the/// `repo_path` parent.////// If an intermediate directory exists and if it is a symlink, this function/// will return an error. The `working_copy_path` directory may be a symlink.////// Note that this does not prevent TOCTOU bugs caused by concurrent checkouts./// Another process may remove the directory created by this function and put a/// symlink there.fn create_parent_dirs(    working_copy_path: &Path,    repo_path: &RepoPath,) -> Result<bool, CheckoutError> {    let parent_path = repo_path.parent().expect("repo path shouldn't be root");    let mut dir_path = working_copy_path.to_owned();    for c in parent_path.components() {        dir_path.push(c.as_str());        match fs::create_dir(&dir_path) {            Ok(()) => {}            Err(_)                if dir_path                    .symlink_metadata()                    .map(|m| m.is_dir())                    .unwrap_or(false) => {}            Err(err) => {                if dir_path.is_file() {                    return Ok(true);                }                return Err(CheckoutError::Other {                    message: format!(                        "Failed to create parent directories for {}",                        repo_path.to_fs_path(working_copy_path).display(),                    ),                    err: err.into(),                });            }        }    }    Ok(false)} fn mtime_from_metadata(metadata: &Metadata) -> MillisSinceEpoch {    let time = metadata        .modified()        .expect("File mtime not supported on this platform?");    let since_epoch = time        .duration_since(UNIX_EPOCH)        .expect("mtime before unix epoch");     MillisSinceEpoch(        i64::try_from(since_epoch.as_millis())            .expect("mtime billions of years into the future or past"),    )} fn file_state(metadata: &Metadata) -> Option<FileState> {    let metadata_file_type = metadata.file_type();    let file_type = if metadata_file_type.is_dir() {        None    } else if metadata_file_type.is_symlink() {        Some(FileType::Symlink)    } else if metadata_file_type.is_file() {        #[cfg(unix)]        if metadata.permissions().mode() & 0o111 != 0 {            Some(FileType::Normal { executable: true })        } else {            Some(FileType::Normal { executable: false })        }        #[cfg(windows)]        Some(FileType::Normal { executable: () })    } else {        None    };    file_type.map(|file_type| {        let mtime = mtime_from_metadata(metadata);        let size = metadata.len();        FileState {            file_type,            mtime,            size,        }    })} struct FsmonitorMatcher {    matcher: Option<Box<dyn Matcher>>,    watchman_clock: Option<crate::protos::working_copy::WatchmanClock>,} struct DirectoryToVisit<'a> {    dir: RepoPathBuf,    disk_dir: PathBuf,    git_ignore: Arc<GitIgnoreFile>,    file_states: FileStates<'a>,} #[derive(Debug, Error)]pub enum TreeStateError {    #[error("Reading tree state from {path}")]    ReadTreeState {        path: PathBuf,        source: std::io::Error,    },    #[error("Decoding tree state from {path}")]    DecodeTreeState {        path: PathBuf,        source: prost::DecodeError,    },    #[error("Writing tree state to temporary file {path}")]    WriteTreeState {        path: PathBuf,        source: std::io::Error,    },    #[error("Persisting tree state to file {path}")]    PersistTreeState {

Lines 1–500 of 1974.