3178 lines · 120.4 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. #![expect(missing_docs)] use std::borrow::Cow;use std::cmp::Ordering;use std::collections::HashMap;use std::collections::HashSet;use std::error::Error;use std::ffi::OsString;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::Arc;use std::sync::OnceLock;use std::sync::mpsc::Sender;use std::sync::mpsc::channel;use std::time::SystemTime; use async_trait::async_trait;use either::Either;use futures::AsyncRead;use futures::AsyncReadExt as _;use futures::StreamExt as _;use futures::io::AllowStdIo;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 tracing::instrument;use tracing::trace_span; use crate::backend::BackendError;use crate::backend::CopyId;use crate::backend::FileId;use crate::backend::MillisSinceEpoch;use crate::backend::SymlinkId;use crate::backend::TreeId;use crate::backend::TreeValue;use crate::commit::Commit;use crate::config::ConfigGetError;use crate::conflict_labels::ConflictLabels;use crate::conflicts;use crate::conflicts::ConflictMarkerStyle;use crate::conflicts::ConflictMaterializeOptions;use crate::conflicts::MIN_CONFLICT_MARKER_LEN;use crate::conflicts::MaterializedTreeValue;use crate::conflicts::choose_materialized_conflict_marker_len;use crate::conflicts::materialize_merge_result_to_bytes;use crate::conflicts::materialize_tree_value;pub use crate::eol::EolConversionMode;use crate::eol::TargetEolStrategy;use crate::file_util::FileIdentity;use crate::file_util::check_symlink_support;use crate::file_util::copy_async_to_sync;use crate::file_util::persist_temp_file;use crate::file_util::symlink_file;use crate::fsmonitor::FsmonitorSettings;#[cfg(feature = "watchman")]use crate::fsmonitor::WatchmanConfig;#[cfg(feature = "watchman")]use crate::fsmonitor::watchman;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::matchers::UnionMatcher;use crate::merge::Merge;use crate::merge::MergeBuilder;use crate::merge::MergedTreeValue;use crate::merge::SameChange;use crate::merged_tree::MergedTree;use crate::merged_tree::TreeDiffEntry;use crate::merged_tree_builder::MergedTreeBuilder;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::settings::UserSettings;use crate::store::Store;use crate::working_copy::CheckoutError;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; fn symlink_target_convert_to_store(path: &Path) -> Option<Cow<'_, str>> {    let path = path.to_str()?;    if std::path::MAIN_SEPARATOR == '/' {        Some(Cow::Borrowed(path))    } else {        // When storing the symlink target on Windows, convert "\" to "/", so that the        // symlink remains valid on Unix.        //        // Note that we don't use std::path to handle the conversion, because it        // performs poorly with Windows verbatim paths like \\?\Global\C:\file.txt.        Some(Cow::Owned(path.replace(std::path::MAIN_SEPARATOR_STR, "/")))    }} fn symlink_target_convert_to_disk(path: &str) -> PathBuf {    let path = if std::path::MAIN_SEPARATOR == '/' {        Cow::Borrowed(path)    } else {        // Use the main separator to reformat the input path to avoid creating a broken        // symlink with the incorrect separator "/".        //        // See https://github.com/jj-vcs/jj/issues/6934 for the relevant bug.        Cow::Owned(path.replace('/', std::path::MAIN_SEPARATOR_STR))    };    PathBuf::from(path.as_ref())} /// How to propagate executable bit changes in file metadata to/from the repo.////// On Windows, executable bits are always ignored, but on Unix they are/// respected by default, but may be ignored by user settings or if we find/// that the filesystem of the working copy doesn't support executable bits.#[derive(Clone, Copy, Debug)]enum ExecChangePolicy {    Ignore,    #[cfg_attr(windows, expect(dead_code))]    Respect,} /// The executable bit change setting as exposed to the user.#[derive(Clone, Copy, Debug, Default, serde::Deserialize)]#[serde(rename_all = "kebab-case")]pub enum ExecChangeSetting {    Ignore,    Respect,    #[default]    Auto,} impl ExecChangePolicy {    /// Get the executable bit policy based on user settings and executable bit    /// support in the working copy's state path.    ///    /// On Unix we check whether executable bits are supported in the working    /// copy to determine respect/ignorance, but we default to respect.    #[cfg_attr(windows, expect(unused_variables))]    fn new(exec_change_setting: ExecChangeSetting, state_path: &Path) -> Self {        #[cfg(windows)]        return Self::Ignore;        #[cfg(unix)]        return match exec_change_setting {            ExecChangeSetting::Ignore => Self::Ignore,            ExecChangeSetting::Respect => Self::Respect,            ExecChangeSetting::Auto => {                match crate::file_util::check_executable_bit_support(state_path) {                    Ok(false) => Self::Ignore,                    Ok(true) => Self::Respect,                    Err(err) => {                        tracing::warn!(?err, "Error when checking for executable bit support");                        Self::Respect                    }                }            }        };    }} /// On-disk state of file executable as cached in the file states. This does/// *not* necessarily equal the `executable` field of [`TreeValue::File`]: the/// two are allowed to diverge if and only if we're ignoring executable bit/// changes.////// This will only ever be true on Windows if the repo is also being accessed/// from a Unix version of jj, such as when accessed from WSL.#[derive(Clone, Copy, Debug, Eq, PartialEq)]pub struct ExecBit(bool); impl ExecBit {    /// Get the executable bit for a tree value to write to the repo store.    ///    /// If we're ignoring the executable bit, then we fallback to the previous    /// in-repo executable bit if present.    fn for_tree_value(        self,        exec_policy: ExecChangePolicy,        prev_in_repo: impl FnOnce() -> Option<bool>,    ) -> bool {        match exec_policy {            ExecChangePolicy::Ignore => prev_in_repo().unwrap_or(false),            ExecChangePolicy::Respect => self.0,        }    }     /// Set the on-disk executable bit to be written based on the in-repo bit or    /// the previous on-disk executable bit.    ///    /// On Windows, we return `false` because when we later write files, we    /// always create them anew, and the executable bit will be `false` even if    /// shared with a Unix machine.    ///    /// `prev_on_disk` is a closure because it is somewhat expensive and is only    /// used if ignoring the executable bit on Unix.    fn new_from_repo(        in_repo: bool,        exec_policy: ExecChangePolicy,        prev_on_disk: impl FnOnce() -> Option<Self>,    ) -> Self {        match exec_policy {            _ if cfg!(windows) => Self(false),            ExecChangePolicy::Ignore => prev_on_disk().unwrap_or(Self(false)),            ExecChangePolicy::Respect => Self(in_repo),        }    }     /// Load the on-disk executable bit from file metadata.    #[cfg_attr(windows, expect(unused_variables))]    fn new_from_disk(metadata: &Metadata) -> Self {        #[cfg(unix)]        return Self(metadata.permissions().mode() & 0o111 != 0);        #[cfg(windows)]        return Self(false);    }} /// Set the executable bit of a file on-disk. This is a no-op on Windows.////// On Unix, we manually set the executable bit to the previous value on-disk./// This is necessary because we write all files by creating them new, so files/// won't preserve their permissions naturally.#[cfg_attr(windows, expect(unused_variables))]fn set_executable(exec_bit: ExecBit, disk_path: &Path) -> Result<(), io::Error> {    #[cfg(unix)]    {        let mode = if exec_bit.0 { 0o755 } else { 0o644 };        fs::set_permissions(disk_path, fs::Permissions::from_mode(mode))?;    }    Ok(())} #[derive(Debug, PartialEq, Eq, Clone)]pub enum FileType {    Normal { exec_bit: ExecBit },    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 {        Self {            file_type: FileType::Normal {                exec_bit: ExecBit(false),            },            mtime: MillisSinceEpoch(0),            size: 0,            materialized_conflict_data: None,        }    }     fn for_file(        exec_bit: ExecBit,        size: u64,        metadata: &Metadata,    ) -> Result<Self, MtimeOutOfRange> {        Ok(Self {            file_type: FileType::Normal { exec_bit },            mtime: mtime_from_metadata(metadata)?,            size,            materialized_conflict_data: None,        })    }     fn for_symlink(metadata: &Metadata) -> Result<Self, MtimeOutOfRange> {        // 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.        Ok(Self {            file_type: FileType::Symlink,            mtime: mtime_from_metadata(metadata)?,            size: metadata.len(),            materialized_conflict_data: None,        })    }     fn for_gitsubmodule() -> Self {        Self {            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::local_working_copy::FileStateEntry>,} impl FileStatesMap {    fn new() -> Self {        Self { data: Vec::new() }    }     fn from_proto(        mut data: Vec<crate::protos::local_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));        Self { 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::local_working_copy::FileStateEntry],} impl<'a> FileStates<'a> {    fn from_sorted(data: &'a [crate::protos::local_working_copy::FileStateEntry]) -> Self {        debug_assert!(is_file_state_entries_proto_unique_and_sorted(data));        Self { 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)    }     /// Returns the executable bit state if `path` is a normal file.    pub fn get_exec_bit(&self, path: &RepoPath) -> Option<ExecBit> {        match self.get(path)?.file_type {            FileType::Normal { exec_bit } => Some(exec_bit),            FileType::Symlink | FileType::GitSubmodule => None,        }    }     /// 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()    }

Lines 1–500 of 3178.