Details
Details
- Reviewers
Alphare pulkit - Group Reviewers
hg-reviewers - Commits
- rHGbd88b6bfd8da: rhg: Add support for dirstate-v2
Diff Detail
Diff Detail
- Repository
- rHG Mercurial
- Branch
- default
- Lint
No Linters Available - Unit
No Unit Test Coverage
| Alphare | |
| pulkit |
| hg-reviewers |
| No Linters Available |
| No Unit Test Coverage |
| Path | Packages | |||
|---|---|---|---|---|
| M | rust/hg-core/src/dirstate.rs (8 lines) | |||
| M | rust/hg-core/src/dirstate_tree.rs (2 lines) | |||
| M | rust/hg-core/src/dirstate_tree/on_disk.rs (63 lines) | |||
| M | rust/hg-core/src/operations/list_tracked_files.rs (35 lines) | |||
| M | rust/hg-core/src/repo.rs (15 lines) | |||
| M | rust/hg-core/src/requirements.rs (3 lines) | |||
| M | rust/rhg/src/commands/status.rs (14 lines) |
| Commit | Parents | Author | Summary | Date |
|---|---|---|---|---|
| 1c6657679b42 | 48cc6be23155 | Simon Sapin | May 25 2021, 3:20 AM |
| Status | Author | Revision | |
|---|---|---|---|
| Closed | SimonSapin | ||
| Closed | SimonSapin |
| // dirstate module | // dirstate module | ||||
| // | // | ||||
| // Copyright 2019 Raphaël Gomès <rgomes@octobus.net> | // Copyright 2019 Raphaël Gomès <rgomes@octobus.net> | ||||
| // | // | ||||
| // This software may be used and distributed according to the terms of the | // This software may be used and distributed according to the terms of the | ||||
| // GNU General Public License version 2 or any later version. | // GNU General Public License version 2 or any later version. | ||||
| use crate::dirstate_tree::on_disk::DirstateV2ParseError; | use crate::dirstate_tree::on_disk::DirstateV2ParseError; | ||||
| use crate::errors::HgError; | use crate::errors::HgError; | ||||
| use crate::revlog::node::NULL_NODE; | |||||
| use crate::revlog::Node; | use crate::revlog::Node; | ||||
| use crate::utils::hg_path::{HgPath, HgPathBuf}; | use crate::utils::hg_path::{HgPath, HgPathBuf}; | ||||
| use crate::FastHashMap; | use crate::FastHashMap; | ||||
| use bytes_cast::{unaligned, BytesCast}; | use bytes_cast::{unaligned, BytesCast}; | ||||
| use std::convert::TryFrom; | use std::convert::TryFrom; | ||||
| pub mod dirs_multiset; | pub mod dirs_multiset; | ||||
| pub mod dirstate_map; | pub mod dirstate_map; | ||||
| pub mod parsers; | pub mod parsers; | ||||
| pub mod status; | pub mod status; | ||||
| #[derive(Debug, PartialEq, Clone, BytesCast)] | #[derive(Debug, PartialEq, Clone, BytesCast)] | ||||
| #[repr(C)] | #[repr(C)] | ||||
| pub struct DirstateParents { | pub struct DirstateParents { | ||||
| pub p1: Node, | pub p1: Node, | ||||
| pub p2: Node, | pub p2: Node, | ||||
| } | } | ||||
| impl DirstateParents { | |||||
| pub const NULL: Self = Self { | |||||
| p1: NULL_NODE, | |||||
| p2: NULL_NODE, | |||||
| }; | |||||
| } | |||||
| /// The C implementation uses all signed types. This will be an issue | /// The C implementation uses all signed types. This will be an issue | ||||
| /// either when 4GB+ source files are commonplace or in 2038, whichever | /// either when 4GB+ source files are commonplace or in 2038, whichever | ||||
| /// comes first. | /// comes first. | ||||
| #[derive(Debug, PartialEq, Copy, Clone)] | #[derive(Debug, PartialEq, Copy, Clone)] | ||||
| pub struct DirstateEntry { | pub struct DirstateEntry { | ||||
| pub state: EntryState, | pub state: EntryState, | ||||
| pub mode: i32, | pub mode: i32, | ||||
| pub mtime: i32, | pub mtime: i32, | ||||
| pub mod dirstate_map; | pub mod dirstate_map; | ||||
| pub mod dispatch; | pub mod dispatch; | ||||
| pub mod on_disk; | pub mod on_disk; | ||||
| pub mod path_with_basename; | pub mod path_with_basename; | ||||
| mod status; | pub mod status; | ||||
| } | } | ||||
| impl From<DirstateV2ParseError> for crate::DirstateError { | impl From<DirstateV2ParseError> for crate::DirstateError { | ||||
| fn from(error: DirstateV2ParseError) -> Self { | fn from(error: DirstateV2ParseError) -> Self { | ||||
| HgError::from(error).into() | HgError::from(error).into() | ||||
| } | } | ||||
| } | } | ||||
| fn read_header(on_disk: &[u8]) -> Result<&Header, DirstateV2ParseError> { | |||||
| let (header, _) = | |||||
| Header::from_bytes(on_disk).map_err(|_| DirstateV2ParseError)?; | |||||
| if header.marker == *V2_FORMAT_MARKER { | |||||
| Ok(header) | |||||
| } else { | |||||
| Err(DirstateV2ParseError) | |||||
| } | |||||
| } | |||||
| pub(super) fn read<'on_disk>( | pub(super) fn read<'on_disk>( | ||||
| on_disk: &'on_disk [u8], | on_disk: &'on_disk [u8], | ||||
| ) -> Result< | ) -> Result< | ||||
| (DirstateMap<'on_disk>, Option<DirstateParents>), | (DirstateMap<'on_disk>, Option<DirstateParents>), | ||||
| DirstateV2ParseError, | DirstateV2ParseError, | ||||
| > { | > { | ||||
| if on_disk.is_empty() { | if on_disk.is_empty() { | ||||
| return Ok((DirstateMap::empty(on_disk), None)); | return Ok((DirstateMap::empty(on_disk), None)); | ||||
| } | } | ||||
| let (header, _) = | let header = read_header(on_disk)?; | ||||
| Header::from_bytes(on_disk).map_err(|_| DirstateV2ParseError)?; | |||||
| let Header { | |||||
| marker, | |||||
| parents, | |||||
| root, | |||||
| nodes_with_entry_count, | |||||
| nodes_with_copy_source_count, | |||||
| } = header; | |||||
| if marker != V2_FORMAT_MARKER { | |||||
| return Err(DirstateV2ParseError); | |||||
| } | |||||
| let dirstate_map = DirstateMap { | let dirstate_map = DirstateMap { | ||||
| on_disk, | on_disk, | ||||
| root: dirstate_map::ChildNodes::OnDisk(read_slice::<Node>( | root: dirstate_map::ChildNodes::OnDisk(read_slice::<Node>( | ||||
| on_disk, *root, | on_disk, | ||||
| header.root, | |||||
| )?), | )?), | ||||
| nodes_with_entry_count: nodes_with_entry_count.get(), | nodes_with_entry_count: header.nodes_with_entry_count.get(), | ||||
| nodes_with_copy_source_count: nodes_with_copy_source_count.get(), | nodes_with_copy_source_count: header | ||||
| .nodes_with_copy_source_count | |||||
| .get(), | |||||
| }; | }; | ||||
| let parents = Some(parents.clone()); | let parents = Some(header.parents.clone()); | ||||
| Ok((dirstate_map, parents)) | Ok((dirstate_map, parents)) | ||||
| } | } | ||||
| impl Node { | impl Node { | ||||
| pub(super) fn full_path<'on_disk>( | pub(super) fn full_path<'on_disk>( | ||||
| &self, | &self, | ||||
| on_disk: &'on_disk [u8], | on_disk: &'on_disk [u8], | ||||
| ) -> Result<&'on_disk HgPath, DirstateV2ParseError> { | ) -> Result<&'on_disk HgPath, DirstateV2ParseError> { | ||||
| let len = usize::try_from(slice.len.get()).unwrap_or(std::usize::MAX); | let len = usize::try_from(slice.len.get()).unwrap_or(std::usize::MAX); | ||||
| on_disk | on_disk | ||||
| .get(start..) | .get(start..) | ||||
| .and_then(|bytes| T::slice_from_bytes(bytes, len).ok()) | .and_then(|bytes| T::slice_from_bytes(bytes, len).ok()) | ||||
| .map(|(slice, _rest)| slice) | .map(|(slice, _rest)| slice) | ||||
| .ok_or_else(|| DirstateV2ParseError) | .ok_or_else(|| DirstateV2ParseError) | ||||
| } | } | ||||
| pub(crate) fn parse_dirstate_parents( | |||||
| on_disk: &[u8], | |||||
| ) -> Result<&DirstateParents, HgError> { | |||||
| Ok(&read_header(on_disk)?.parents) | |||||
| } | |||||
| pub(crate) fn for_each_tracked_path<'on_disk>( | |||||
| on_disk: &'on_disk [u8], | |||||
| mut f: impl FnMut(&'on_disk HgPath), | |||||
| ) -> Result<(), DirstateV2ParseError> { | |||||
| let header = read_header(on_disk)?; | |||||
| fn recur<'on_disk>( | |||||
| on_disk: &'on_disk [u8], | |||||
| nodes: Slice, | |||||
| f: &mut impl FnMut(&'on_disk HgPath), | |||||
| ) -> Result<(), DirstateV2ParseError> { | |||||
| for node in read_slice::<Node>(on_disk, nodes)? { | |||||
| if let Some(state) = node.state()? { | |||||
| if state.is_tracked() { | |||||
| f(node.full_path(on_disk)?) | |||||
| } | |||||
| } | |||||
| recur(on_disk, node.children, f)? | |||||
| } | |||||
| Ok(()) | |||||
| } | |||||
| recur(on_disk, header.root, &mut f) | |||||
| } | |||||
| pub(super) fn write( | pub(super) fn write( | ||||
| dirstate_map: &mut DirstateMap, | dirstate_map: &mut DirstateMap, | ||||
| parents: DirstateParents, | parents: DirstateParents, | ||||
| ) -> Result<Vec<u8>, DirstateError> { | ) -> Result<Vec<u8>, DirstateError> { | ||||
| let header_len = std::mem::size_of::<Header>(); | let header_len = std::mem::size_of::<Header>(); | ||||
| // This ignores the space for paths, and for nodes without an entry. | // This ignores the space for paths, and for nodes without an entry. | ||||
| // TODO: better estimate? Skip the `Vec` and write to a file directly? | // TODO: better estimate? Skip the `Vec` and write to a file directly? | ||||
| // list_tracked_files.rs | // list_tracked_files.rs | ||||
| // | // | ||||
| // Copyright 2020 Antoine Cezar <antoine.cezar@octobus.net> | // Copyright 2020 Antoine Cezar <antoine.cezar@octobus.net> | ||||
| // | // | ||||
| // This software may be used and distributed according to the terms of the | // This software may be used and distributed according to the terms of the | ||||
| // GNU General Public License version 2 or any later version. | // GNU General Public License version 2 or any later version. | ||||
| use crate::dirstate::parsers::parse_dirstate_entries; | use crate::dirstate::parsers::parse_dirstate_entries; | ||||
| use crate::dirstate_tree::on_disk::for_each_tracked_path; | |||||
| use crate::errors::HgError; | use crate::errors::HgError; | ||||
| use crate::repo::Repo; | use crate::repo::Repo; | ||||
| use crate::revlog::changelog::Changelog; | use crate::revlog::changelog::Changelog; | ||||
| use crate::revlog::manifest::{Manifest, ManifestEntry}; | use crate::revlog::manifest::{Manifest, ManifestEntry}; | ||||
| use crate::revlog::node::Node; | use crate::revlog::node::Node; | ||||
| use crate::revlog::revlog::RevlogError; | use crate::revlog::revlog::RevlogError; | ||||
| use crate::utils::hg_path::HgPath; | use crate::utils::hg_path::HgPath; | ||||
| use crate::DirstateError; | |||||
| use rayon::prelude::*; | use rayon::prelude::*; | ||||
| /// List files under Mercurial control in the working directory | /// List files under Mercurial control in the working directory | ||||
| /// by reading the dirstate | /// by reading the dirstate | ||||
| pub struct Dirstate { | pub struct Dirstate { | ||||
| /// The `dirstate` content. | /// The `dirstate` content. | ||||
| content: Vec<u8>, | content: Vec<u8>, | ||||
| dirstate_v2: bool, | |||||
| } | } | ||||
| impl Dirstate { | impl Dirstate { | ||||
| pub fn new(repo: &Repo) -> Result<Self, HgError> { | pub fn new(repo: &Repo) -> Result<Self, HgError> { | ||||
| let content = repo.hg_vfs().read("dirstate")?; | Ok(Self { | ||||
| Ok(Self { content }) | content: repo.hg_vfs().read("dirstate")?, | ||||
| dirstate_v2: repo.has_dirstate_v2(), | |||||
| }) | |||||
| } | } | ||||
| pub fn tracked_files(&self) -> Result<Vec<&HgPath>, HgError> { | pub fn tracked_files(&self) -> Result<Vec<&HgPath>, DirstateError> { | ||||
| let mut files = Vec::new(); | let mut files = Vec::new(); | ||||
| if !self.content.is_empty() { | |||||
| if self.dirstate_v2 { | |||||
| for_each_tracked_path(&self.content, |path| files.push(path))? | |||||
| } else { | |||||
| let _parents = parse_dirstate_entries( | let _parents = parse_dirstate_entries( | ||||
| &self.content, | &self.content, | ||||
| |path, entry, _copy_source| { | |path, entry, _copy_source| { | ||||
| if entry.state.is_tracked() { | if entry.state.is_tracked() { | ||||
| files.push(path) | files.push(path) | ||||
| } | } | ||||
| Ok(()) | Ok(()) | ||||
| }, | }, | ||||
| )?; | )?; | ||||
| } | |||||
| } | |||||
| files.par_sort_unstable(); | files.par_sort_unstable(); | ||||
| Ok(files) | Ok(files) | ||||
| } | } | ||||
| } | } | ||||
| /// List files under Mercurial control at a given revision. | /// List files under Mercurial control at a given revision. | ||||
| pub fn list_rev_tracked_files( | pub fn list_rev_tracked_files( | ||||
| repo: &Repo, | repo: &Repo, | ||||
| /// For accessing the working copy | /// For accessing the working copy | ||||
| pub fn working_directory_vfs(&self) -> Vfs<'_> { | pub fn working_directory_vfs(&self) -> Vfs<'_> { | ||||
| Vfs { | Vfs { | ||||
| base: &self.working_directory, | base: &self.working_directory, | ||||
| } | } | ||||
| } | } | ||||
| pub fn has_dirstate_v2(&self) -> bool { | |||||
| self.requirements | |||||
| .contains(requirements::DIRSTATE_V2_REQUIREMENT) | |||||
| } | |||||
| pub fn dirstate_parents( | pub fn dirstate_parents( | ||||
| &self, | &self, | ||||
| ) -> Result<crate::dirstate::DirstateParents, HgError> { | ) -> Result<crate::dirstate::DirstateParents, HgError> { | ||||
| let dirstate = self.hg_vfs().mmap_open("dirstate")?; | let dirstate = self.hg_vfs().mmap_open("dirstate")?; | ||||
| let parents = | if dirstate.is_empty() { | ||||
| crate::dirstate::parsers::parse_dirstate_parents(&dirstate)?; | return Ok(crate::dirstate::DirstateParents::NULL); | ||||
| } | |||||
| let parents = if self.has_dirstate_v2() { | |||||
| crate::dirstate_tree::on_disk::parse_dirstate_parents(&dirstate)? | |||||
| } else { | |||||
| crate::dirstate::parsers::parse_dirstate_parents(&dirstate)? | |||||
| }; | |||||
| Ok(parents.clone()) | Ok(parents.clone()) | ||||
| } | } | ||||
| } | } | ||||
| impl Vfs<'_> { | impl Vfs<'_> { | ||||
| pub fn join(&self, relative_path: impl AsRef<Path>) -> PathBuf { | pub fn join(&self, relative_path: impl AsRef<Path>) -> PathBuf { | ||||
| self.base.join(relative_path) | self.base.join(relative_path) | ||||
| } | } | ||||
| /// rhg supports repository with or without these | /// rhg supports repository with or without these | ||||
| const SUPPORTED: &[&str] = &[ | const SUPPORTED: &[&str] = &[ | ||||
| "generaldelta", | "generaldelta", | ||||
| SHARED_REQUIREMENT, | SHARED_REQUIREMENT, | ||||
| SHARESAFE_REQUIREMENT, | SHARESAFE_REQUIREMENT, | ||||
| SPARSEREVLOG_REQUIREMENT, | SPARSEREVLOG_REQUIREMENT, | ||||
| RELATIVE_SHARED_REQUIREMENT, | RELATIVE_SHARED_REQUIREMENT, | ||||
| REVLOG_COMPRESSION_ZSTD, | REVLOG_COMPRESSION_ZSTD, | ||||
| DIRSTATE_V2_REQUIREMENT, | |||||
| // As of this writing everything rhg does is read-only. | // As of this writing everything rhg does is read-only. | ||||
| // When it starts writing to the repository, it’ll need to either keep the | // When it starts writing to the repository, it’ll need to either keep the | ||||
| // persistent nodemap up to date or remove this entry: | // persistent nodemap up to date or remove this entry: | ||||
| NODEMAP_REQUIREMENT, | NODEMAP_REQUIREMENT, | ||||
| ]; | ]; | ||||
| // Copied from mercurial/requirements.py: | // Copied from mercurial/requirements.py: | ||||
| pub(crate) const DIRSTATE_V2_REQUIREMENT: &str = "exp-dirstate-v2"; | |||||
| /// When narrowing is finalized and no longer subject to format changes, | /// When narrowing is finalized and no longer subject to format changes, | ||||
| /// we should move this to just "narrow" or similar. | /// we should move this to just "narrow" or similar. | ||||
| #[allow(unused)] | #[allow(unused)] | ||||
| pub(crate) const NARROW_REQUIREMENT: &str = "narrowhg-experimental"; | pub(crate) const NARROW_REQUIREMENT: &str = "narrowhg-experimental"; | ||||
| /// Enables sparse working directory usage | /// Enables sparse working directory usage | ||||
| #[allow(unused)] | #[allow(unused)] | ||||
| pub(crate) const SPARSE_REQUIREMENT: &str = "exp-sparse"; | pub(crate) const SPARSE_REQUIREMENT: &str = "exp-sparse"; | ||||
| // status.rs | // status.rs | ||||
| // | // | ||||
| // Copyright 2020, Georges Racinet <georges.racinets@octobus.net> | // Copyright 2020, Georges Racinet <georges.racinets@octobus.net> | ||||
| // | // | ||||
| // This software may be used and distributed according to the terms of the | // This software may be used and distributed according to the terms of the | ||||
| // GNU General Public License version 2 or any later version. | // GNU General Public License version 2 or any later version. | ||||
| use crate::error::CommandError; | use crate::error::CommandError; | ||||
| use crate::ui::Ui; | use crate::ui::Ui; | ||||
| use clap::{Arg, SubCommand}; | use clap::{Arg, SubCommand}; | ||||
| use hg; | use hg; | ||||
| use hg::dirstate_tree::dirstate_map::DirstateMap; | |||||
| use hg::errors::HgResultExt; | use hg::errors::HgResultExt; | ||||
| use hg::errors::IoResultExt; | use hg::errors::IoResultExt; | ||||
| use hg::matchers::AlwaysMatcher; | use hg::matchers::AlwaysMatcher; | ||||
| use hg::operations::cat; | use hg::operations::cat; | ||||
| use hg::repo::Repo; | use hg::repo::Repo; | ||||
| use hg::revlog::node::Node; | use hg::revlog::node::Node; | ||||
| use hg::utils::hg_path::{hg_path_to_os_string, HgPath}; | use hg::utils::hg_path::{hg_path_to_os_string, HgPath}; | ||||
| use hg::{DirstateMap, StatusError}; | use hg::StatusError; | ||||
| use hg::{HgPathCow, StatusOptions}; | use hg::{HgPathCow, StatusOptions}; | ||||
| use log::{info, warn}; | use log::{info, warn}; | ||||
| use std::convert::TryInto; | use std::convert::TryInto; | ||||
| use std::fs; | use std::fs; | ||||
| use std::io::BufReader; | use std::io::BufReader; | ||||
| use std::io::Read; | use std::io::Read; | ||||
| pub const HELP_TEXT: &str = " | pub const HELP_TEXT: &str = " | ||||
| if requested.is_empty() { | if requested.is_empty() { | ||||
| DEFAULT_DISPLAY_STATES | DEFAULT_DISPLAY_STATES | ||||
| } else { | } else { | ||||
| requested | requested | ||||
| } | } | ||||
| }; | }; | ||||
| let repo = invocation.repo?; | let repo = invocation.repo?; | ||||
| let mut dmap = DirstateMap::new(); | |||||
| let dirstate_data = | let dirstate_data = | ||||
| repo.hg_vfs().mmap_open("dirstate").io_not_found_as_none()?; | repo.hg_vfs().mmap_open("dirstate").io_not_found_as_none()?; | ||||
| let dirstate_data = match &dirstate_data { | let dirstate_data = match &dirstate_data { | ||||
| Some(mmap) => &**mmap, | Some(mmap) => &**mmap, | ||||
| None => b"", | None => b"", | ||||
| }; | }; | ||||
| let parents = dmap.read(dirstate_data)?; | let (mut dmap, parents) = if repo.has_dirstate_v2() { | ||||
| DirstateMap::new_v2(dirstate_data)? | |||||
| } else { | |||||
| DirstateMap::new_v1(dirstate_data)? | |||||
| }; | |||||
| let options = StatusOptions { | let options = StatusOptions { | ||||
| // TODO should be provided by the dirstate parsing and | // TODO should be provided by the dirstate parsing and | ||||
| // hence be stored on dmap. Using a value that assumes we aren't | // hence be stored on dmap. Using a value that assumes we aren't | ||||
| // below the time resolution granularity of the FS and the | // below the time resolution granularity of the FS and the | ||||
| // dirstate. | // dirstate. | ||||
| last_normal_time: 0, | last_normal_time: 0, | ||||
| // we're currently supporting file systems with exec flags only | // we're currently supporting file systems with exec flags only | ||||
| // anyway | // anyway | ||||
| check_exec: true, | check_exec: true, | ||||
| list_clean: display_states.clean, | list_clean: display_states.clean, | ||||
| list_unknown: display_states.unknown, | list_unknown: display_states.unknown, | ||||
| list_ignored: display_states.ignored, | list_ignored: display_states.ignored, | ||||
| collect_traversed_dirs: false, | collect_traversed_dirs: false, | ||||
| }; | }; | ||||
| let ignore_file = repo.working_directory_vfs().join(".hgignore"); // TODO hardcoded | let ignore_file = repo.working_directory_vfs().join(".hgignore"); // TODO hardcoded | ||||
| let (mut ds_status, pattern_warnings) = hg::status( | let (mut ds_status, pattern_warnings) = hg::dirstate_tree::status::status( | ||||
| &dmap, | &mut dmap, | ||||
| &AlwaysMatcher, | &AlwaysMatcher, | ||||
| repo.working_directory_path().to_owned(), | repo.working_directory_path().to_owned(), | ||||
| vec![ignore_file], | vec![ignore_file], | ||||
| options, | options, | ||||
| )?; | )?; | ||||
| if !pattern_warnings.is_empty() { | if !pattern_warnings.is_empty() { | ||||
| warn!("Pattern warnings: {:?}", &pattern_warnings); | warn!("Pattern warnings: {:?}", &pattern_warnings); | ||||
| } | } | ||||