// 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::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::Arc;use std::sync::OnceLock;use std::sync::mpsc::Sender;use std::sync::mpsc::channel;use std::time::UNIX_EPOCH; use async_trait::async_trait;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 tokio::io::AsyncReadExt as _;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::config::ConfigGetError;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::BlockingAsyncReader;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::try_symlink;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::merge::Merge;use crate::merge::MergeBuilder;use crate::merge::MergedTreeValue;use crate::merge::SameChange;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::settings::UserSettings;use crate::store::Store;use crate::tree::Tree;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; /// 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 { Self(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 { Self() } 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); Self { file_type: FileType::Normal { executable }, mtime: MillisSinceEpoch(0), size: 0, materialized_conflict_data: None, } } fn for_file(executable: bool, size: u64, metadata: &Metadata) -> Self { let executable = FileExecutableFlag::from_bool_lossy(executable); Self { file_type: FileType::Normal { executable }, mtime: mtime_from_metadata(metadata), size, materialized_conflict_data: None, } } 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. 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) } /// 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::local_working_copy::FileStateEntry>, fn(&crate::protos::local_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() }} fn file_state_from_proto(proto: &crate::protos::local_working_copy::FileState) -> FileState { let file_type = match proto.file_type() { crate::protos::local_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::local_working_copy::FileType::Executable => FileType::Normal { executable: FileExecutableFlag::from_bool_lossy(true), }, crate::protos::local_working_copy::FileType::Symlink => FileType::Symlink, crate::protos::local_working_copy::FileType::Conflict => FileType::Normal { executable: FileExecutableFlag::from_bool_lossy(false), }, crate::protos::local_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::local_working_copy::FileState { let mut proto = crate::protos::local_working_copy::FileState::default(); let file_type = match &file_state.file_type { FileType::Normal { executable } => { if executable.unwrap_or_else(Default::default) { crate::protos::local_working_copy::FileType::Executable } else { crate::protos::local_working_copy::FileType::Normal } } FileType::Symlink => crate::protos::local_working_copy::FileType::Symlink, FileType::GitSubmodule => crate::protos::local_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::local_working_copy::MaterializedConflictData { conflict_marker_len: data.conflict_marker_len, } }); proto} fn file_state_entry_from_proto( proto: &crate::protos::local_working_copy::FileStateEntry,) -> (&RepoPath, FileState) { let path = RepoPath::from_internal_string(&proto.path).unwrap(); (path, file_state_from_proto(proto.state.as_ref().unwrap()))} fn file_state_entry_to_proto( path: RepoPathBuf, state: &FileState,Lines 1–500 of 2710. 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::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::Arc; use std::sync::OnceLock; use std::sync::mpsc::Sender; use std::sync::mpsc::channel; use std::time::UNIX_EPOCH; use async_trait::async_trait; 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 tokio::io::AsyncReadExt as _; 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::config::ConfigGetError; 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::BlockingAsyncReader; 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::try_symlink; 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::merge::Merge; use crate::merge::MergeBuilder; use crate::merge::MergedTreeValue; use crate::merge::SameChange; 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::settings::UserSettings; use crate::store::Store; use crate::tree::Tree; 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; /// 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 { Self(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 { Self() } 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); Self { file_type: FileType::Normal { executable }, mtime: MillisSinceEpoch(0), size: 0, materialized_conflict_data: None, } } fn for_file(executable: bool, size: u64, metadata: &Metadata) -> Self { let executable = FileExecutableFlag::from_bool_lossy(executable); Self { file_type: FileType::Normal { executable }, mtime: mtime_from_metadata(metadata), size, materialized_conflict_data: None, } } 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. 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) } /// 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::local_working_copy::FileStateEntry>, fn(&crate::protos::local_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() } } fn file_state_from_proto(proto: &crate::protos::local_working_copy::FileState) -> FileState { let file_type = match proto.file_type() { crate::protos::local_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::local_working_copy::FileType::Executable => FileType::Normal { executable: FileExecutableFlag::from_bool_lossy(true), }, crate::protos::local_working_copy::FileType::Symlink => FileType::Symlink, crate::protos::local_working_copy::FileType::Conflict => FileType::Normal { executable: FileExecutableFlag::from_bool_lossy(false), }, crate::protos::local_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::local_working_copy::FileState { let mut proto = crate::protos::local_working_copy::FileState::default(); let file_type = match &file_state.file_type { FileType::Normal { executable } => { if executable.unwrap_or_else(Default::default) { crate::protos::local_working_copy::FileType::Executable } else { crate::protos::local_working_copy::FileType::Normal } } FileType::Symlink => crate::protos::local_working_copy::FileType::Symlink, FileType::GitSubmodule => crate::protos::local_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::local_working_copy::MaterializedConflictData { conflict_marker_len: data.conflict_marker_len, } }); proto } fn file_state_entry_from_proto( proto: &crate::protos::local_working_copy::FileStateEntry, ) -> (&RepoPath, FileState) { let path = RepoPath::from_internal_string(&proto.path).unwrap(); (path, file_state_from_proto(proto.state.as_ref().unwrap())) } fn file_state_entry_to_proto( path: RepoPathBuf, state: &FileState,