dirstate_map.rs
1929 lines
| 65.2 KiB
| application/rls-services+xml
|
RustLexer
|
r47872 | use bytes_cast::BytesCast; | ||
|
r47888 | use micro_timer::timed; | ||
|
r47895 | use std::borrow::Cow; | ||
|
r47865 | use std::path::PathBuf; | ||
|
r48058 | use super::on_disk; | ||
|
r48126 | use super::on_disk::DirstateV2ParseError; | ||
|
r48883 | use super::owning::OwningDirstateMap; | ||
|
r47867 | use super::path_with_basename::WithBasename; | ||
|
r47872 | use crate::dirstate::parsers::pack_entry; | ||
use crate::dirstate::parsers::packed_entry_size; | ||||
|
r47868 | use crate::dirstate::parsers::parse_dirstate_entries; | ||
|
r48882 | use crate::dirstate::CopyMapIter; | ||
|
r49992 | use crate::dirstate::DirstateV2Data; | ||
use crate::dirstate::ParentFileData; | ||||
|
r48882 | use crate::dirstate::StateMapIter; | ||
|
r49007 | use crate::dirstate::TruncatedTimestamp; | ||
|
r47865 | use crate::matchers::Matcher; | ||
use crate::utils::hg_path::{HgPath, HgPathBuf}; | ||||
use crate::DirstateEntry; | ||||
use crate::DirstateError; | ||||
|
r49995 | use crate::DirstateMapError; | ||
|
r47865 | use crate::DirstateParents; | ||
use crate::DirstateStatus; | ||||
|
r49805 | use crate::FastHashbrownMap as FastHashMap; | ||
|
r47865 | use crate::PatternFileWarning; | ||
use crate::StatusError; | ||||
use crate::StatusOptions; | ||||
|
r48481 | /// Append to an existing data file if the amount of unreachable data (not used | ||
/// anymore) is less than this fraction of the total amount of existing data. | ||||
const ACCEPTABLE_UNREACHABLE_BYTES_RATIO: f32 = 0.5; | ||||
|
r50232 | #[derive(Debug, PartialEq, Eq)] | ||
/// Version of the on-disk format | ||||
pub enum DirstateVersion { | ||||
V1, | ||||
V2, | ||||
} | ||||
r51116 | #[derive(Debug, PartialEq, Eq)] | |||
pub enum DirstateMapWriteMode { | ||||
Auto, | ||||
ForceNewDataFile, | ||||
|
r51117 | ForceAppend, | ||
r51116 | } | |||
|
r50016 | #[derive(Debug)] | ||
|
r47893 | pub struct DirstateMap<'on_disk> { | ||
/// Contents of the `.hg/dirstate` file | ||||
|
r48058 | pub(super) on_disk: &'on_disk [u8], | ||
|
r47893 | |||
|
r47895 | pub(super) root: ChildNodes<'on_disk>, | ||
|
r47873 | |||
/// Number of nodes anywhere in the tree that have `.entry.is_some()`. | ||||
|
r48058 | pub(super) nodes_with_entry_count: u32, | ||
|
r47873 | |||
/// Number of nodes anywhere in the tree that have | ||||
/// `.copy_source.is_some()`. | ||||
|
r48058 | pub(super) nodes_with_copy_source_count: u32, | ||
|
r48202 | |||
/// See on_disk::Header | ||||
pub(super) ignore_patterns_hash: on_disk::IgnorePatternsHash, | ||||
|
r48481 | |||
/// How many bytes of `on_disk` are not used anymore | ||||
pub(super) unreachable_bytes: u32, | ||||
|
r50037 | |||
/// Size of the data used to first load this `DirstateMap`. Used in case | ||||
/// we need to write some new metadata, but no new data on disk. | ||||
pub(super) old_data_size: usize, | ||||
|
r50232 | |||
pub(super) dirstate_version: DirstateVersion, | ||||
|
r51117 | |||
/// Controlled by config option `devel.dirstate.v2.data_update_mode` | ||||
pub(super) write_mode: DirstateMapWriteMode, | ||||
|
r47867 | } | ||
/// Using a plain `HgPathBuf` of the full path from the repository root as a | ||||
/// map key would also work: all paths in a given map have the same parent | ||||
/// path, so comparing full paths gives the same result as comparing base | ||||
|
r48057 | /// names. However `HashMap` would waste time always re-hashing the same | ||
|
r47867 | /// string prefix. | ||
|
r48057 | pub(super) type NodeKey<'on_disk> = WithBasename<Cow<'on_disk, HgPath>>; | ||
|
r48124 | |||
|
r48136 | /// Similar to `&'tree Cow<'on_disk, HgPath>`, but can also be returned | ||
/// for on-disk nodes that don’t actually have a `Cow` to borrow. | ||||
|
r50016 | #[derive(Debug)] | ||
|
r48136 | pub(super) enum BorrowedPath<'tree, 'on_disk> { | ||
InMemory(&'tree HgPathBuf), | ||||
OnDisk(&'on_disk HgPath), | ||||
} | ||||
|
r50016 | #[derive(Debug)] | ||
|
r48124 | pub(super) enum ChildNodes<'on_disk> { | ||
InMemory(FastHashMap<NodeKey<'on_disk>, Node<'on_disk>>), | ||||
|
r48128 | OnDisk(&'on_disk [on_disk::Node]), | ||
|
r48124 | } | ||
|
r50016 | #[derive(Debug)] | ||
|
r48124 | pub(super) enum ChildNodesRef<'tree, 'on_disk> { | ||
InMemory(&'tree FastHashMap<NodeKey<'on_disk>, Node<'on_disk>>), | ||||
|
r48128 | OnDisk(&'on_disk [on_disk::Node]), | ||
|
r48124 | } | ||
|
r50016 | #[derive(Debug)] | ||
|
r48124 | pub(super) enum NodeRef<'tree, 'on_disk> { | ||
InMemory(&'tree NodeKey<'on_disk>, &'tree Node<'on_disk>), | ||||
|
r48128 | OnDisk(&'on_disk on_disk::Node), | ||
|
r48124 | } | ||
|
r48136 | impl<'tree, 'on_disk> BorrowedPath<'tree, 'on_disk> { | ||
pub fn detach_from_tree(&self) -> Cow<'on_disk, HgPath> { | ||||
match *self { | ||||
BorrowedPath::InMemory(in_memory) => Cow::Owned(in_memory.clone()), | ||||
BorrowedPath::OnDisk(on_disk) => Cow::Borrowed(on_disk), | ||||
} | ||||
} | ||||
} | ||||
impl<'tree, 'on_disk> std::ops::Deref for BorrowedPath<'tree, 'on_disk> { | ||||
type Target = HgPath; | ||||
fn deref(&self) -> &HgPath { | ||||
match *self { | ||||
BorrowedPath::InMemory(in_memory) => in_memory, | ||||
BorrowedPath::OnDisk(on_disk) => on_disk, | ||||
} | ||||
} | ||||
} | ||||
|
r48124 | impl Default for ChildNodes<'_> { | ||
fn default() -> Self { | ||||
ChildNodes::InMemory(Default::default()) | ||||
} | ||||
} | ||||
impl<'on_disk> ChildNodes<'on_disk> { | ||||
pub(super) fn as_ref<'tree>( | ||||
&'tree self, | ||||
) -> ChildNodesRef<'tree, 'on_disk> { | ||||
match self { | ||||
ChildNodes::InMemory(nodes) => ChildNodesRef::InMemory(nodes), | ||||
|
r48128 | ChildNodes::OnDisk(nodes) => ChildNodesRef::OnDisk(nodes), | ||
|
r48124 | } | ||
} | ||||
pub(super) fn is_empty(&self) -> bool { | ||||
match self { | ||||
ChildNodes::InMemory(nodes) => nodes.is_empty(), | ||||
|
r48128 | ChildNodes::OnDisk(nodes) => nodes.is_empty(), | ||
|
r48124 | } | ||
} | ||||
|
r48481 | fn make_mut( | ||
|
r48124 | &mut self, | ||
|
r48128 | on_disk: &'on_disk [u8], | ||
|
r48481 | unreachable_bytes: &mut u32, | ||
|
r48126 | ) -> Result< | ||
&mut FastHashMap<NodeKey<'on_disk>, Node<'on_disk>>, | ||||
DirstateV2ParseError, | ||||
> { | ||||
|
r48124 | match self { | ||
|
r48126 | ChildNodes::InMemory(nodes) => Ok(nodes), | ||
|
r48128 | ChildNodes::OnDisk(nodes) => { | ||
|
r48481 | *unreachable_bytes += | ||
std::mem::size_of_val::<[on_disk::Node]>(nodes) as u32; | ||||
|
r48128 | let nodes = nodes | ||
.iter() | ||||
.map(|node| { | ||||
Ok(( | ||||
node.path(on_disk)?, | ||||
node.to_in_memory_node(on_disk)?, | ||||
)) | ||||
}) | ||||
.collect::<Result<_, _>>()?; | ||||
*self = ChildNodes::InMemory(nodes); | ||||
match self { | ||||
ChildNodes::InMemory(nodes) => Ok(nodes), | ||||
ChildNodes::OnDisk(_) => unreachable!(), | ||||
} | ||||
} | ||||
|
r48124 | } | ||
} | ||||
} | ||||
impl<'tree, 'on_disk> ChildNodesRef<'tree, 'on_disk> { | ||||
pub(super) fn get( | ||||
&self, | ||||
base_name: &HgPath, | ||||
|
r48128 | on_disk: &'on_disk [u8], | ||
|
r48126 | ) -> Result<Option<NodeRef<'tree, 'on_disk>>, DirstateV2ParseError> { | ||
|
r48124 | match self { | ||
|
r48126 | ChildNodesRef::InMemory(nodes) => Ok(nodes | ||
|
r48124 | .get_key_value(base_name) | ||
|
r48126 | .map(|(k, v)| NodeRef::InMemory(k, v))), | ||
|
r48128 | ChildNodesRef::OnDisk(nodes) => { | ||
let mut parse_result = Ok(()); | ||||
let search_result = nodes.binary_search_by(|node| { | ||||
match node.base_name(on_disk) { | ||||
Ok(node_base_name) => node_base_name.cmp(base_name), | ||||
Err(e) => { | ||||
parse_result = Err(e); | ||||
// Dummy comparison result, `search_result` won’t | ||||
// be used since `parse_result` is an error | ||||
std::cmp::Ordering::Equal | ||||
} | ||||
} | ||||
}); | ||||
parse_result.map(|()| { | ||||
search_result.ok().map(|i| NodeRef::OnDisk(&nodes[i])) | ||||
}) | ||||
} | ||||
|
r48124 | } | ||
} | ||||
/// Iterate in undefined order | ||||
pub(super) fn iter( | ||||
&self, | ||||
) -> impl Iterator<Item = NodeRef<'tree, 'on_disk>> { | ||||
match self { | ||||
|
r48128 | ChildNodesRef::InMemory(nodes) => itertools::Either::Left( | ||
nodes.iter().map(|(k, v)| NodeRef::InMemory(k, v)), | ||||
), | ||||
ChildNodesRef::OnDisk(nodes) => { | ||||
itertools::Either::Right(nodes.iter().map(NodeRef::OnDisk)) | ||||
|
r48124 | } | ||
} | ||||
} | ||||
/// Iterate in parallel in undefined order | ||||
pub(super) fn par_iter( | ||||
&self, | ||||
) -> impl rayon::iter::ParallelIterator<Item = NodeRef<'tree, 'on_disk>> | ||||
{ | ||||
use rayon::prelude::*; | ||||
match self { | ||||
|
r48128 | ChildNodesRef::InMemory(nodes) => rayon::iter::Either::Left( | ||
nodes.par_iter().map(|(k, v)| NodeRef::InMemory(k, v)), | ||||
), | ||||
ChildNodesRef::OnDisk(nodes) => rayon::iter::Either::Right( | ||||
nodes.par_iter().map(NodeRef::OnDisk), | ||||
), | ||||
|
r48124 | } | ||
} | ||||
pub(super) fn sorted(&self) -> Vec<NodeRef<'tree, 'on_disk>> { | ||||
match self { | ||||
ChildNodesRef::InMemory(nodes) => { | ||||
let mut vec: Vec<_> = nodes | ||||
.iter() | ||||
.map(|(k, v)| NodeRef::InMemory(k, v)) | ||||
.collect(); | ||||
|
r48126 | fn sort_key<'a>(node: &'a NodeRef) -> &'a HgPath { | ||
match node { | ||||
NodeRef::InMemory(path, _node) => path.base_name(), | ||||
|
r48128 | NodeRef::OnDisk(_) => unreachable!(), | ||
|
r48126 | } | ||
} | ||||
|
r48124 | // `sort_unstable_by_key` doesn’t allow keys borrowing from the | ||
// value: https://github.com/rust-lang/rust/issues/34162 | ||||
|
r48126 | vec.sort_unstable_by(|a, b| sort_key(a).cmp(sort_key(b))); | ||
|
r48124 | vec | ||
} | ||||
|
r48128 | ChildNodesRef::OnDisk(nodes) => { | ||
// Nodes on disk are already sorted | ||||
nodes.iter().map(NodeRef::OnDisk).collect() | ||||
} | ||||
|
r48124 | } | ||
} | ||||
} | ||||
impl<'tree, 'on_disk> NodeRef<'tree, 'on_disk> { | ||||
|
r48126 | pub(super) fn full_path( | ||
&self, | ||||
|
r48128 | on_disk: &'on_disk [u8], | ||
|
r48126 | ) -> Result<&'tree HgPath, DirstateV2ParseError> { | ||
|
r48124 | match self { | ||
|
r48126 | NodeRef::InMemory(path, _node) => Ok(path.full_path()), | ||
|
r48128 | NodeRef::OnDisk(node) => node.full_path(on_disk), | ||
|
r48124 | } | ||
} | ||||
|
r48136 | /// Returns a `BorrowedPath`, which can be turned into a `Cow<'on_disk, | ||
/// HgPath>` detached from `'tree` | ||||
pub(super) fn full_path_borrowed( | ||||
|
r48126 | &self, | ||
|
r48128 | on_disk: &'on_disk [u8], | ||
|
r48136 | ) -> Result<BorrowedPath<'tree, 'on_disk>, DirstateV2ParseError> { | ||
|
r48124 | match self { | ||
|
r48136 | NodeRef::InMemory(path, _node) => match path.full_path() { | ||
Cow::Borrowed(on_disk) => Ok(BorrowedPath::OnDisk(on_disk)), | ||||
Cow::Owned(in_memory) => Ok(BorrowedPath::InMemory(in_memory)), | ||||
}, | ||||
|
r48128 | NodeRef::OnDisk(node) => { | ||
|
r48136 | Ok(BorrowedPath::OnDisk(node.full_path(on_disk)?)) | ||
|
r48128 | } | ||
|
r48126 | } | ||
} | ||||
pub(super) fn base_name( | ||||
&self, | ||||
|
r48128 | on_disk: &'on_disk [u8], | ||
|
r48126 | ) -> Result<&'tree HgPath, DirstateV2ParseError> { | ||
match self { | ||||
NodeRef::InMemory(path, _node) => Ok(path.base_name()), | ||||
|
r48128 | NodeRef::OnDisk(node) => node.base_name(on_disk), | ||
|
r48124 | } | ||
} | ||||
|
r48126 | pub(super) fn children( | ||
&self, | ||||
|
r48128 | on_disk: &'on_disk [u8], | ||
|
r48126 | ) -> Result<ChildNodesRef<'tree, 'on_disk>, DirstateV2ParseError> { | ||
|
r48124 | match self { | ||
|
r48126 | NodeRef::InMemory(_path, node) => Ok(node.children.as_ref()), | ||
|
r48128 | NodeRef::OnDisk(node) => { | ||
Ok(ChildNodesRef::OnDisk(node.children(on_disk)?)) | ||||
} | ||||
|
r48124 | } | ||
} | ||||
|
r48126 | pub(super) fn has_copy_source(&self) -> bool { | ||
|
r48124 | match self { | ||
|
r48126 | NodeRef::InMemory(_path, node) => node.copy_source.is_some(), | ||
|
r48128 | NodeRef::OnDisk(node) => node.has_copy_source(), | ||
|
r48124 | } | ||
} | ||||
|
r48126 | pub(super) fn copy_source( | ||
&self, | ||||
|
r48128 | on_disk: &'on_disk [u8], | ||
|
r48126 | ) -> Result<Option<&'tree HgPath>, DirstateV2ParseError> { | ||
|
r48124 | match self { | ||
NodeRef::InMemory(_path, node) => { | ||||
|
r48126 | Ok(node.copy_source.as_ref().map(|s| &**s)) | ||
|
r48124 | } | ||
|
r48128 | NodeRef::OnDisk(node) => node.copy_source(on_disk), | ||
|
r48124 | } | ||
} | ||||
|
r49285 | /// Returns a `BorrowedPath`, which can be turned into a `Cow<'on_disk, | ||
/// HgPath>` detached from `'tree` | ||||
pub(super) fn copy_source_borrowed( | ||||
&self, | ||||
on_disk: &'on_disk [u8], | ||||
) -> Result<Option<BorrowedPath<'tree, 'on_disk>>, DirstateV2ParseError> | ||||
{ | ||||
Ok(match self { | ||||
NodeRef::InMemory(_path, node) => { | ||||
node.copy_source.as_ref().map(|source| match source { | ||||
Cow::Borrowed(on_disk) => BorrowedPath::OnDisk(on_disk), | ||||
Cow::Owned(in_memory) => BorrowedPath::InMemory(in_memory), | ||||
}) | ||||
} | ||||
NodeRef::OnDisk(node) => node | ||||
.copy_source(on_disk)? | ||||
.map(|source| BorrowedPath::OnDisk(source)), | ||||
}) | ||||
} | ||||
|
r48124 | |||
|
r48126 | pub(super) fn entry( | ||
&self, | ||||
) -> Result<Option<DirstateEntry>, DirstateV2ParseError> { | ||||
|
r48124 | match self { | ||
|
r48137 | NodeRef::InMemory(_path, node) => { | ||
Ok(node.data.as_entry().copied()) | ||||
} | ||||
|
r48128 | NodeRef::OnDisk(node) => node.entry(), | ||
|
r48124 | } | ||
} | ||||
|
r48126 | |||
|
r48138 | pub(super) fn cached_directory_mtime( | ||
&self, | ||||
|
r49007 | ) -> Result<Option<TruncatedTimestamp>, DirstateV2ParseError> { | ||
|
r48138 | match self { | ||
|
r49007 | NodeRef::InMemory(_path, node) => Ok(match node.data { | ||
|
r48138 | NodeData::CachedDirectory { mtime } => Some(mtime), | ||
_ => None, | ||||
|
r49007 | }), | ||
|
r48138 | NodeRef::OnDisk(node) => node.cached_directory_mtime(), | ||
} | ||||
} | ||||
|
r48272 | pub(super) fn descendants_with_entry_count(&self) -> u32 { | ||
match self { | ||||
NodeRef::InMemory(_path, node) => { | ||||
node.descendants_with_entry_count | ||||
} | ||||
NodeRef::OnDisk(node) => node.descendants_with_entry_count.get(), | ||||
} | ||||
} | ||||
|
r48124 | pub(super) fn tracked_descendants_count(&self) -> u32 { | ||
match self { | ||||
NodeRef::InMemory(_path, node) => node.tracked_descendants_count, | ||||
|
r48128 | NodeRef::OnDisk(node) => node.tracked_descendants_count.get(), | ||
|
r48124 | } | ||
} | ||||
} | ||||
|
r47867 | |||
|
r47876 | /// Represents a file or a directory | ||
|
r50016 | #[derive(Default, Debug)] | ||
|
r47895 | pub(super) struct Node<'on_disk> { | ||
|
r48137 | pub(super) data: NodeData, | ||
|
r47876 | |||
|
r47895 | pub(super) copy_source: Option<Cow<'on_disk, HgPath>>, | ||
|
r47876 | |||
|
r47895 | pub(super) children: ChildNodes<'on_disk>, | ||
|
r47876 | |||
|
r48272 | /// How many (non-inclusive) descendants of this node have an entry. | ||
pub(super) descendants_with_entry_count: u32, | ||||
/// How many (non-inclusive) descendants of this node have an entry whose | ||||
/// state is "tracked". | ||||
|
r48058 | pub(super) tracked_descendants_count: u32, | ||
|
r47876 | } | ||
|
r50016 | #[derive(Debug)] | ||
|
r48137 | pub(super) enum NodeData { | ||
Entry(DirstateEntry), | ||||
|
r49007 | CachedDirectory { mtime: TruncatedTimestamp }, | ||
|
r48137 | None, | ||
} | ||||
impl Default for NodeData { | ||||
fn default() -> Self { | ||||
NodeData::None | ||||
} | ||||
} | ||||
impl NodeData { | ||||
fn has_entry(&self) -> bool { | ||||
match self { | ||||
NodeData::Entry(_) => true, | ||||
_ => false, | ||||
} | ||||
} | ||||
fn as_entry(&self) -> Option<&DirstateEntry> { | ||||
match self { | ||||
NodeData::Entry(entry) => Some(entry), | ||||
_ => None, | ||||
} | ||||
} | ||||
|
r49997 | |||
fn as_entry_mut(&mut self) -> Option<&mut DirstateEntry> { | ||||
match self { | ||||
NodeData::Entry(entry) => Some(entry), | ||||
_ => None, | ||||
} | ||||
} | ||||
|
r48137 | } | ||
|
r47893 | impl<'on_disk> DirstateMap<'on_disk> { | ||
|
r48058 | pub(super) fn empty(on_disk: &'on_disk [u8]) -> Self { | ||
Self { | ||||
on_disk, | ||||
root: ChildNodes::default(), | ||||
nodes_with_entry_count: 0, | ||||
nodes_with_copy_source_count: 0, | ||||
|
r48202 | ignore_patterns_hash: [0; on_disk::IGNORE_PATTERNS_HASH_LEN], | ||
|
r48481 | unreachable_bytes: 0, | ||
|
r50037 | old_data_size: 0, | ||
|
r50232 | dirstate_version: DirstateVersion::V1, | ||
|
r51117 | write_mode: DirstateMapWriteMode::Auto, | ||
|
r48058 | } | ||
} | ||||
|
r48055 | #[timed] | ||
|
r48475 | pub fn new_v2( | ||
on_disk: &'on_disk [u8], | ||||
data_size: usize, | ||||
|
r48482 | metadata: &[u8], | ||
|
r48475 | ) -> Result<Self, DirstateError> { | ||
if let Some(data) = on_disk.get(..data_size) { | ||||
|
r48482 | Ok(on_disk::read(data, metadata)?) | ||
|
r48475 | } else { | ||
|
r50268 | Err(DirstateV2ParseError::new("not enough bytes on disk").into()) | ||
|
r48475 | } | ||
|
r48055 | } | ||
#[timed] | ||||
pub fn new_v1( | ||||
|
r47893 | on_disk: &'on_disk [u8], | ||
) -> Result<(Self, Option<DirstateParents>), DirstateError> { | ||||
|
r48058 | let mut map = Self::empty(on_disk); | ||
|
r48055 | if map.on_disk.is_empty() { | ||
return Ok((map, None)); | ||||
|
r47867 | } | ||
|
r47893 | |||
let parents = parse_dirstate_entries( | ||||
|
r48055 | map.on_disk, | ||
|
r47893 | |path, entry, copy_source| { | ||
|
r50027 | let tracked = entry.tracked(); | ||
|
r50021 | let node = Self::get_or_insert_node_inner( | ||
|
r48127 | map.on_disk, | ||
|
r48481 | &mut map.unreachable_bytes, | ||
|
r48055 | &mut map.root, | ||
|
r47893 | path, | ||
|
r47896 | WithBasename::to_cow_borrowed, | ||
|
r47893 | |ancestor| { | ||
if tracked { | ||||
ancestor.tracked_descendants_count += 1 | ||||
} | ||||
|
r48272 | ancestor.descendants_with_entry_count += 1 | ||
|
r47893 | }, | ||
|
r48126 | )?; | ||
|
r47893 | assert!( | ||
|
r48137 | !node.data.has_entry(), | ||
|
r47893 | "duplicate dirstate entry in read" | ||
); | ||||
assert!( | ||||
node.copy_source.is_none(), | ||||
"duplicate dirstate entry in read" | ||||
); | ||||
|
r48137 | node.data = NodeData::Entry(*entry); | ||
|
r47895 | node.copy_source = copy_source.map(Cow::Borrowed); | ||
|
r48055 | map.nodes_with_entry_count += 1; | ||
|
r47893 | if copy_source.is_some() { | ||
|
r48055 | map.nodes_with_copy_source_count += 1 | ||
|
r47893 | } | ||
|
r48126 | Ok(()) | ||
|
r47893 | }, | ||
)?; | ||||
|
r48055 | let parents = Some(parents.clone()); | ||
|
r47893 | |||
|
r48055 | Ok((map, parents)) | ||
|
r47867 | } | ||
|
r48478 | /// Assuming dirstate-v2 format, returns whether the next write should | ||
/// append to the existing data file that contains `self.on_disk` (true), | ||||
/// or create a new data file from scratch (false). | ||||
pub(super) fn write_should_append(&self) -> bool { | ||||
|
r51117 | match self.write_mode { | ||
DirstateMapWriteMode::ForceAppend => true, | ||||
DirstateMapWriteMode::ForceNewDataFile => false, | ||||
DirstateMapWriteMode::Auto => { | ||||
let ratio = | ||||
self.unreachable_bytes as f32 / self.on_disk.len() as f32; | ||||
ratio < ACCEPTABLE_UNREACHABLE_BYTES_RATIO | ||||
} | ||||
} | ||||
|
r48478 | } | ||
|
r48124 | fn get_node<'tree>( | ||
&'tree self, | ||||
path: &HgPath, | ||||
|
r48126 | ) -> Result<Option<NodeRef<'tree, 'on_disk>>, DirstateV2ParseError> { | ||
|
r48124 | let mut children = self.root.as_ref(); | ||
|
r47869 | let mut components = path.components(); | ||
let mut component = | ||||
components.next().expect("expected at least one components"); | ||||
loop { | ||||
|
r48127 | if let Some(child) = children.get(component, self.on_disk)? { | ||
|
r48126 | if let Some(next_component) = components.next() { | ||
component = next_component; | ||||
|
r48127 | children = child.children(self.on_disk)?; | ||
|
r48126 | } else { | ||
return Ok(Some(child)); | ||||
} | ||||
|
r47869 | } else { | ||
|
r48126 | return Ok(None); | ||
|
r47869 | } | ||
} | ||||
} | ||||
|
r47876 | /// Returns a mutable reference to the node at `path` if it exists | ||
/// | ||||
|
r50024 | /// `each_ancestor` is a callback that is called for each ancestor node | ||
/// when descending the tree. It is used to keep the different counters | ||||
/// of the `DirstateMap` up-to-date. | ||||
fn get_node_mut<'tree>( | ||||
&'tree mut self, | ||||
path: &HgPath, | ||||
each_ancestor: impl FnMut(&mut Node), | ||||
) -> Result<Option<&'tree mut Node<'on_disk>>, DirstateV2ParseError> { | ||||
Self::get_node_mut_inner( | ||||
self.on_disk, | ||||
&mut self.unreachable_bytes, | ||||
&mut self.root, | ||||
path, | ||||
each_ancestor, | ||||
) | ||||
} | ||||
/// Lower-level version of `get_node_mut`. | ||||
/// | ||||
|
r47873 | /// This takes `root` instead of `&mut self` so that callers can mutate | ||
|
r50022 | /// other fields while the returned borrow is still valid. | ||
/// | ||||
/// `each_ancestor` is a callback that is called for each ancestor node | ||||
/// when descending the tree. It is used to keep the different counters | ||||
/// of the `DirstateMap` up-to-date. | ||||
|
r50024 | fn get_node_mut_inner<'tree>( | ||
|
r48127 | on_disk: &'on_disk [u8], | ||
|
r48481 | unreachable_bytes: &mut u32, | ||
|
r47895 | root: &'tree mut ChildNodes<'on_disk>, | ||
|
r47874 | path: &HgPath, | ||
|
r50022 | mut each_ancestor: impl FnMut(&mut Node), | ||
|
r48126 | ) -> Result<Option<&'tree mut Node<'on_disk>>, DirstateV2ParseError> { | ||
|
r47874 | let mut children = root; | ||
let mut components = path.components(); | ||||
let mut component = | ||||
components.next().expect("expected at least one components"); | ||||
loop { | ||||
|
r48481 | if let Some(child) = children | ||
.make_mut(on_disk, unreachable_bytes)? | ||||
.get_mut(component) | ||||
|
r48127 | { | ||
|
r48126 | if let Some(next_component) = components.next() { | ||
|
r50022 | each_ancestor(child); | ||
|
r48126 | component = next_component; | ||
children = &mut child.children; | ||||
} else { | ||||
return Ok(Some(child)); | ||||
} | ||||
|
r47874 | } else { | ||
|
r48126 | return Ok(None); | ||
|
r47874 | } | ||
} | ||||
} | ||||
|
r50021 | /// Get a mutable reference to the node at `path`, creating it if it does | ||
/// not exist. | ||||
/// | ||||
/// `each_ancestor` is a callback that is called for each ancestor node | ||||
/// when descending the tree. It is used to keep the different counters | ||||
/// of the `DirstateMap` up-to-date. | ||||
|
r48481 | fn get_or_insert_node<'tree, 'path>( | ||
|
r48268 | &'tree mut self, | ||
|
r50021 | path: &'path HgPath, | ||
each_ancestor: impl FnMut(&mut Node), | ||||
|
r48268 | ) -> Result<&'tree mut Node<'on_disk>, DirstateV2ParseError> { | ||
|
r50021 | Self::get_or_insert_node_inner( | ||
|
r48268 | self.on_disk, | ||
|
r48481 | &mut self.unreachable_bytes, | ||
|
r48268 | &mut self.root, | ||
path, | ||||
WithBasename::to_cow_owned, | ||||
|
r50021 | each_ancestor, | ||
|
r48268 | ) | ||
} | ||||
|
r50021 | /// Lower-level version of `get_or_insert_node_inner`, which is used when | ||
/// parsing disk data to remove allocations for new nodes. | ||||
fn get_or_insert_node_inner<'tree, 'path>( | ||||
|
r48127 | on_disk: &'on_disk [u8], | ||
|
r48481 | unreachable_bytes: &mut u32, | ||
|
r47895 | root: &'tree mut ChildNodes<'on_disk>, | ||
|
r47896 | path: &'path HgPath, | ||
to_cow: impl Fn( | ||||
WithBasename<&'path HgPath>, | ||||
) -> WithBasename<Cow<'on_disk, HgPath>>, | ||||
|
r47890 | mut each_ancestor: impl FnMut(&mut Node), | ||
|
r48126 | ) -> Result<&'tree mut Node<'on_disk>, DirstateV2ParseError> { | ||
|
r47873 | let mut child_nodes = root; | ||
|
r47867 | let mut inclusive_ancestor_paths = | ||
WithBasename::inclusive_ancestors_of(path); | ||||
let mut ancestor_path = inclusive_ancestor_paths | ||||
.next() | ||||
.expect("expected at least one inclusive ancestor"); | ||||
loop { | ||||
|
r49805 | let (_, child_node) = child_nodes | ||
|
r48481 | .make_mut(on_disk, unreachable_bytes)? | ||
|
r49805 | .raw_entry_mut() | ||
.from_key(ancestor_path.base_name()) | ||||
.or_insert_with(|| (to_cow(ancestor_path), Node::default())); | ||||
|
r47867 | if let Some(next) = inclusive_ancestor_paths.next() { | ||
|
r47890 | each_ancestor(child_node); | ||
|
r47867 | ancestor_path = next; | ||
child_nodes = &mut child_node.children; | ||||
} else { | ||||
|
r48126 | return Ok(child_node); | ||
|
r47867 | } | ||
} | ||||
|
r47865 | } | ||
|
r47870 | |||
|
r49992 | fn reset_state( | ||
&mut self, | ||||
filename: &HgPath, | ||||
old_entry_opt: Option<DirstateEntry>, | ||||
wc_tracked: bool, | ||||
p1_tracked: bool, | ||||
p2_info: bool, | ||||
has_meaningful_mtime: bool, | ||||
parent_file_data_opt: Option<ParentFileData>, | ||||
) -> Result<(), DirstateError> { | ||||
let (had_entry, was_tracked) = match old_entry_opt { | ||||
Some(old_entry) => (true, old_entry.tracked()), | ||||
None => (false, false), | ||||
}; | ||||
|
r50021 | let node = self.get_or_insert_node(filename, |ancestor| { | ||
if !had_entry { | ||||
ancestor.descendants_with_entry_count += 1; | ||||
} | ||||
if was_tracked { | ||||
if !wc_tracked { | ||||
ancestor.tracked_descendants_count = ancestor | ||||
.tracked_descendants_count | ||||
.checked_sub(1) | ||||
.expect("tracked count to be >= 0"); | ||||
|
r49992 | } | ||
|
r50021 | } else { | ||
if wc_tracked { | ||||
ancestor.tracked_descendants_count += 1; | ||||
|
r49992 | } | ||
|
r50021 | } | ||
})?; | ||||
|
r49992 | |||
let v2_data = if let Some(parent_file_data) = parent_file_data_opt { | ||||
DirstateV2Data { | ||||
wc_tracked, | ||||
p1_tracked, | ||||
p2_info, | ||||
mode_size: parent_file_data.mode_size, | ||||
mtime: if has_meaningful_mtime { | ||||
parent_file_data.mtime | ||||
} else { | ||||
None | ||||
}, | ||||
..Default::default() | ||||
} | ||||
} else { | ||||
DirstateV2Data { | ||||
wc_tracked, | ||||
p1_tracked, | ||||
p2_info, | ||||
..Default::default() | ||||
} | ||||
}; | ||||
|
r50021 | node.data = NodeData::Entry(DirstateEntry::from_v2_data(v2_data)); | ||
|
r49992 | if !had_entry { | ||
self.nodes_with_entry_count += 1; | ||||
} | ||||
Ok(()) | ||||
} | ||||
|
r49988 | fn set_tracked( | ||
&mut self, | ||||
filename: &HgPath, | ||||
old_entry_opt: Option<DirstateEntry>, | ||||
) -> Result<bool, DirstateV2ParseError> { | ||||
let was_tracked = old_entry_opt.map_or(false, |e| e.tracked()); | ||||
let had_entry = old_entry_opt.is_some(); | ||||
let tracked_count_increment = if was_tracked { 0 } else { 1 }; | ||||
let mut new = false; | ||||
|
r50021 | let node = self.get_or_insert_node(filename, |ancestor| { | ||
if !had_entry { | ||||
ancestor.descendants_with_entry_count += 1; | ||||
} | ||||
|
r49988 | |||
|
r50021 | ancestor.tracked_descendants_count += tracked_count_increment; | ||
})?; | ||||
if let Some(old_entry) = old_entry_opt { | ||||
|
r49988 | let mut e = old_entry.clone(); | ||
if e.tracked() { | ||||
// XXX | ||||
// This is probably overkill for more case, but we need this to | ||||
// fully replace the `normallookup` call with `set_tracked` | ||||
// one. Consider smoothing this in the future. | ||||
e.set_possibly_dirty(); | ||||
} else { | ||||
new = true; | ||||
e.set_tracked(); | ||||
} | ||||
|
r50021 | node.data = NodeData::Entry(e) | ||
|
r49988 | } else { | ||
|
r50021 | node.data = NodeData::Entry(DirstateEntry::new_tracked()); | ||
|
r49988 | self.nodes_with_entry_count += 1; | ||
new = true; | ||||
}; | ||||
Ok(new) | ||||
} | ||||
|
r50023 | /// Set a node as untracked in the dirstate. | ||
/// | ||||
/// It is the responsibility of the caller to remove the copy source and/or | ||||
/// the entry itself if appropriate. | ||||
/// | ||||
/// # Panics | ||||
/// | ||||
/// Panics if the node does not exist. | ||||
|
r49999 | fn set_untracked( | ||
&mut self, | ||||
filename: &HgPath, | ||||
old_entry: DirstateEntry, | ||||
) -> Result<(), DirstateV2ParseError> { | ||||
|
r50024 | let node = self | ||
.get_node_mut(filename, |ancestor| { | ||||
|
r50023 | ancestor.tracked_descendants_count = ancestor | ||
.tracked_descendants_count | ||||
.checked_sub(1) | ||||
.expect("tracked_descendants_count should be >= 0"); | ||||
|
r50024 | })? | ||
.expect("node should exist"); | ||||
|
r49999 | let mut new_entry = old_entry.clone(); | ||
new_entry.set_untracked(); | ||||
node.data = NodeData::Entry(new_entry); | ||||
Ok(()) | ||||
} | ||||
|
r50023 | /// Set a node as clean in the dirstate. | ||
/// | ||||
/// It is the responsibility of the caller to remove the copy source. | ||||
/// | ||||
/// # Panics | ||||
/// | ||||
/// Panics if the node does not exist. | ||||
|
r49995 | fn set_clean( | ||
&mut self, | ||||
filename: &HgPath, | ||||
old_entry: DirstateEntry, | ||||
mode: u32, | ||||
size: u32, | ||||
mtime: TruncatedTimestamp, | ||||
) -> Result<(), DirstateError> { | ||||
|
r50024 | let node = self | ||
.get_node_mut(filename, |ancestor| { | ||||
|
r50023 | if !old_entry.tracked() { | ||
ancestor.tracked_descendants_count += 1; | ||||
} | ||||
|
r50024 | })? | ||
.expect("node should exist"); | ||||
|
r49995 | let mut new_entry = old_entry.clone(); | ||
new_entry.set_clean(mode, size, mtime); | ||||
node.data = NodeData::Entry(new_entry); | ||||
Ok(()) | ||||
} | ||||
|
r50023 | /// Set a node as possibly dirty in the dirstate. | ||
/// | ||||
/// # Panics | ||||
/// | ||||
/// Panics if the node does not exist. | ||||
|
r49997 | fn set_possibly_dirty( | ||
&mut self, | ||||
filename: &HgPath, | ||||
) -> Result<(), DirstateError> { | ||||
|
r50024 | let node = self | ||
.get_node_mut(filename, |_ancestor| {})? | ||||
.expect("node should exist"); | ||||
|
r49997 | let entry = node.data.as_entry_mut().expect("entry should exist"); | ||
entry.set_possibly_dirty(); | ||||
node.data = NodeData::Entry(*entry); | ||||
Ok(()) | ||||
} | ||||
|
r50018 | /// Clears the cached mtime for the (potential) folder at `path`. | ||
pub(super) fn clear_cached_mtime( | ||||
|
r47873 | &mut self, | ||
path: &HgPath, | ||||
|
r48126 | ) -> Result<(), DirstateV2ParseError> { | ||
|
r50024 | let node = match self.get_node_mut(path, |_ancestor| {})? { | ||
|
r50018 | Some(node) => node, | ||
None => return Ok(()), | ||||
}; | ||||
if let NodeData::CachedDirectory { .. } = &node.data { | ||||
node.data = NodeData::None | ||||
} | ||||
Ok(()) | ||||
} | ||||
|
r47876 | |||
|
r50019 | /// Sets the cached mtime for the (potential) folder at `path`. | ||
pub(super) fn set_cached_mtime( | ||||
&mut self, | ||||
path: &HgPath, | ||||
mtime: TruncatedTimestamp, | ||||
) -> Result<(), DirstateV2ParseError> { | ||||
|
r50024 | let node = match self.get_node_mut(path, |_ancestor| {})? { | ||
|
r50019 | Some(node) => node, | ||
None => return Ok(()), | ||||
}; | ||||
match &node.data { | ||||
NodeData::Entry(_) => {} // Don’t overwrite an entry | ||||
NodeData::CachedDirectory { .. } | NodeData::None => { | ||||
node.data = NodeData::CachedDirectory { mtime } | ||||
} | ||||
|
r47873 | } | ||
|
r48126 | Ok(()) | ||
|
r47873 | } | ||
|
r48124 | fn iter_nodes<'tree>( | ||
&'tree self, | ||||
|
r48126 | ) -> impl Iterator< | ||
Item = Result<NodeRef<'tree, 'on_disk>, DirstateV2ParseError>, | ||||
> + 'tree { | ||||
|
r47870 | // Depth first tree traversal. | ||
// | ||||
// If we could afford internal iteration and recursion, | ||||
// this would look like: | ||||
// | ||||
// ``` | ||||
// fn traverse_children( | ||||
// children: &ChildNodes, | ||||
// each: &mut impl FnMut(&Node), | ||||
// ) { | ||||
// for child in children.values() { | ||||
// traverse_children(&child.children, each); | ||||
// each(child); | ||||
// } | ||||
// } | ||||
// ``` | ||||
// | ||||
// However we want an external iterator and therefore can’t use the | ||||
// call stack. Use an explicit stack instead: | ||||
let mut stack = Vec::new(); | ||||
|
r48124 | let mut iter = self.root.as_ref().iter(); | ||
|
r47870 | std::iter::from_fn(move || { | ||
|
r48124 | while let Some(child_node) = iter.next() { | ||
|
r48127 | let children = match child_node.children(self.on_disk) { | ||
|
r48126 | Ok(children) => children, | ||
Err(error) => return Some(Err(error)), | ||||
}; | ||||
|
r47870 | // Pseudo-recursion | ||
|
r48126 | let new_iter = children.iter(); | ||
|
r47870 | let old_iter = std::mem::replace(&mut iter, new_iter); | ||
|
r48124 | stack.push((child_node, old_iter)); | ||
|
r47870 | } | ||
// Found the end of a `children.iter()` iterator. | ||||
|
r48124 | if let Some((child_node, next_iter)) = stack.pop() { | ||
|
r47870 | // "Return" from pseudo-recursion by restoring state from the | ||
// explicit stack | ||||
iter = next_iter; | ||||
|
r48126 | Some(Ok(child_node)) | ||
|
r47870 | } else { | ||
// Reached the bottom of the stack, we’re done | ||||
None | ||||
} | ||||
}) | ||||
} | ||||
|
r48481 | fn count_dropped_path(unreachable_bytes: &mut u32, path: &Cow<HgPath>) { | ||
if let Cow::Borrowed(path) = path { | ||||
*unreachable_bytes += path.len() as u32 | ||||
} | ||||
} | ||||
|
r51117 | |||
pub(crate) fn set_write_mode(&mut self, write_mode: DirstateMapWriteMode) { | ||||
self.write_mode = write_mode; | ||||
} | ||||
|
r48126 | } | ||
/// Like `Iterator::filter_map`, but over a fallible iterator of `Result`s. | ||||
/// | ||||
/// The callback is only called for incoming `Ok` values. Errors are passed | ||||
/// through as-is. In order to let it use the `?` operator the callback is | ||||
/// expected to return a `Result` of `Option`, instead of an `Option` of | ||||
/// `Result`. | ||||
fn filter_map_results<'a, I, F, A, B, E>( | ||||
iter: I, | ||||
f: F, | ||||
) -> impl Iterator<Item = Result<B, E>> + 'a | ||||
where | ||||
I: Iterator<Item = Result<A, E>> + 'a, | ||||
F: Fn(A) -> Result<Option<B>, E> + 'a, | ||||
{ | ||||
iter.filter_map(move |result| match result { | ||||
Ok(node) => f(node).transpose(), | ||||
Err(e) => Some(Err(e)), | ||||
}) | ||||
|
r47865 | } | ||
|
r48883 | impl OwningDirstateMap { | ||
pub fn clear(&mut self) { | ||||
|
r49864 | self.with_dmap_mut(|map| { | ||
map.root = Default::default(); | ||||
map.nodes_with_entry_count = 0; | ||||
map.nodes_with_copy_source_count = 0; | ||||
}); | ||||
|
r47865 | } | ||
|
r49988 | pub fn set_tracked( | ||
&mut self, | ||||
filename: &HgPath, | ||||
) -> Result<bool, DirstateV2ParseError> { | ||||
let old_entry_opt = self.get(filename)?; | ||||
self.with_dmap_mut(|map| map.set_tracked(filename, old_entry_opt)) | ||||
} | ||||
|
r49999 | pub fn set_untracked( | ||
|
r48861 | &mut self, | ||
filename: &HgPath, | ||||
|
r49999 | ) -> Result<bool, DirstateError> { | ||
let old_entry_opt = self.get(filename)?; | ||||
match old_entry_opt { | ||||
None => Ok(false), | ||||
Some(old_entry) => { | ||||
if !old_entry.tracked() { | ||||
// `DirstateMap::set_untracked` is not a noop if | ||||
// already not tracked as it will decrement the | ||||
// tracked counters while going down. | ||||
return Ok(true); | ||||
} | ||||
if old_entry.added() { | ||||
// Untracking an "added" entry will just result in a | ||||
// worthless entry (and other parts of the code will | ||||
// complain about it), just drop it entirely. | ||||
self.drop_entry_and_copy_source(filename)?; | ||||
return Ok(true); | ||||
} | ||||
if !old_entry.p2_info() { | ||||
self.copy_map_remove(filename)?; | ||||
} | ||||
self.with_dmap_mut(|map| { | ||||
map.set_untracked(filename, old_entry)?; | ||||
Ok(true) | ||||
}) | ||||
} | ||||
} | ||||
r48492 | } | |||
|
r49995 | pub fn set_clean( | ||
|
r47865 | &mut self, | ||
|
r47877 | filename: &HgPath, | ||
|
r49995 | mode: u32, | ||
size: u32, | ||||
mtime: TruncatedTimestamp, | ||||
|
r48126 | ) -> Result<(), DirstateError> { | ||
|
r49995 | let old_entry = match self.get(filename)? { | ||
None => { | ||||
return Err( | ||||
DirstateMapError::PathNotFound(filename.into()).into() | ||||
) | ||||
} | ||||
Some(e) => e, | ||||
}; | ||||
self.copy_map_remove(filename)?; | ||||
|
r49864 | self.with_dmap_mut(|map| { | ||
|
r49995 | map.set_clean(filename, old_entry, mode, size, mtime) | ||
|
r49864 | }) | ||
|
r47865 | } | ||
|
r49997 | pub fn set_possibly_dirty( | ||
&mut self, | ||||
filename: &HgPath, | ||||
) -> Result<(), DirstateError> { | ||||
if self.get(filename)?.is_none() { | ||||
return Err(DirstateMapError::PathNotFound(filename.into()).into()); | ||||
} | ||||
self.with_dmap_mut(|map| map.set_possibly_dirty(filename)) | ||||
} | ||||
|
r49992 | pub fn reset_state( | ||
|
r47865 | &mut self, | ||
|
r47877 | filename: &HgPath, | ||
|
r49992 | wc_tracked: bool, | ||
p1_tracked: bool, | ||||
p2_info: bool, | ||||
has_meaningful_mtime: bool, | ||||
parent_file_data_opt: Option<ParentFileData>, | ||||
|
r48126 | ) -> Result<(), DirstateError> { | ||
|
r49992 | if !(p1_tracked || p2_info || wc_tracked) { | ||
self.drop_entry_and_copy_source(filename)?; | ||||
return Ok(()); | ||||
} | ||||
self.copy_map_remove(filename)?; | ||||
r48300 | let old_entry_opt = self.get(filename)?; | |||
|
r49864 | self.with_dmap_mut(|map| { | ||
|
r49992 | map.reset_state( | ||
filename, | ||||
old_entry_opt, | ||||
wc_tracked, | ||||
p1_tracked, | ||||
p2_info, | ||||
has_meaningful_mtime, | ||||
parent_file_data_opt, | ||||
) | ||||
|
r49864 | }) | ||
|
r47865 | } | ||
|
r48883 | pub fn drop_entry_and_copy_source( | ||
|
r48864 | &mut self, | ||
filename: &HgPath, | ||||
) -> Result<(), DirstateError> { | ||||
|
r50014 | let was_tracked = self.get(filename)?.map_or(false, |e| e.tracked()); | ||
|
r47963 | struct Dropped { | ||
was_tracked: bool, | ||||
had_entry: bool, | ||||
had_copy_source: bool, | ||||
} | ||||
|
r48141 | |||
/// If this returns `Ok(Some((dropped, removed)))`, then | ||||
/// | ||||
/// * `dropped` is about the leaf node that was at `filename` | ||||
/// * `removed` is whether this particular level of recursion just | ||||
/// removed a node in `nodes`. | ||||
|
r48127 | fn recur<'on_disk>( | ||
on_disk: &'on_disk [u8], | ||||
|
r48481 | unreachable_bytes: &mut u32, | ||
|
r48127 | nodes: &mut ChildNodes<'on_disk>, | ||
|
r48126 | path: &HgPath, | ||
|
r48141 | ) -> Result<Option<(Dropped, bool)>, DirstateV2ParseError> { | ||
|
r47963 | let (first_path_component, rest_of_path) = | ||
path.split_first_component(); | ||||
|
r48481 | let nodes = nodes.make_mut(on_disk, unreachable_bytes)?; | ||
let node = if let Some(node) = nodes.get_mut(first_path_component) | ||||
|
r48126 | { | ||
node | ||||
} else { | ||||
return Ok(None); | ||||
}; | ||||
|
r47963 | let dropped; | ||
if let Some(rest) = rest_of_path { | ||||
|
r48481 | if let Some((d, removed)) = recur( | ||
on_disk, | ||||
unreachable_bytes, | ||||
&mut node.children, | ||||
rest, | ||||
)? { | ||||
|
r48126 | dropped = d; | ||
|
r48272 | if dropped.had_entry { | ||
|
r49865 | node.descendants_with_entry_count = node | ||
.descendants_with_entry_count | ||||
.checked_sub(1) | ||||
.expect( | ||||
"descendants_with_entry_count should be >= 0", | ||||
); | ||||
|
r48272 | } | ||
|
r48126 | if dropped.was_tracked { | ||
|
r49865 | node.tracked_descendants_count = node | ||
.tracked_descendants_count | ||||
.checked_sub(1) | ||||
.expect( | ||||
"tracked_descendants_count should be >= 0", | ||||
); | ||||
|
r48126 | } | ||
|
r48141 | |||
// Directory caches must be invalidated when removing a | ||||
// child node | ||||
if removed { | ||||
if let NodeData::CachedDirectory { .. } = &node.data { | ||||
node.data = NodeData::None | ||||
} | ||||
} | ||||
|
r48126 | } else { | ||
return Ok(None); | ||||
|
r47890 | } | ||
|
r47963 | } else { | ||
|
r49866 | let entry = node.data.as_entry(); | ||
let was_tracked = entry.map_or(false, |entry| entry.tracked()); | ||||
let had_entry = entry.is_some(); | ||||
|
r48137 | if had_entry { | ||
node.data = NodeData::None | ||||
} | ||||
|
r49867 | let mut had_copy_source = false; | ||
|
r48481 | if let Some(source) = &node.copy_source { | ||
|
r48864 | DirstateMap::count_dropped_path(unreachable_bytes, source); | ||
|
r49867 | had_copy_source = true; | ||
|
r48864 | node.copy_source = None | ||
|
r48481 | } | ||
|
r47963 | dropped = Dropped { | ||
|
r49866 | was_tracked, | ||
|
r48137 | had_entry, | ||
|
r49867 | had_copy_source, | ||
|
r47963 | }; | ||
|
r47964 | } | ||
// After recursion, for both leaf (rest_of_path is None) nodes and | ||||
// parent nodes, remove a node if it just became empty. | ||||
|
r48141 | let remove = !node.data.has_entry() | ||
|
r47964 | && node.copy_source.is_none() | ||
|
r48141 | && node.children.is_empty(); | ||
if remove { | ||||
|
r48481 | let (key, _) = | ||
nodes.remove_entry(first_path_component).unwrap(); | ||||
DirstateMap::count_dropped_path( | ||||
unreachable_bytes, | ||||
key.full_path(), | ||||
) | ||||
|
r47963 | } | ||
|
r48141 | Ok(Some((dropped, remove))) | ||
|
r47963 | } | ||
|
r47877 | |||
|
r49864 | self.with_dmap_mut(|map| { | ||
if let Some((dropped, _removed)) = recur( | ||||
map.on_disk, | ||||
&mut map.unreachable_bytes, | ||||
&mut map.root, | ||||
filename, | ||||
)? { | ||||
if dropped.had_entry { | ||||
|
r49865 | map.nodes_with_entry_count = map | ||
.nodes_with_entry_count | ||||
.checked_sub(1) | ||||
.expect("nodes_with_entry_count should be >= 0"); | ||||
|
r49864 | } | ||
if dropped.had_copy_source { | ||||
|
r49865 | map.nodes_with_copy_source_count = map | ||
.nodes_with_copy_source_count | ||||
.checked_sub(1) | ||||
.expect("nodes_with_copy_source_count should be >= 0"); | ||||
|
r49864 | } | ||
} else { | ||||
debug_assert!(!was_tracked); | ||||
|
r47877 | } | ||
|
r49864 | Ok(()) | ||
}) | ||||
|
r47865 | } | ||
|
r48883 | pub fn has_tracked_dir( | ||
|
r47865 | &mut self, | ||
|
r47876 | directory: &HgPath, | ||
|
r48126 | ) -> Result<bool, DirstateError> { | ||
|
r49864 | self.with_dmap_mut(|map| { | ||
if let Some(node) = map.get_node(directory)? { | ||||
// A node without a `DirstateEntry` was created to hold child | ||||
// nodes, and is therefore a directory. | ||||
|
r50031 | let is_dir = node.entry()?.is_none(); | ||
Ok(is_dir && node.tracked_descendants_count() > 0) | ||||
|
r49864 | } else { | ||
Ok(false) | ||||
} | ||||
}) | ||||
|
r47865 | } | ||
|
r48883 | pub fn has_dir( | ||
&mut self, | ||||
directory: &HgPath, | ||||
) -> Result<bool, DirstateError> { | ||||
|
r49864 | self.with_dmap_mut(|map| { | ||
if let Some(node) = map.get_node(directory)? { | ||||
// A node without a `DirstateEntry` was created to hold child | ||||
// nodes, and is therefore a directory. | ||||
|
r50031 | let is_dir = node.entry()?.is_none(); | ||
Ok(is_dir && node.descendants_with_entry_count() > 0) | ||||
|
r49864 | } else { | ||
Ok(false) | ||||
} | ||||
}) | ||||
|
r47865 | } | ||
|
r48055 | #[timed] | ||
|
r48883 | pub fn pack_v1( | ||
|
r49244 | &self, | ||
|
r47872 | parents: DirstateParents, | ||
|
r47865 | ) -> Result<Vec<u8>, DirstateError> { | ||
|
r49244 | let map = self.get_map(); | ||
|
r47872 | // Optizimation (to be measured?): pre-compute size to avoid `Vec` | ||
// reallocations | ||||
let mut size = parents.as_bytes().len(); | ||||
|
r48883 | for node in map.iter_nodes() { | ||
|
r48126 | let node = node?; | ||
r49221 | if node.entry()?.is_some() { | |||
|
r48127 | size += packed_entry_size( | ||
|
r48883 | node.full_path(map.on_disk)?, | ||
node.copy_source(map.on_disk)?, | ||||
|
r48127 | ); | ||
|
r47872 | } | ||
} | ||||
let mut packed = Vec::with_capacity(size); | ||||
packed.extend(parents.as_bytes()); | ||||
|
r48883 | for node in map.iter_nodes() { | ||
|
r48126 | let node = node?; | ||
if let Some(entry) = node.entry()? { | ||||
|
r47872 | pack_entry( | ||
|
r48883 | node.full_path(map.on_disk)?, | ||
|
r48124 | &entry, | ||
|
r48883 | node.copy_source(map.on_disk)?, | ||
|
r47872 | &mut packed, | ||
); | ||||
} | ||||
} | ||||
Ok(packed) | ||||
|
r47865 | } | ||
|
r48482 | /// Returns new data and metadata together with whether that data should be | ||
/// appended to the existing data file whose content is at | ||||
|
r48883 | /// `map.on_disk` (true), instead of written to a new data file | ||
|
r50037 | /// (false), and the previous size of data on disk. | ||
|
r48055 | #[timed] | ||
|
r48883 | pub fn pack_v2( | ||
|
r49244 | &self, | ||
r51116 | write_mode: DirstateMapWriteMode, | |||
|
r50037 | ) -> Result<(Vec<u8>, on_disk::TreeMetadata, bool, usize), DirstateError> | ||
{ | ||||
|
r49244 | let map = self.get_map(); | ||
r51116 | on_disk::write(map, write_mode) | |||
|
r48055 | } | ||
|
r49864 | /// `callback` allows the caller to process and do something with the | ||
/// results of the status. This is needed to do so efficiently (i.e. | ||||
/// without cloning the `DirstateStatus` object with its paths) because | ||||
/// we need to borrow from `Self`. | ||||
pub fn with_status<R>( | ||||
&mut self, | ||||
matcher: &(dyn Matcher + Sync), | ||||
|
r47882 | root_dir: PathBuf, | ||
ignore_files: Vec<PathBuf>, | ||||
options: StatusOptions, | ||||
|
r49864 | callback: impl for<'r> FnOnce( | ||
Result<(DirstateStatus<'r>, Vec<PatternFileWarning>), StatusError>, | ||||
) -> R, | ||||
) -> R { | ||||
self.with_dmap_mut(|map| { | ||||
callback(super::status::status( | ||||
map, | ||||
matcher, | ||||
root_dir, | ||||
ignore_files, | ||||
options, | ||||
)) | ||||
}) | ||||
|
r47865 | } | ||
|
r48883 | pub fn copy_map_len(&self) -> usize { | ||
let map = self.get_map(); | ||||
map.nodes_with_copy_source_count as usize | ||||
|
r47865 | } | ||
|
r48883 | pub fn copy_map_iter(&self) -> CopyMapIter<'_> { | ||
let map = self.get_map(); | ||||
Box::new(filter_map_results(map.iter_nodes(), move |node| { | ||||
Ok(if let Some(source) = node.copy_source(map.on_disk)? { | ||||
Some((node.full_path(map.on_disk)?, source)) | ||||
|
r48126 | } else { | ||
None | ||||
}) | ||||
|
r47870 | })) | ||
|
r47865 | } | ||
|
r48883 | pub fn copy_map_contains_key( | ||
|
r48126 | &self, | ||
key: &HgPath, | ||||
) -> Result<bool, DirstateV2ParseError> { | ||||
|
r48883 | let map = self.get_map(); | ||
Ok(if let Some(node) = map.get_node(key)? { | ||||
|
r48126 | node.has_copy_source() | ||
|
r47869 | } else { | ||
false | ||||
|
r48126 | }) | ||
|
r47865 | } | ||
|
r48883 | pub fn copy_map_get( | ||
|
r48126 | &self, | ||
key: &HgPath, | ||||
) -> Result<Option<&HgPath>, DirstateV2ParseError> { | ||||
|
r48883 | let map = self.get_map(); | ||
if let Some(node) = map.get_node(key)? { | ||||
if let Some(source) = node.copy_source(map.on_disk)? { | ||||
|
r48126 | return Ok(Some(source)); | ||
} | ||||
} | ||||
Ok(None) | ||||
|
r47865 | } | ||
|
r48883 | pub fn copy_map_remove( | ||
|
r48126 | &mut self, | ||
key: &HgPath, | ||||
) -> Result<Option<HgPathBuf>, DirstateV2ParseError> { | ||||
|
r49864 | self.with_dmap_mut(|map| { | ||
let count = &mut map.nodes_with_copy_source_count; | ||||
let unreachable_bytes = &mut map.unreachable_bytes; | ||||
|
r50024 | Ok(DirstateMap::get_node_mut_inner( | ||
|
r49864 | map.on_disk, | ||
unreachable_bytes, | ||||
&mut map.root, | ||||
key, | ||||
|
r50022 | |_ancestor| {}, | ||
|
r49864 | )? | ||
.and_then(|node| { | ||||
if let Some(source) = &node.copy_source { | ||||
|
r50025 | *count = count | ||
.checked_sub(1) | ||||
.expect("nodes_with_copy_source_count should be >= 0"); | ||||
|
r49864 | DirstateMap::count_dropped_path(unreachable_bytes, source); | ||
} | ||||
node.copy_source.take().map(Cow::into_owned) | ||||
})) | ||||
}) | ||||
|
r47865 | } | ||
|
r48883 | pub fn copy_map_insert( | ||
|
r47865 | &mut self, | ||
|
r50015 | key: &HgPath, | ||
value: &HgPath, | ||||
|
r48126 | ) -> Result<Option<HgPathBuf>, DirstateV2ParseError> { | ||
|
r49864 | self.with_dmap_mut(|map| { | ||
|
r50021 | let node = map.get_or_insert_node(&key, |_ancestor| {})?; | ||
let had_copy_source = node.copy_source.is_none(); | ||||
let old = node | ||||
.copy_source | ||||
.replace(value.to_owned().into()) | ||||
.map(Cow::into_owned); | ||||
if had_copy_source { | ||||
|
r49864 | map.nodes_with_copy_source_count += 1 | ||
} | ||||
|
r50021 | Ok(old) | ||
|
r49864 | }) | ||
|
r47865 | } | ||
|
r48883 | pub fn len(&self) -> usize { | ||
let map = self.get_map(); | ||||
map.nodes_with_entry_count as usize | ||||
|
r47865 | } | ||
|
r48883 | pub fn contains_key( | ||
|
r48126 | &self, | ||
key: &HgPath, | ||||
) -> Result<bool, DirstateV2ParseError> { | ||||
Ok(self.get(key)?.is_some()) | ||||
|
r47865 | } | ||
|
r48883 | pub fn get( | ||
|
r48126 | &self, | ||
key: &HgPath, | ||||
) -> Result<Option<DirstateEntry>, DirstateV2ParseError> { | ||||
|
r48883 | let map = self.get_map(); | ||
Ok(if let Some(node) = map.get_node(key)? { | ||||
|
r48126 | node.entry()? | ||
} else { | ||||
None | ||||
}) | ||||
|
r47865 | } | ||
|
r48883 | pub fn iter(&self) -> StateMapIter<'_> { | ||
let map = self.get_map(); | ||||
Box::new(filter_map_results(map.iter_nodes(), move |node| { | ||||
|
r48126 | Ok(if let Some(entry) = node.entry()? { | ||
|
r48883 | Some((node.full_path(map.on_disk)?, entry)) | ||
|
r48126 | } else { | ||
None | ||||
}) | ||||
|
r47870 | })) | ||
|
r47865 | } | ||
|
r48140 | |||
|
r48883 | pub fn iter_tracked_dirs( | ||
|
r48483 | &mut self, | ||
) -> Result< | ||||
Box< | ||||
dyn Iterator<Item = Result<&HgPath, DirstateV2ParseError>> | ||||
+ Send | ||||
+ '_, | ||||
>, | ||||
DirstateError, | ||||
> { | ||||
|
r49864 | let map = self.get_map(); | ||
|
r48883 | let on_disk = map.on_disk; | ||
|
r48483 | Ok(Box::new(filter_map_results( | ||
|
r48883 | map.iter_nodes(), | ||
|
r48483 | move |node| { | ||
Ok(if node.tracked_descendants_count() > 0 { | ||||
Some(node.full_path(on_disk)?) | ||||
} else { | ||||
None | ||||
}) | ||||
}, | ||||
))) | ||||
} | ||||
|
r50011 | /// Only public because it needs to be exposed to the Python layer. | ||
/// It is not the full `setparents` logic, only the parts that mutate the | ||||
/// entries. | ||||
pub fn setparents_fixup( | ||||
&mut self, | ||||
) -> Result<Vec<(HgPathBuf, HgPathBuf)>, DirstateV2ParseError> { | ||||
// XXX | ||||
// All the copying and re-querying is quite inefficient, but this is | ||||
// still a lot better than doing it from Python. | ||||
// | ||||
// The better solution is to develop a mechanism for `iter_mut`, | ||||
// which will be a lot more involved: we're dealing with a lazy, | ||||
// append-mostly, tree-like data structure. This will do for now. | ||||
let mut copies = vec![]; | ||||
let mut files_with_p2_info = vec![]; | ||||
for res in self.iter() { | ||||
let (path, entry) = res?; | ||||
if entry.p2_info() { | ||||
files_with_p2_info.push(path.to_owned()) | ||||
} | ||||
} | ||||
self.with_dmap_mut(|map| { | ||||
for path in files_with_p2_info.iter() { | ||||
|
r50021 | let node = map.get_or_insert_node(path, |_| {})?; | ||
|
r50011 | let entry = | ||
node.data.as_entry_mut().expect("entry should exist"); | ||||
entry.drop_merge_data(); | ||||
if let Some(source) = node.copy_source.take().as_deref() { | ||||
copies.push((path.to_owned(), source.to_owned())); | ||||
} | ||||
} | ||||
Ok(copies) | ||||
}) | ||||
} | ||||
|
r48883 | pub fn debug_iter( | ||
|
r48140 | &self, | ||
|
r48835 | all: bool, | ||
|
r48140 | ) -> Box< | ||
dyn Iterator< | ||||
Item = Result< | ||||
|
r48483 | (&HgPath, (u8, i32, i32, i32)), | ||
|
r48140 | DirstateV2ParseError, | ||
>, | ||||
> + Send | ||||
+ '_, | ||||
> { | ||||
|
r48883 | let map = self.get_map(); | ||
Box::new(filter_map_results(map.iter_nodes(), move |node| { | ||||
|
r48483 | let debug_tuple = if let Some(entry) = node.entry()? { | ||
entry.debug_tuple() | ||||
|
r48835 | } else if !all { | ||
return Ok(None); | ||||
|
r49007 | } else if let Some(mtime) = node.cached_directory_mtime()? { | ||
(b' ', 0, -1, mtime.truncated_seconds() as i32) | ||||
|
r48140 | } else { | ||
|
r48483 | (b' ', 0, -1, -1) | ||
}; | ||||
|
r48883 | Ok(Some((node.full_path(map.on_disk)?, debug_tuple))) | ||
|
r48140 | })) | ||
} | ||||
|
r47865 | } | ||
|
r50017 | #[cfg(test)] | ||
mod tests { | ||||
use super::*; | ||||
/// Shortcut to return tracked descendants of a path. | ||||
/// Panics if the path does not exist. | ||||
fn tracked_descendants(map: &OwningDirstateMap, path: &[u8]) -> u32 { | ||||
let path = dbg!(HgPath::new(path)); | ||||
let node = map.get_map().get_node(path); | ||||
node.unwrap().unwrap().tracked_descendants_count() | ||||
} | ||||
/// Shortcut to return descendants with an entry. | ||||
/// Panics if the path does not exist. | ||||
fn descendants_with_an_entry(map: &OwningDirstateMap, path: &[u8]) -> u32 { | ||||
let path = dbg!(HgPath::new(path)); | ||||
let node = map.get_map().get_node(path); | ||||
node.unwrap().unwrap().descendants_with_entry_count() | ||||
} | ||||
fn assert_does_not_exist(map: &OwningDirstateMap, path: &[u8]) { | ||||
let path = dbg!(HgPath::new(path)); | ||||
let node = map.get_map().get_node(path); | ||||
assert!(node.unwrap().is_none()); | ||||
} | ||||
/// Shortcut for path creation in tests | ||||
fn p(b: &[u8]) -> &HgPath { | ||||
HgPath::new(b) | ||||
} | ||||
/// Test the very simple case a single tracked file | ||||
#[test] | ||||
fn test_tracked_descendants_simple() -> Result<(), DirstateError> { | ||||
let mut map = OwningDirstateMap::new_empty(vec![]); | ||||
assert_eq!(map.len(), 0); | ||||
map.set_tracked(p(b"some/nested/path"))?; | ||||
assert_eq!(map.len(), 1); | ||||
assert_eq!(tracked_descendants(&map, b"some"), 1); | ||||
assert_eq!(tracked_descendants(&map, b"some/nested"), 1); | ||||
assert_eq!(tracked_descendants(&map, b"some/nested/path"), 0); | ||||
map.set_untracked(p(b"some/nested/path"))?; | ||||
assert_eq!(map.len(), 0); | ||||
assert!(map.get_map().get_node(p(b"some"))?.is_none()); | ||||
Ok(()) | ||||
} | ||||
/// Test the simple case of all tracked, but multiple files | ||||
#[test] | ||||
fn test_tracked_descendants_multiple() -> Result<(), DirstateError> { | ||||
let mut map = OwningDirstateMap::new_empty(vec![]); | ||||
map.set_tracked(p(b"some/nested/path"))?; | ||||
map.set_tracked(p(b"some/nested/file"))?; | ||||
// one layer without any files to test deletion cascade | ||||
map.set_tracked(p(b"some/other/nested/path"))?; | ||||
map.set_tracked(p(b"root_file"))?; | ||||
map.set_tracked(p(b"some/file"))?; | ||||
map.set_tracked(p(b"some/file2"))?; | ||||
map.set_tracked(p(b"some/file3"))?; | ||||
assert_eq!(map.len(), 7); | ||||
assert_eq!(tracked_descendants(&map, b"some"), 6); | ||||
assert_eq!(tracked_descendants(&map, b"some/nested"), 2); | ||||
assert_eq!(tracked_descendants(&map, b"some/other"), 1); | ||||
assert_eq!(tracked_descendants(&map, b"some/other/nested"), 1); | ||||
assert_eq!(tracked_descendants(&map, b"some/nested/path"), 0); | ||||
map.set_untracked(p(b"some/nested/path"))?; | ||||
assert_eq!(map.len(), 6); | ||||
assert_eq!(tracked_descendants(&map, b"some"), 5); | ||||
assert_eq!(tracked_descendants(&map, b"some/nested"), 1); | ||||
assert_eq!(tracked_descendants(&map, b"some/other"), 1); | ||||
assert_eq!(tracked_descendants(&map, b"some/other/nested"), 1); | ||||
map.set_untracked(p(b"some/nested/file"))?; | ||||
assert_eq!(map.len(), 5); | ||||
assert_eq!(tracked_descendants(&map, b"some"), 4); | ||||
assert_eq!(tracked_descendants(&map, b"some/other"), 1); | ||||
assert_eq!(tracked_descendants(&map, b"some/other/nested"), 1); | ||||
assert_does_not_exist(&map, b"some_nested"); | ||||
map.set_untracked(p(b"some/other/nested/path"))?; | ||||
assert_eq!(map.len(), 4); | ||||
assert_eq!(tracked_descendants(&map, b"some"), 3); | ||||
assert_does_not_exist(&map, b"some/other"); | ||||
map.set_untracked(p(b"root_file"))?; | ||||
assert_eq!(map.len(), 3); | ||||
assert_eq!(tracked_descendants(&map, b"some"), 3); | ||||
assert_does_not_exist(&map, b"root_file"); | ||||
map.set_untracked(p(b"some/file"))?; | ||||
assert_eq!(map.len(), 2); | ||||
assert_eq!(tracked_descendants(&map, b"some"), 2); | ||||
assert_does_not_exist(&map, b"some/file"); | ||||
map.set_untracked(p(b"some/file2"))?; | ||||
assert_eq!(map.len(), 1); | ||||
assert_eq!(tracked_descendants(&map, b"some"), 1); | ||||
assert_does_not_exist(&map, b"some/file2"); | ||||
map.set_untracked(p(b"some/file3"))?; | ||||
assert_eq!(map.len(), 0); | ||||
assert_does_not_exist(&map, b"some/file3"); | ||||
Ok(()) | ||||
} | ||||
/// Check with a mix of tracked and non-tracked items | ||||
#[test] | ||||
fn test_tracked_descendants_different() -> Result<(), DirstateError> { | ||||
let mut map = OwningDirstateMap::new_empty(vec![]); | ||||
// A file that was just added | ||||
map.set_tracked(p(b"some/nested/path"))?; | ||||
// This has no information, the dirstate should ignore it | ||||
map.reset_state(p(b"some/file"), false, false, false, false, None)?; | ||||
assert_does_not_exist(&map, b"some/file"); | ||||
// A file that was removed | ||||
map.reset_state( | ||||
p(b"some/nested/file"), | ||||
false, | ||||
true, | ||||
false, | ||||
false, | ||||
None, | ||||
)?; | ||||
assert!(!map.get(p(b"some/nested/file"))?.unwrap().tracked()); | ||||
// Only present in p2 | ||||
map.reset_state(p(b"some/file3"), false, false, true, false, None)?; | ||||
assert!(!map.get(p(b"some/file3"))?.unwrap().tracked()); | ||||
// A file that was merged | ||||
map.reset_state(p(b"root_file"), true, true, true, false, None)?; | ||||
assert!(map.get(p(b"root_file"))?.unwrap().tracked()); | ||||
// A file that is added, with info from p2 | ||||
// XXX is that actually possible? | ||||
map.reset_state(p(b"some/file2"), true, false, true, false, None)?; | ||||
assert!(map.get(p(b"some/file2"))?.unwrap().tracked()); | ||||
// A clean file | ||||
// One layer without any files to test deletion cascade | ||||
map.reset_state( | ||||
p(b"some/other/nested/path"), | ||||
true, | ||||
true, | ||||
false, | ||||
false, | ||||
None, | ||||
)?; | ||||
assert!(map.get(p(b"some/other/nested/path"))?.unwrap().tracked()); | ||||
assert_eq!(map.len(), 6); | ||||
assert_eq!(tracked_descendants(&map, b"some"), 3); | ||||
assert_eq!(descendants_with_an_entry(&map, b"some"), 5); | ||||
assert_eq!(tracked_descendants(&map, b"some/other/nested"), 1); | ||||
assert_eq!(descendants_with_an_entry(&map, b"some/other/nested"), 1); | ||||
assert_eq!(tracked_descendants(&map, b"some/other/nested/path"), 0); | ||||
assert_eq!( | ||||
descendants_with_an_entry(&map, b"some/other/nested/path"), | ||||
0 | ||||
); | ||||
assert_eq!(tracked_descendants(&map, b"some/nested"), 1); | ||||
assert_eq!(descendants_with_an_entry(&map, b"some/nested"), 2); | ||||
// might as well check this | ||||
map.set_untracked(p(b"path/does/not/exist"))?; | ||||
assert_eq!(map.len(), 6); | ||||
map.set_untracked(p(b"some/other/nested/path"))?; | ||||
// It is set untracked but not deleted since it held other information | ||||
assert_eq!(map.len(), 6); | ||||
assert_eq!(tracked_descendants(&map, b"some"), 2); | ||||
assert_eq!(descendants_with_an_entry(&map, b"some"), 5); | ||||
assert_eq!(descendants_with_an_entry(&map, b"some/other"), 1); | ||||
assert_eq!(descendants_with_an_entry(&map, b"some/other/nested"), 1); | ||||
assert_eq!(tracked_descendants(&map, b"some/nested"), 1); | ||||
assert_eq!(descendants_with_an_entry(&map, b"some/nested"), 2); | ||||
map.set_untracked(p(b"some/nested/path"))?; | ||||
// It is set untracked *and* deleted since it was only added | ||||
assert_eq!(map.len(), 5); | ||||
assert_eq!(tracked_descendants(&map, b"some"), 1); | ||||
assert_eq!(descendants_with_an_entry(&map, b"some"), 4); | ||||
assert_eq!(tracked_descendants(&map, b"some/nested"), 0); | ||||
assert_eq!(descendants_with_an_entry(&map, b"some/nested"), 1); | ||||
assert_does_not_exist(&map, b"some/nested/path"); | ||||
map.set_untracked(p(b"root_file"))?; | ||||
// Untracked but not deleted | ||||
assert_eq!(map.len(), 5); | ||||
assert!(map.get(p(b"root_file"))?.is_some()); | ||||
map.set_untracked(p(b"some/file2"))?; | ||||
assert_eq!(map.len(), 5); | ||||
assert_eq!(tracked_descendants(&map, b"some"), 0); | ||||
assert!(map.get(p(b"some/file2"))?.is_some()); | ||||
map.set_untracked(p(b"some/file3"))?; | ||||
assert_eq!(map.len(), 5); | ||||
assert_eq!(tracked_descendants(&map, b"some"), 0); | ||||
assert!(map.get(p(b"some/file3"))?.is_some()); | ||||
Ok(()) | ||||
} | ||||
/// Check that copies counter is correctly updated | ||||
#[test] | ||||
fn test_copy_source() -> Result<(), DirstateError> { | ||||
let mut map = OwningDirstateMap::new_empty(vec![]); | ||||
// Clean file | ||||
map.reset_state(p(b"files/clean"), true, true, false, false, None)?; | ||||
// Merged file | ||||
map.reset_state(p(b"files/from_p2"), true, true, true, false, None)?; | ||||
// Removed file | ||||
map.reset_state(p(b"removed"), false, true, false, false, None)?; | ||||
// Added file | ||||
map.reset_state(p(b"files/added"), true, false, false, false, None)?; | ||||
// Add copy | ||||
map.copy_map_insert(p(b"files/clean"), p(b"clean_copy_source"))?; | ||||
assert_eq!(map.copy_map_len(), 1); | ||||
// Copy override | ||||
map.copy_map_insert(p(b"files/clean"), p(b"other_clean_copy_source"))?; | ||||
assert_eq!(map.copy_map_len(), 1); | ||||
// Multiple copies | ||||
map.copy_map_insert(p(b"removed"), p(b"removed_copy_source"))?; | ||||
assert_eq!(map.copy_map_len(), 2); | ||||
map.copy_map_insert(p(b"files/added"), p(b"added_copy_source"))?; | ||||
assert_eq!(map.copy_map_len(), 3); | ||||
// Added, so the entry is completely removed | ||||
map.set_untracked(p(b"files/added"))?; | ||||
assert_does_not_exist(&map, b"files/added"); | ||||
assert_eq!(map.copy_map_len(), 2); | ||||
// Removed, so the entry is kept around, so is its copy | ||||
map.set_untracked(p(b"removed"))?; | ||||
assert!(map.get(p(b"removed"))?.is_some()); | ||||
assert_eq!(map.copy_map_len(), 2); | ||||
// Clean, so the entry is kept around, but not its copy | ||||
map.set_untracked(p(b"files/clean"))?; | ||||
assert!(map.get(p(b"files/clean"))?.is_some()); | ||||
assert_eq!(map.copy_map_len(), 1); | ||||
map.copy_map_insert(p(b"files/from_p2"), p(b"from_p2_copy_source"))?; | ||||
assert_eq!(map.copy_map_len(), 2); | ||||
// Info from p2, so its copy source info is kept around | ||||
map.set_untracked(p(b"files/from_p2"))?; | ||||
assert!(map.get(p(b"files/from_p2"))?.is_some()); | ||||
assert_eq!(map.copy_map_len(), 2); | ||||
Ok(()) | ||||
} | ||||
/// Test with "on disk" data. For the sake of this test, the "on disk" data | ||||
/// does not actually come from the disk, but it's opaque to the code being | ||||
/// tested. | ||||
#[test] | ||||
fn test_on_disk() -> Result<(), DirstateError> { | ||||
// First let's create some data to put "on disk" | ||||
let mut map = OwningDirstateMap::new_empty(vec![]); | ||||
// A file that was just added | ||||
map.set_tracked(p(b"some/nested/added"))?; | ||||
map.copy_map_insert(p(b"some/nested/added"), p(b"added_copy_source"))?; | ||||
// A file that was removed | ||||
map.reset_state( | ||||
p(b"some/nested/removed"), | ||||
false, | ||||
true, | ||||
false, | ||||
false, | ||||
None, | ||||
)?; | ||||
// Only present in p2 | ||||
map.reset_state( | ||||
p(b"other/p2_info_only"), | ||||
false, | ||||
false, | ||||
true, | ||||
false, | ||||
None, | ||||
)?; | ||||
map.copy_map_insert( | ||||
p(b"other/p2_info_only"), | ||||
p(b"other/p2_info_copy_source"), | ||||
)?; | ||||
// A file that was merged | ||||
map.reset_state(p(b"merged"), true, true, true, false, None)?; | ||||
// A file that is added, with info from p2 | ||||
// XXX is that actually possible? | ||||
map.reset_state( | ||||
p(b"other/added_with_p2"), | ||||
true, | ||||
false, | ||||
true, | ||||
false, | ||||
None, | ||||
)?; | ||||
// One layer without any files to test deletion cascade | ||||
// A clean file | ||||
map.reset_state( | ||||
p(b"some/other/nested/clean"), | ||||
true, | ||||
true, | ||||
false, | ||||
false, | ||||
None, | ||||
)?; | ||||
|
r50039 | let (packed, metadata, _should_append, _old_data_size) = | ||
r51116 | map.pack_v2(DirstateMapWriteMode::ForceNewDataFile)?; | |||
|
r50017 | let packed_len = packed.len(); | ||
assert!(packed_len > 0); | ||||
// Recreate "from disk" | ||||
let mut map = OwningDirstateMap::new_v2( | ||||
packed, | ||||
packed_len, | ||||
metadata.as_bytes(), | ||||
)?; | ||||
// Check that everything is accounted for | ||||
assert!(map.contains_key(p(b"some/nested/added"))?); | ||||
assert!(map.contains_key(p(b"some/nested/removed"))?); | ||||
assert!(map.contains_key(p(b"merged"))?); | ||||
assert!(map.contains_key(p(b"other/p2_info_only"))?); | ||||
assert!(map.contains_key(p(b"other/added_with_p2"))?); | ||||
assert!(map.contains_key(p(b"some/other/nested/clean"))?); | ||||
assert_eq!( | ||||
map.copy_map_get(p(b"some/nested/added"))?, | ||||
Some(p(b"added_copy_source")) | ||||
); | ||||
assert_eq!( | ||||
map.copy_map_get(p(b"other/p2_info_only"))?, | ||||
Some(p(b"other/p2_info_copy_source")) | ||||
); | ||||
assert_eq!(tracked_descendants(&map, b"some"), 2); | ||||
assert_eq!(descendants_with_an_entry(&map, b"some"), 3); | ||||
assert_eq!(tracked_descendants(&map, b"other"), 1); | ||||
assert_eq!(descendants_with_an_entry(&map, b"other"), 2); | ||||
assert_eq!(tracked_descendants(&map, b"some/other"), 1); | ||||
assert_eq!(descendants_with_an_entry(&map, b"some/other"), 1); | ||||
assert_eq!(tracked_descendants(&map, b"some/other/nested"), 1); | ||||
assert_eq!(descendants_with_an_entry(&map, b"some/other/nested"), 1); | ||||
assert_eq!(tracked_descendants(&map, b"some/nested"), 1); | ||||
assert_eq!(descendants_with_an_entry(&map, b"some/nested"), 2); | ||||
assert_eq!(map.len(), 6); | ||||
assert_eq!(map.get_map().unreachable_bytes, 0); | ||||
assert_eq!(map.copy_map_len(), 2); | ||||
// Shouldn't change anything since it's already not tracked | ||||
map.set_untracked(p(b"some/nested/removed"))?; | ||||
assert_eq!(map.get_map().unreachable_bytes, 0); | ||||
match map.get_map().root { | ||||
ChildNodes::InMemory(_) => { | ||||
panic!("root should not have been mutated") | ||||
} | ||||
_ => (), | ||||
} | ||||
// We haven't mutated enough (nothing, actually), we should still be in | ||||
// the append strategy | ||||
assert!(map.get_map().write_should_append()); | ||||
// But this mutates the structure, so there should be unreachable_bytes | ||||
assert!(map.set_untracked(p(b"some/nested/added"))?); | ||||
let unreachable_bytes = map.get_map().unreachable_bytes; | ||||
assert!(unreachable_bytes > 0); | ||||
match map.get_map().root { | ||||
ChildNodes::OnDisk(_) => panic!("root should have been mutated"), | ||||
_ => (), | ||||
} | ||||
// This should not mutate the structure either, since `root` has | ||||
// already been mutated along with its direct children. | ||||
map.set_untracked(p(b"merged"))?; | ||||
assert_eq!(map.get_map().unreachable_bytes, unreachable_bytes); | ||||
match map.get_map().get_node(p(b"other/added_with_p2"))?.unwrap() { | ||||
NodeRef::InMemory(_, _) => { | ||||
panic!("'other/added_with_p2' should not have been mutated") | ||||
} | ||||
_ => (), | ||||
} | ||||
// But this should, since it's in a different path | ||||
// than `<root>some/nested/add` | ||||
map.set_untracked(p(b"other/added_with_p2"))?; | ||||
assert!(map.get_map().unreachable_bytes > unreachable_bytes); | ||||
match map.get_map().get_node(p(b"other/added_with_p2"))?.unwrap() { | ||||
NodeRef::OnDisk(_) => { | ||||
panic!("'other/added_with_p2' should have been mutated") | ||||
} | ||||
_ => (), | ||||
} | ||||
// We have rewritten most of the tree, we should create a new file | ||||
assert!(!map.get_map().write_should_append()); | ||||
Ok(()) | ||||
} | ||||
} | ||||