// 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. Load more
// 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() }