// 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. 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. #![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 {