Show More
@@ -1,75 +1,105 b'' | |||||
1 | // list_tracked_files.rs |
|
1 | // list_tracked_files.rs | |
2 | // |
|
2 | // | |
3 | // Copyright 2020 Antoine Cezar <antoine.cezar@octobus.net> |
|
3 | // Copyright 2020 Antoine Cezar <antoine.cezar@octobus.net> | |
4 | // |
|
4 | // | |
5 | // This software may be used and distributed according to the terms of the |
|
5 | // This software may be used and distributed according to the terms of the | |
6 | // GNU General Public License version 2 or any later version. |
|
6 | // GNU General Public License version 2 or any later version. | |
7 |
|
7 | |||
8 | use std::path::PathBuf; |
|
8 | use std::path::PathBuf; | |
9 |
|
9 | |||
10 | use crate::repo::Repo; |
|
10 | use crate::repo::Repo; | |
11 | use crate::revlog::changelog::Changelog; |
|
11 | use crate::revlog::changelog::Changelog; | |
12 | use crate::revlog::manifest::Manifest; |
|
12 | use crate::revlog::manifest::Manifest; | |
13 | use crate::revlog::path_encode::path_encode; |
|
13 | use crate::revlog::path_encode::path_encode; | |
14 | use crate::revlog::revlog::Revlog; |
|
14 | use crate::revlog::revlog::Revlog; | |
15 | use crate::revlog::revlog::RevlogError; |
|
15 | use crate::revlog::revlog::RevlogError; | |
16 | use crate::revlog::Node; |
|
16 | use crate::revlog::Node; | |
17 | use crate::utils::files::get_path_from_bytes; |
|
17 | use crate::utils::files::get_path_from_bytes; | |
18 | use crate::utils::hg_path::{HgPath, HgPathBuf}; |
|
18 | use crate::utils::hg_path::{HgPath, HgPathBuf}; | |
19 |
|
19 | |||
|
20 | pub struct CatOutput { | |||
|
21 | /// Whether any file in the manifest matched the paths given as CLI | |||
|
22 | /// arguments | |||
|
23 | pub found_any: bool, | |||
|
24 | /// The contents of matching files, in manifest order | |||
|
25 | pub concatenated: Vec<u8>, | |||
|
26 | /// Which of the CLI arguments did not match any manifest file | |||
|
27 | pub missing: Vec<HgPathBuf>, | |||
|
28 | /// The node ID that the given revset was resolved to | |||
|
29 | pub node: Node, | |||
|
30 | } | |||
|
31 | ||||
20 | const METADATA_DELIMITER: [u8; 2] = [b'\x01', b'\n']; |
|
32 | const METADATA_DELIMITER: [u8; 2] = [b'\x01', b'\n']; | |
21 |
|
33 | |||
22 | /// List files under Mercurial control at a given revision. |
|
34 | /// Output the given revision of files | |
23 | /// |
|
35 | /// | |
24 | /// * `root`: Repository root |
|
36 | /// * `root`: Repository root | |
25 | /// * `rev`: The revision to cat the files from. |
|
37 | /// * `rev`: The revision to cat the files from. | |
26 | /// * `files`: The files to output. |
|
38 | /// * `files`: The files to output. | |
27 | pub fn cat( |
|
39 | pub fn cat<'a>( | |
28 | repo: &Repo, |
|
40 | repo: &Repo, | |
29 | revset: &str, |
|
41 | revset: &str, | |
30 | files: &[HgPathBuf], |
|
42 | files: &'a [HgPathBuf], | |
31 |
) -> Result< |
|
43 | ) -> Result<CatOutput, RevlogError> { | |
32 | let rev = crate::revset::resolve_single(revset, repo)?; |
|
44 | let rev = crate::revset::resolve_single(revset, repo)?; | |
33 | let changelog = Changelog::open(repo)?; |
|
45 | let changelog = Changelog::open(repo)?; | |
34 | let manifest = Manifest::open(repo)?; |
|
46 | let manifest = Manifest::open(repo)?; | |
35 | let changelog_entry = changelog.get_rev(rev)?; |
|
47 | let changelog_entry = changelog.get_rev(rev)?; | |
|
48 | let node = *changelog | |||
|
49 | .node_from_rev(rev) | |||
|
50 | .expect("should succeed when changelog.get_rev did"); | |||
36 | let manifest_node = |
|
51 | let manifest_node = | |
37 | Node::from_hex_for_repo(&changelog_entry.manifest_node()?)?; |
|
52 | Node::from_hex_for_repo(&changelog_entry.manifest_node()?)?; | |
38 | let manifest_entry = manifest.get_node(manifest_node.into())?; |
|
53 | let manifest_entry = manifest.get_node(manifest_node.into())?; | |
39 | let mut bytes = vec![]; |
|
54 | let mut bytes = vec![]; | |
|
55 | let mut matched = vec![false; files.len()]; | |||
|
56 | let mut found_any = false; | |||
40 |
|
57 | |||
41 | for (manifest_file, node_bytes) in manifest_entry.files_with_nodes() { |
|
58 | for (manifest_file, node_bytes) in manifest_entry.files_with_nodes() { | |
42 | for cat_file in files.iter() { |
|
59 | for (cat_file, is_matched) in files.iter().zip(&mut matched) { | |
43 | if cat_file.as_bytes() == manifest_file.as_bytes() { |
|
60 | if cat_file.as_bytes() == manifest_file.as_bytes() { | |
|
61 | *is_matched = true; | |||
|
62 | found_any = true; | |||
44 | let index_path = store_path(manifest_file, b".i"); |
|
63 | let index_path = store_path(manifest_file, b".i"); | |
45 | let data_path = store_path(manifest_file, b".d"); |
|
64 | let data_path = store_path(manifest_file, b".d"); | |
46 |
|
65 | |||
47 | let file_log = |
|
66 | let file_log = | |
48 | Revlog::open(repo, &index_path, Some(&data_path))?; |
|
67 | Revlog::open(repo, &index_path, Some(&data_path))?; | |
49 | let file_node = Node::from_hex_for_repo(node_bytes)?; |
|
68 | let file_node = Node::from_hex_for_repo(node_bytes)?; | |
50 | let file_rev = file_log.get_node_rev(file_node.into())?; |
|
69 | let file_rev = file_log.get_node_rev(file_node.into())?; | |
51 | let data = file_log.get_rev_data(file_rev)?; |
|
70 | let data = file_log.get_rev_data(file_rev)?; | |
52 | if data.starts_with(&METADATA_DELIMITER) { |
|
71 | if data.starts_with(&METADATA_DELIMITER) { | |
53 | let end_delimiter_position = data |
|
72 | let end_delimiter_position = data | |
54 | [METADATA_DELIMITER.len()..] |
|
73 | [METADATA_DELIMITER.len()..] | |
55 | .windows(METADATA_DELIMITER.len()) |
|
74 | .windows(METADATA_DELIMITER.len()) | |
56 | .position(|bytes| bytes == METADATA_DELIMITER); |
|
75 | .position(|bytes| bytes == METADATA_DELIMITER); | |
57 | if let Some(position) = end_delimiter_position { |
|
76 | if let Some(position) = end_delimiter_position { | |
58 | let offset = METADATA_DELIMITER.len() * 2; |
|
77 | let offset = METADATA_DELIMITER.len() * 2; | |
59 | bytes.extend(data[position + offset..].iter()); |
|
78 | bytes.extend(data[position + offset..].iter()); | |
60 | } |
|
79 | } | |
61 | } else { |
|
80 | } else { | |
62 | bytes.extend(data); |
|
81 | bytes.extend(data); | |
63 | } |
|
82 | } | |
64 | } |
|
83 | } | |
65 | } |
|
84 | } | |
66 | } |
|
85 | } | |
67 |
|
86 | |||
68 | Ok(bytes) |
|
87 | let missing: Vec<_> = files | |
|
88 | .iter() | |||
|
89 | .zip(&matched) | |||
|
90 | .filter(|pair| !*pair.1) | |||
|
91 | .map(|pair| pair.0.clone()) | |||
|
92 | .collect(); | |||
|
93 | Ok(CatOutput { | |||
|
94 | found_any, | |||
|
95 | concatenated: bytes, | |||
|
96 | missing, | |||
|
97 | node, | |||
|
98 | }) | |||
69 | } |
|
99 | } | |
70 |
|
100 | |||
71 | fn store_path(hg_path: &HgPath, suffix: &[u8]) -> PathBuf { |
|
101 | fn store_path(hg_path: &HgPath, suffix: &[u8]) -> PathBuf { | |
72 | let encoded_bytes = |
|
102 | let encoded_bytes = | |
73 | path_encode(&[b"data/", hg_path.as_bytes(), suffix].concat()); |
|
103 | path_encode(&[b"data/", hg_path.as_bytes(), suffix].concat()); | |
74 | get_path_from_bytes(&encoded_bytes).into() |
|
104 | get_path_from_bytes(&encoded_bytes).into() | |
75 | } |
|
105 | } |
@@ -1,12 +1,12 b'' | |||||
1 | //! A distinction is made between operations and commands. |
|
1 | //! A distinction is made between operations and commands. | |
2 | //! An operation is what can be done whereas a command is what is exposed by |
|
2 | //! An operation is what can be done whereas a command is what is exposed by | |
3 | //! the cli. A single command can use several operations to achieve its goal. |
|
3 | //! the cli. A single command can use several operations to achieve its goal. | |
4 |
|
4 | |||
5 | mod cat; |
|
5 | mod cat; | |
6 | mod debugdata; |
|
6 | mod debugdata; | |
7 | mod dirstate_status; |
|
7 | mod dirstate_status; | |
8 | mod list_tracked_files; |
|
8 | mod list_tracked_files; | |
9 | pub use cat::cat; |
|
9 | pub use cat::{cat, CatOutput}; | |
10 | pub use debugdata::{debug_data, DebugDataKind}; |
|
10 | pub use debugdata::{debug_data, DebugDataKind}; | |
11 | pub use list_tracked_files::Dirstate; |
|
11 | pub use list_tracked_files::Dirstate; | |
12 | pub use list_tracked_files::{list_rev_tracked_files, FilesForRev}; |
|
12 | pub use list_tracked_files::{list_rev_tracked_files, FilesForRev}; |
@@ -1,61 +1,65 b'' | |||||
1 | use crate::errors::HgError; |
|
1 | use crate::errors::HgError; | |
2 | use crate::repo::Repo; |
|
2 | use crate::repo::Repo; | |
3 | use crate::revlog::revlog::{Revlog, RevlogError}; |
|
3 | use crate::revlog::revlog::{Revlog, RevlogError}; | |
4 | use crate::revlog::NodePrefix; |
|
|||
5 | use crate::revlog::Revision; |
|
4 | use crate::revlog::Revision; | |
|
5 | use crate::revlog::{Node, NodePrefix}; | |||
6 |
|
6 | |||
7 | /// A specialized `Revlog` to work with `changelog` data format. |
|
7 | /// A specialized `Revlog` to work with `changelog` data format. | |
8 | pub struct Changelog { |
|
8 | pub struct Changelog { | |
9 | /// The generic `revlog` format. |
|
9 | /// The generic `revlog` format. | |
10 | pub(crate) revlog: Revlog, |
|
10 | pub(crate) revlog: Revlog, | |
11 | } |
|
11 | } | |
12 |
|
12 | |||
13 | impl Changelog { |
|
13 | impl Changelog { | |
14 | /// Open the `changelog` of a repository given by its root. |
|
14 | /// Open the `changelog` of a repository given by its root. | |
15 | pub fn open(repo: &Repo) -> Result<Self, RevlogError> { |
|
15 | pub fn open(repo: &Repo) -> Result<Self, RevlogError> { | |
16 | let revlog = Revlog::open(repo, "00changelog.i", None)?; |
|
16 | let revlog = Revlog::open(repo, "00changelog.i", None)?; | |
17 | Ok(Self { revlog }) |
|
17 | Ok(Self { revlog }) | |
18 | } |
|
18 | } | |
19 |
|
19 | |||
20 | /// Return the `ChangelogEntry` a given node id. |
|
20 | /// Return the `ChangelogEntry` a given node id. | |
21 | pub fn get_node( |
|
21 | pub fn get_node( | |
22 | &self, |
|
22 | &self, | |
23 | node: NodePrefix, |
|
23 | node: NodePrefix, | |
24 | ) -> Result<ChangelogEntry, RevlogError> { |
|
24 | ) -> Result<ChangelogEntry, RevlogError> { | |
25 | let rev = self.revlog.get_node_rev(node)?; |
|
25 | let rev = self.revlog.get_node_rev(node)?; | |
26 | self.get_rev(rev) |
|
26 | self.get_rev(rev) | |
27 | } |
|
27 | } | |
28 |
|
28 | |||
29 | /// Return the `ChangelogEntry` of a given node revision. |
|
29 | /// Return the `ChangelogEntry` of a given node revision. | |
30 | pub fn get_rev( |
|
30 | pub fn get_rev( | |
31 | &self, |
|
31 | &self, | |
32 | rev: Revision, |
|
32 | rev: Revision, | |
33 | ) -> Result<ChangelogEntry, RevlogError> { |
|
33 | ) -> Result<ChangelogEntry, RevlogError> { | |
34 | let bytes = self.revlog.get_rev_data(rev)?; |
|
34 | let bytes = self.revlog.get_rev_data(rev)?; | |
35 | Ok(ChangelogEntry { bytes }) |
|
35 | Ok(ChangelogEntry { bytes }) | |
36 | } |
|
36 | } | |
|
37 | ||||
|
38 | pub fn node_from_rev(&self, rev: Revision) -> Option<&Node> { | |||
|
39 | Some(self.revlog.index.get_entry(rev)?.hash()) | |||
|
40 | } | |||
37 | } |
|
41 | } | |
38 |
|
42 | |||
39 | /// `Changelog` entry which knows how to interpret the `changelog` data bytes. |
|
43 | /// `Changelog` entry which knows how to interpret the `changelog` data bytes. | |
40 | #[derive(Debug)] |
|
44 | #[derive(Debug)] | |
41 | pub struct ChangelogEntry { |
|
45 | pub struct ChangelogEntry { | |
42 | /// The data bytes of the `changelog` entry. |
|
46 | /// The data bytes of the `changelog` entry. | |
43 | bytes: Vec<u8>, |
|
47 | bytes: Vec<u8>, | |
44 | } |
|
48 | } | |
45 |
|
49 | |||
46 | impl ChangelogEntry { |
|
50 | impl ChangelogEntry { | |
47 | /// Return an iterator over the lines of the entry. |
|
51 | /// Return an iterator over the lines of the entry. | |
48 | pub fn lines(&self) -> impl Iterator<Item = &[u8]> { |
|
52 | pub fn lines(&self) -> impl Iterator<Item = &[u8]> { | |
49 | self.bytes |
|
53 | self.bytes | |
50 | .split(|b| b == &b'\n') |
|
54 | .split(|b| b == &b'\n') | |
51 | .filter(|line| !line.is_empty()) |
|
55 | .filter(|line| !line.is_empty()) | |
52 | } |
|
56 | } | |
53 |
|
57 | |||
54 | /// Return the node id of the `manifest` referenced by this `changelog` |
|
58 | /// Return the node id of the `manifest` referenced by this `changelog` | |
55 | /// entry. |
|
59 | /// entry. | |
56 | pub fn manifest_node(&self) -> Result<&[u8], RevlogError> { |
|
60 | pub fn manifest_node(&self) -> Result<&[u8], RevlogError> { | |
57 | self.lines() |
|
61 | self.lines() | |
58 | .next() |
|
62 | .next() | |
59 | .ok_or_else(|| HgError::corrupted("empty changelog entry").into()) |
|
63 | .ok_or_else(|| HgError::corrupted("empty changelog entry").into()) | |
60 | } |
|
64 | } | |
61 | } |
|
65 | } |
@@ -1,405 +1,415 b'' | |||||
1 | // Copyright 2019-2020 Georges Racinet <georges.racinet@octobus.net> |
|
1 | // Copyright 2019-2020 Georges Racinet <georges.racinet@octobus.net> | |
2 | // |
|
2 | // | |
3 | // This software may be used and distributed according to the terms of the |
|
3 | // This software may be used and distributed according to the terms of the | |
4 | // GNU General Public License version 2 or any later version. |
|
4 | // GNU General Public License version 2 or any later version. | |
5 |
|
5 | |||
6 | //! Definitions and utilities for Revision nodes |
|
6 | //! Definitions and utilities for Revision nodes | |
7 | //! |
|
7 | //! | |
8 | //! In Mercurial code base, it is customary to call "a node" the binary SHA |
|
8 | //! In Mercurial code base, it is customary to call "a node" the binary SHA | |
9 | //! of a revision. |
|
9 | //! of a revision. | |
10 |
|
10 | |||
11 | use crate::errors::HgError; |
|
11 | use crate::errors::HgError; | |
12 | use bytes_cast::BytesCast; |
|
12 | use bytes_cast::BytesCast; | |
13 | use std::convert::{TryFrom, TryInto}; |
|
13 | use std::convert::{TryFrom, TryInto}; | |
14 | use std::fmt; |
|
14 | use std::fmt; | |
15 |
|
15 | |||
16 | /// The length in bytes of a `Node` |
|
16 | /// The length in bytes of a `Node` | |
17 | /// |
|
17 | /// | |
18 | /// This constant is meant to ease refactors of this module, and |
|
18 | /// This constant is meant to ease refactors of this module, and | |
19 | /// are private so that calling code does not expect all nodes have |
|
19 | /// are private so that calling code does not expect all nodes have | |
20 | /// the same size, should we support several formats concurrently in |
|
20 | /// the same size, should we support several formats concurrently in | |
21 | /// the future. |
|
21 | /// the future. | |
22 | pub const NODE_BYTES_LENGTH: usize = 20; |
|
22 | pub const NODE_BYTES_LENGTH: usize = 20; | |
23 |
|
23 | |||
24 | /// Id of the null node. |
|
24 | /// Id of the null node. | |
25 | /// |
|
25 | /// | |
26 | /// Used to indicate the absence of node. |
|
26 | /// Used to indicate the absence of node. | |
27 | pub const NULL_NODE_ID: [u8; NODE_BYTES_LENGTH] = [0u8; NODE_BYTES_LENGTH]; |
|
27 | pub const NULL_NODE_ID: [u8; NODE_BYTES_LENGTH] = [0u8; NODE_BYTES_LENGTH]; | |
28 |
|
28 | |||
29 | /// The length in bytes of a `Node` |
|
29 | /// The length in bytes of a `Node` | |
30 | /// |
|
30 | /// | |
31 | /// see also `NODES_BYTES_LENGTH` about it being private. |
|
31 | /// see also `NODES_BYTES_LENGTH` about it being private. | |
32 | const NODE_NYBBLES_LENGTH: usize = 2 * NODE_BYTES_LENGTH; |
|
32 | const NODE_NYBBLES_LENGTH: usize = 2 * NODE_BYTES_LENGTH; | |
33 |
|
33 | |||
|
34 | /// Default for UI presentation | |||
|
35 | const SHORT_PREFIX_DEFAULT_NYBBLES_LENGTH: u8 = 12; | |||
|
36 | ||||
34 | /// Private alias for readability and to ease future change |
|
37 | /// Private alias for readability and to ease future change | |
35 | type NodeData = [u8; NODE_BYTES_LENGTH]; |
|
38 | type NodeData = [u8; NODE_BYTES_LENGTH]; | |
36 |
|
39 | |||
37 | /// Binary revision SHA |
|
40 | /// Binary revision SHA | |
38 | /// |
|
41 | /// | |
39 | /// ## Future changes of hash size |
|
42 | /// ## Future changes of hash size | |
40 | /// |
|
43 | /// | |
41 | /// To accomodate future changes of hash size, Rust callers |
|
44 | /// To accomodate future changes of hash size, Rust callers | |
42 | /// should use the conversion methods at the boundaries (FFI, actual |
|
45 | /// should use the conversion methods at the boundaries (FFI, actual | |
43 | /// computation of hashes and I/O) only, and only if required. |
|
46 | /// computation of hashes and I/O) only, and only if required. | |
44 | /// |
|
47 | /// | |
45 | /// All other callers outside of unit tests should just handle `Node` values |
|
48 | /// All other callers outside of unit tests should just handle `Node` values | |
46 | /// and never make any assumption on the actual length, using [`nybbles_len`] |
|
49 | /// and never make any assumption on the actual length, using [`nybbles_len`] | |
47 | /// if they need a loop boundary. |
|
50 | /// if they need a loop boundary. | |
48 | /// |
|
51 | /// | |
49 | /// All methods that create a `Node` either take a type that enforces |
|
52 | /// All methods that create a `Node` either take a type that enforces | |
50 | /// the size or return an error at runtime. |
|
53 | /// the size or return an error at runtime. | |
51 | /// |
|
54 | /// | |
52 | /// [`nybbles_len`]: #method.nybbles_len |
|
55 | /// [`nybbles_len`]: #method.nybbles_len | |
53 | #[derive(Copy, Clone, Debug, PartialEq, BytesCast, derive_more::From)] |
|
56 | #[derive(Copy, Clone, Debug, PartialEq, BytesCast, derive_more::From)] | |
54 | #[repr(transparent)] |
|
57 | #[repr(transparent)] | |
55 | pub struct Node { |
|
58 | pub struct Node { | |
56 | data: NodeData, |
|
59 | data: NodeData, | |
57 | } |
|
60 | } | |
58 |
|
61 | |||
59 | /// The node value for NULL_REVISION |
|
62 | /// The node value for NULL_REVISION | |
60 | pub const NULL_NODE: Node = Node { |
|
63 | pub const NULL_NODE: Node = Node { | |
61 | data: [0; NODE_BYTES_LENGTH], |
|
64 | data: [0; NODE_BYTES_LENGTH], | |
62 | }; |
|
65 | }; | |
63 |
|
66 | |||
64 | /// Return an error if the slice has an unexpected length |
|
67 | /// Return an error if the slice has an unexpected length | |
65 | impl<'a> TryFrom<&'a [u8]> for &'a Node { |
|
68 | impl<'a> TryFrom<&'a [u8]> for &'a Node { | |
66 | type Error = (); |
|
69 | type Error = (); | |
67 |
|
70 | |||
68 | #[inline] |
|
71 | #[inline] | |
69 | fn try_from(bytes: &'a [u8]) -> Result<Self, Self::Error> { |
|
72 | fn try_from(bytes: &'a [u8]) -> Result<Self, Self::Error> { | |
70 | match Node::from_bytes(bytes) { |
|
73 | match Node::from_bytes(bytes) { | |
71 | Ok((node, rest)) if rest.is_empty() => Ok(node), |
|
74 | Ok((node, rest)) if rest.is_empty() => Ok(node), | |
72 | _ => Err(()), |
|
75 | _ => Err(()), | |
73 | } |
|
76 | } | |
74 | } |
|
77 | } | |
75 | } |
|
78 | } | |
76 |
|
79 | |||
77 | /// Return an error if the slice has an unexpected length |
|
80 | /// Return an error if the slice has an unexpected length | |
78 | impl TryFrom<&'_ [u8]> for Node { |
|
81 | impl TryFrom<&'_ [u8]> for Node { | |
79 | type Error = std::array::TryFromSliceError; |
|
82 | type Error = std::array::TryFromSliceError; | |
80 |
|
83 | |||
81 | #[inline] |
|
84 | #[inline] | |
82 | fn try_from(bytes: &'_ [u8]) -> Result<Self, Self::Error> { |
|
85 | fn try_from(bytes: &'_ [u8]) -> Result<Self, Self::Error> { | |
83 | let data = bytes.try_into()?; |
|
86 | let data = bytes.try_into()?; | |
84 | Ok(Self { data }) |
|
87 | Ok(Self { data }) | |
85 | } |
|
88 | } | |
86 | } |
|
89 | } | |
87 |
|
90 | |||
88 | impl From<&'_ NodeData> for Node { |
|
91 | impl From<&'_ NodeData> for Node { | |
89 | #[inline] |
|
92 | #[inline] | |
90 | fn from(data: &'_ NodeData) -> Self { |
|
93 | fn from(data: &'_ NodeData) -> Self { | |
91 | Self { data: *data } |
|
94 | Self { data: *data } | |
92 | } |
|
95 | } | |
93 | } |
|
96 | } | |
94 |
|
97 | |||
95 | impl fmt::LowerHex for Node { |
|
98 | impl fmt::LowerHex for Node { | |
96 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
|
99 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | |
97 | for &byte in &self.data { |
|
100 | for &byte in &self.data { | |
98 | write!(f, "{:02x}", byte)? |
|
101 | write!(f, "{:02x}", byte)? | |
99 | } |
|
102 | } | |
100 | Ok(()) |
|
103 | Ok(()) | |
101 | } |
|
104 | } | |
102 | } |
|
105 | } | |
103 |
|
106 | |||
104 | #[derive(Debug)] |
|
107 | #[derive(Debug)] | |
105 | pub struct FromHexError; |
|
108 | pub struct FromHexError; | |
106 |
|
109 | |||
107 | /// Low level utility function, also for prefixes |
|
110 | /// Low level utility function, also for prefixes | |
108 | fn get_nybble(s: &[u8], i: usize) -> u8 { |
|
111 | fn get_nybble(s: &[u8], i: usize) -> u8 { | |
109 | if i % 2 == 0 { |
|
112 | if i % 2 == 0 { | |
110 | s[i / 2] >> 4 |
|
113 | s[i / 2] >> 4 | |
111 | } else { |
|
114 | } else { | |
112 | s[i / 2] & 0x0f |
|
115 | s[i / 2] & 0x0f | |
113 | } |
|
116 | } | |
114 | } |
|
117 | } | |
115 |
|
118 | |||
116 | impl Node { |
|
119 | impl Node { | |
117 | /// Retrieve the `i`th half-byte of the binary data. |
|
120 | /// Retrieve the `i`th half-byte of the binary data. | |
118 | /// |
|
121 | /// | |
119 | /// This is also the `i`th hexadecimal digit in numeric form, |
|
122 | /// This is also the `i`th hexadecimal digit in numeric form, | |
120 | /// also called a [nybble](https://en.wikipedia.org/wiki/Nibble). |
|
123 | /// also called a [nybble](https://en.wikipedia.org/wiki/Nibble). | |
121 | pub fn get_nybble(&self, i: usize) -> u8 { |
|
124 | pub fn get_nybble(&self, i: usize) -> u8 { | |
122 | get_nybble(&self.data, i) |
|
125 | get_nybble(&self.data, i) | |
123 | } |
|
126 | } | |
124 |
|
127 | |||
125 | /// Length of the data, in nybbles |
|
128 | /// Length of the data, in nybbles | |
126 | pub fn nybbles_len(&self) -> usize { |
|
129 | pub fn nybbles_len(&self) -> usize { | |
127 | // public exposure as an instance method only, so that we can |
|
130 | // public exposure as an instance method only, so that we can | |
128 | // easily support several sizes of hashes if needed in the future. |
|
131 | // easily support several sizes of hashes if needed in the future. | |
129 | NODE_NYBBLES_LENGTH |
|
132 | NODE_NYBBLES_LENGTH | |
130 | } |
|
133 | } | |
131 |
|
134 | |||
132 | /// Convert from hexadecimal string representation |
|
135 | /// Convert from hexadecimal string representation | |
133 | /// |
|
136 | /// | |
134 | /// Exact length is required. |
|
137 | /// Exact length is required. | |
135 | /// |
|
138 | /// | |
136 | /// To be used in FFI and I/O only, in order to facilitate future |
|
139 | /// To be used in FFI and I/O only, in order to facilitate future | |
137 | /// changes of hash format. |
|
140 | /// changes of hash format. | |
138 | pub fn from_hex(hex: impl AsRef<[u8]>) -> Result<Node, FromHexError> { |
|
141 | pub fn from_hex(hex: impl AsRef<[u8]>) -> Result<Node, FromHexError> { | |
139 | let prefix = NodePrefix::from_hex(hex)?; |
|
142 | let prefix = NodePrefix::from_hex(hex)?; | |
140 | if prefix.nybbles_len() == NODE_NYBBLES_LENGTH { |
|
143 | if prefix.nybbles_len() == NODE_NYBBLES_LENGTH { | |
141 | Ok(Self { data: prefix.data }) |
|
144 | Ok(Self { data: prefix.data }) | |
142 | } else { |
|
145 | } else { | |
143 | Err(FromHexError) |
|
146 | Err(FromHexError) | |
144 | } |
|
147 | } | |
145 | } |
|
148 | } | |
146 |
|
149 | |||
147 | /// `from_hex`, but for input from an internal file of the repository such |
|
150 | /// `from_hex`, but for input from an internal file of the repository such | |
148 | /// as a changelog or manifest entry. |
|
151 | /// as a changelog or manifest entry. | |
149 | /// |
|
152 | /// | |
150 | /// An error is treated as repository corruption. |
|
153 | /// An error is treated as repository corruption. | |
151 | pub fn from_hex_for_repo(hex: impl AsRef<[u8]>) -> Result<Node, HgError> { |
|
154 | pub fn from_hex_for_repo(hex: impl AsRef<[u8]>) -> Result<Node, HgError> { | |
152 | Self::from_hex(hex.as_ref()).map_err(|FromHexError| { |
|
155 | Self::from_hex(hex.as_ref()).map_err(|FromHexError| { | |
153 | HgError::CorruptedRepository(format!( |
|
156 | HgError::CorruptedRepository(format!( | |
154 | "Expected a full hexadecimal node ID, found {}", |
|
157 | "Expected a full hexadecimal node ID, found {}", | |
155 | String::from_utf8_lossy(hex.as_ref()) |
|
158 | String::from_utf8_lossy(hex.as_ref()) | |
156 | )) |
|
159 | )) | |
157 | }) |
|
160 | }) | |
158 | } |
|
161 | } | |
159 |
|
162 | |||
160 | /// Provide access to binary data |
|
163 | /// Provide access to binary data | |
161 | /// |
|
164 | /// | |
162 | /// This is needed by FFI layers, for instance to return expected |
|
165 | /// This is needed by FFI layers, for instance to return expected | |
163 | /// binary values to Python. |
|
166 | /// binary values to Python. | |
164 | pub fn as_bytes(&self) -> &[u8] { |
|
167 | pub fn as_bytes(&self) -> &[u8] { | |
165 | &self.data |
|
168 | &self.data | |
166 | } |
|
169 | } | |
|
170 | ||||
|
171 | pub fn short(&self) -> NodePrefix { | |||
|
172 | NodePrefix { | |||
|
173 | nybbles_len: SHORT_PREFIX_DEFAULT_NYBBLES_LENGTH, | |||
|
174 | data: self.data, | |||
|
175 | } | |||
|
176 | } | |||
167 | } |
|
177 | } | |
168 |
|
178 | |||
169 | /// The beginning of a binary revision SHA. |
|
179 | /// The beginning of a binary revision SHA. | |
170 | /// |
|
180 | /// | |
171 | /// Since it can potentially come from an hexadecimal representation with |
|
181 | /// Since it can potentially come from an hexadecimal representation with | |
172 | /// odd length, it needs to carry around whether the last 4 bits are relevant |
|
182 | /// odd length, it needs to carry around whether the last 4 bits are relevant | |
173 | /// or not. |
|
183 | /// or not. | |
174 | #[derive(Debug, PartialEq, Copy, Clone)] |
|
184 | #[derive(Debug, PartialEq, Copy, Clone)] | |
175 | pub struct NodePrefix { |
|
185 | pub struct NodePrefix { | |
176 | /// In `1..=NODE_NYBBLES_LENGTH` |
|
186 | /// In `1..=NODE_NYBBLES_LENGTH` | |
177 | nybbles_len: u8, |
|
187 | nybbles_len: u8, | |
178 | /// The first `4 * length_in_nybbles` bits are used (considering bits |
|
188 | /// The first `4 * length_in_nybbles` bits are used (considering bits | |
179 | /// within a bytes in big-endian: most significant first), the rest |
|
189 | /// within a bytes in big-endian: most significant first), the rest | |
180 | /// are zero. |
|
190 | /// are zero. | |
181 | data: NodeData, |
|
191 | data: NodeData, | |
182 | } |
|
192 | } | |
183 |
|
193 | |||
184 | impl NodePrefix { |
|
194 | impl NodePrefix { | |
185 | /// Convert from hexadecimal string representation |
|
195 | /// Convert from hexadecimal string representation | |
186 | /// |
|
196 | /// | |
187 | /// Similarly to `hex::decode`, can be used with Unicode string types |
|
197 | /// Similarly to `hex::decode`, can be used with Unicode string types | |
188 | /// (`String`, `&str`) as well as bytes. |
|
198 | /// (`String`, `&str`) as well as bytes. | |
189 | /// |
|
199 | /// | |
190 | /// To be used in FFI and I/O only, in order to facilitate future |
|
200 | /// To be used in FFI and I/O only, in order to facilitate future | |
191 | /// changes of hash format. |
|
201 | /// changes of hash format. | |
192 | pub fn from_hex(hex: impl AsRef<[u8]>) -> Result<Self, FromHexError> { |
|
202 | pub fn from_hex(hex: impl AsRef<[u8]>) -> Result<Self, FromHexError> { | |
193 | let hex = hex.as_ref(); |
|
203 | let hex = hex.as_ref(); | |
194 | let len = hex.len(); |
|
204 | let len = hex.len(); | |
195 | if len > NODE_NYBBLES_LENGTH || len == 0 { |
|
205 | if len > NODE_NYBBLES_LENGTH || len == 0 { | |
196 | return Err(FromHexError); |
|
206 | return Err(FromHexError); | |
197 | } |
|
207 | } | |
198 |
|
208 | |||
199 | let mut data = [0; NODE_BYTES_LENGTH]; |
|
209 | let mut data = [0; NODE_BYTES_LENGTH]; | |
200 | let mut nybbles_len = 0; |
|
210 | let mut nybbles_len = 0; | |
201 | for &ascii_byte in hex { |
|
211 | for &ascii_byte in hex { | |
202 | let nybble = match char::from(ascii_byte).to_digit(16) { |
|
212 | let nybble = match char::from(ascii_byte).to_digit(16) { | |
203 | Some(digit) => digit as u8, |
|
213 | Some(digit) => digit as u8, | |
204 | None => return Err(FromHexError), |
|
214 | None => return Err(FromHexError), | |
205 | }; |
|
215 | }; | |
206 | // Fill in the upper half of a byte first, then the lower half. |
|
216 | // Fill in the upper half of a byte first, then the lower half. | |
207 | let shift = if nybbles_len % 2 == 0 { 4 } else { 0 }; |
|
217 | let shift = if nybbles_len % 2 == 0 { 4 } else { 0 }; | |
208 | data[nybbles_len as usize / 2] |= nybble << shift; |
|
218 | data[nybbles_len as usize / 2] |= nybble << shift; | |
209 | nybbles_len += 1; |
|
219 | nybbles_len += 1; | |
210 | } |
|
220 | } | |
211 | Ok(Self { data, nybbles_len }) |
|
221 | Ok(Self { data, nybbles_len }) | |
212 | } |
|
222 | } | |
213 |
|
223 | |||
214 | pub fn nybbles_len(&self) -> usize { |
|
224 | pub fn nybbles_len(&self) -> usize { | |
215 | self.nybbles_len as _ |
|
225 | self.nybbles_len as _ | |
216 | } |
|
226 | } | |
217 |
|
227 | |||
218 | pub fn is_prefix_of(&self, node: &Node) -> bool { |
|
228 | pub fn is_prefix_of(&self, node: &Node) -> bool { | |
219 | let full_bytes = self.nybbles_len() / 2; |
|
229 | let full_bytes = self.nybbles_len() / 2; | |
220 | if self.data[..full_bytes] != node.data[..full_bytes] { |
|
230 | if self.data[..full_bytes] != node.data[..full_bytes] { | |
221 | return false; |
|
231 | return false; | |
222 | } |
|
232 | } | |
223 | if self.nybbles_len() % 2 == 0 { |
|
233 | if self.nybbles_len() % 2 == 0 { | |
224 | return true; |
|
234 | return true; | |
225 | } |
|
235 | } | |
226 | let last = self.nybbles_len() - 1; |
|
236 | let last = self.nybbles_len() - 1; | |
227 | self.get_nybble(last) == node.get_nybble(last) |
|
237 | self.get_nybble(last) == node.get_nybble(last) | |
228 | } |
|
238 | } | |
229 |
|
239 | |||
230 | /// Retrieve the `i`th half-byte from the prefix. |
|
240 | /// Retrieve the `i`th half-byte from the prefix. | |
231 | /// |
|
241 | /// | |
232 | /// This is also the `i`th hexadecimal digit in numeric form, |
|
242 | /// This is also the `i`th hexadecimal digit in numeric form, | |
233 | /// also called a [nybble](https://en.wikipedia.org/wiki/Nibble). |
|
243 | /// also called a [nybble](https://en.wikipedia.org/wiki/Nibble). | |
234 | pub fn get_nybble(&self, i: usize) -> u8 { |
|
244 | pub fn get_nybble(&self, i: usize) -> u8 { | |
235 | assert!(i < self.nybbles_len()); |
|
245 | assert!(i < self.nybbles_len()); | |
236 | get_nybble(&self.data, i) |
|
246 | get_nybble(&self.data, i) | |
237 | } |
|
247 | } | |
238 |
|
248 | |||
239 | fn iter_nybbles(&self) -> impl Iterator<Item = u8> + '_ { |
|
249 | fn iter_nybbles(&self) -> impl Iterator<Item = u8> + '_ { | |
240 | (0..self.nybbles_len()).map(move |i| get_nybble(&self.data, i)) |
|
250 | (0..self.nybbles_len()).map(move |i| get_nybble(&self.data, i)) | |
241 | } |
|
251 | } | |
242 |
|
252 | |||
243 | /// Return the index first nybble that's different from `node` |
|
253 | /// Return the index first nybble that's different from `node` | |
244 | /// |
|
254 | /// | |
245 | /// If the return value is `None` that means that `self` is |
|
255 | /// If the return value is `None` that means that `self` is | |
246 | /// a prefix of `node`, but the current method is a bit slower |
|
256 | /// a prefix of `node`, but the current method is a bit slower | |
247 | /// than `is_prefix_of`. |
|
257 | /// than `is_prefix_of`. | |
248 | /// |
|
258 | /// | |
249 | /// Returned index is as in `get_nybble`, i.e., starting at 0. |
|
259 | /// Returned index is as in `get_nybble`, i.e., starting at 0. | |
250 | pub fn first_different_nybble(&self, node: &Node) -> Option<usize> { |
|
260 | pub fn first_different_nybble(&self, node: &Node) -> Option<usize> { | |
251 | self.iter_nybbles() |
|
261 | self.iter_nybbles() | |
252 | .zip(NodePrefix::from(*node).iter_nybbles()) |
|
262 | .zip(NodePrefix::from(*node).iter_nybbles()) | |
253 | .position(|(a, b)| a != b) |
|
263 | .position(|(a, b)| a != b) | |
254 | } |
|
264 | } | |
255 | } |
|
265 | } | |
256 |
|
266 | |||
257 | impl fmt::LowerHex for NodePrefix { |
|
267 | impl fmt::LowerHex for NodePrefix { | |
258 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
|
268 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | |
259 | let full_bytes = self.nybbles_len() / 2; |
|
269 | let full_bytes = self.nybbles_len() / 2; | |
260 | for &byte in &self.data[..full_bytes] { |
|
270 | for &byte in &self.data[..full_bytes] { | |
261 | write!(f, "{:02x}", byte)? |
|
271 | write!(f, "{:02x}", byte)? | |
262 | } |
|
272 | } | |
263 | if self.nybbles_len() % 2 == 1 { |
|
273 | if self.nybbles_len() % 2 == 1 { | |
264 | let last = self.nybbles_len() - 1; |
|
274 | let last = self.nybbles_len() - 1; | |
265 | write!(f, "{:x}", self.get_nybble(last))? |
|
275 | write!(f, "{:x}", self.get_nybble(last))? | |
266 | } |
|
276 | } | |
267 | Ok(()) |
|
277 | Ok(()) | |
268 | } |
|
278 | } | |
269 | } |
|
279 | } | |
270 |
|
280 | |||
271 | /// A shortcut for full `Node` references |
|
281 | /// A shortcut for full `Node` references | |
272 | impl From<&'_ Node> for NodePrefix { |
|
282 | impl From<&'_ Node> for NodePrefix { | |
273 | fn from(node: &'_ Node) -> Self { |
|
283 | fn from(node: &'_ Node) -> Self { | |
274 | NodePrefix { |
|
284 | NodePrefix { | |
275 | nybbles_len: node.nybbles_len() as _, |
|
285 | nybbles_len: node.nybbles_len() as _, | |
276 | data: node.data, |
|
286 | data: node.data, | |
277 | } |
|
287 | } | |
278 | } |
|
288 | } | |
279 | } |
|
289 | } | |
280 |
|
290 | |||
281 | /// A shortcut for full `Node` references |
|
291 | /// A shortcut for full `Node` references | |
282 | impl From<Node> for NodePrefix { |
|
292 | impl From<Node> for NodePrefix { | |
283 | fn from(node: Node) -> Self { |
|
293 | fn from(node: Node) -> Self { | |
284 | NodePrefix { |
|
294 | NodePrefix { | |
285 | nybbles_len: node.nybbles_len() as _, |
|
295 | nybbles_len: node.nybbles_len() as _, | |
286 | data: node.data, |
|
296 | data: node.data, | |
287 | } |
|
297 | } | |
288 | } |
|
298 | } | |
289 | } |
|
299 | } | |
290 |
|
300 | |||
291 | impl PartialEq<Node> for NodePrefix { |
|
301 | impl PartialEq<Node> for NodePrefix { | |
292 | fn eq(&self, other: &Node) -> bool { |
|
302 | fn eq(&self, other: &Node) -> bool { | |
293 | Self::from(*other) == *self |
|
303 | Self::from(*other) == *self | |
294 | } |
|
304 | } | |
295 | } |
|
305 | } | |
296 |
|
306 | |||
297 | #[cfg(test)] |
|
307 | #[cfg(test)] | |
298 | mod tests { |
|
308 | mod tests { | |
299 | use super::*; |
|
309 | use super::*; | |
300 |
|
310 | |||
301 | const SAMPLE_NODE_HEX: &str = "0123456789abcdeffedcba9876543210deadbeef"; |
|
311 | const SAMPLE_NODE_HEX: &str = "0123456789abcdeffedcba9876543210deadbeef"; | |
302 | const SAMPLE_NODE: Node = Node { |
|
312 | const SAMPLE_NODE: Node = Node { | |
303 | data: [ |
|
313 | data: [ | |
304 | 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0xfe, 0xdc, 0xba, |
|
314 | 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0xfe, 0xdc, 0xba, | |
305 | 0x98, 0x76, 0x54, 0x32, 0x10, 0xde, 0xad, 0xbe, 0xef, |
|
315 | 0x98, 0x76, 0x54, 0x32, 0x10, 0xde, 0xad, 0xbe, 0xef, | |
306 | ], |
|
316 | ], | |
307 | }; |
|
317 | }; | |
308 |
|
318 | |||
309 | /// Pad an hexadecimal string to reach `NODE_NYBBLES_LENGTH` |
|
319 | /// Pad an hexadecimal string to reach `NODE_NYBBLES_LENGTH` | |
310 | /// The padding is made with zeros. |
|
320 | /// The padding is made with zeros. | |
311 | pub fn hex_pad_right(hex: &str) -> String { |
|
321 | pub fn hex_pad_right(hex: &str) -> String { | |
312 | let mut res = hex.to_string(); |
|
322 | let mut res = hex.to_string(); | |
313 | while res.len() < NODE_NYBBLES_LENGTH { |
|
323 | while res.len() < NODE_NYBBLES_LENGTH { | |
314 | res.push('0'); |
|
324 | res.push('0'); | |
315 | } |
|
325 | } | |
316 | res |
|
326 | res | |
317 | } |
|
327 | } | |
318 |
|
328 | |||
319 | #[test] |
|
329 | #[test] | |
320 | fn test_node_from_hex() { |
|
330 | fn test_node_from_hex() { | |
321 | let not_hex = "012... oops"; |
|
331 | let not_hex = "012... oops"; | |
322 | let too_short = "0123"; |
|
332 | let too_short = "0123"; | |
323 | let too_long = format!("{}0", SAMPLE_NODE_HEX); |
|
333 | let too_long = format!("{}0", SAMPLE_NODE_HEX); | |
324 | assert_eq!(Node::from_hex(SAMPLE_NODE_HEX).unwrap(), SAMPLE_NODE); |
|
334 | assert_eq!(Node::from_hex(SAMPLE_NODE_HEX).unwrap(), SAMPLE_NODE); | |
325 | assert!(Node::from_hex(not_hex).is_err()); |
|
335 | assert!(Node::from_hex(not_hex).is_err()); | |
326 | assert!(Node::from_hex(too_short).is_err()); |
|
336 | assert!(Node::from_hex(too_short).is_err()); | |
327 | assert!(Node::from_hex(&too_long).is_err()); |
|
337 | assert!(Node::from_hex(&too_long).is_err()); | |
328 | } |
|
338 | } | |
329 |
|
339 | |||
330 | #[test] |
|
340 | #[test] | |
331 | fn test_node_encode_hex() { |
|
341 | fn test_node_encode_hex() { | |
332 | assert_eq!(format!("{:x}", SAMPLE_NODE), SAMPLE_NODE_HEX); |
|
342 | assert_eq!(format!("{:x}", SAMPLE_NODE), SAMPLE_NODE_HEX); | |
333 | } |
|
343 | } | |
334 |
|
344 | |||
335 | #[test] |
|
345 | #[test] | |
336 | fn test_prefix_from_to_hex() -> Result<(), FromHexError> { |
|
346 | fn test_prefix_from_to_hex() -> Result<(), FromHexError> { | |
337 | assert_eq!(format!("{:x}", NodePrefix::from_hex("0e1")?), "0e1"); |
|
347 | assert_eq!(format!("{:x}", NodePrefix::from_hex("0e1")?), "0e1"); | |
338 | assert_eq!(format!("{:x}", NodePrefix::from_hex("0e1a")?), "0e1a"); |
|
348 | assert_eq!(format!("{:x}", NodePrefix::from_hex("0e1a")?), "0e1a"); | |
339 | assert_eq!( |
|
349 | assert_eq!( | |
340 | format!("{:x}", NodePrefix::from_hex(SAMPLE_NODE_HEX)?), |
|
350 | format!("{:x}", NodePrefix::from_hex(SAMPLE_NODE_HEX)?), | |
341 | SAMPLE_NODE_HEX |
|
351 | SAMPLE_NODE_HEX | |
342 | ); |
|
352 | ); | |
343 | Ok(()) |
|
353 | Ok(()) | |
344 | } |
|
354 | } | |
345 |
|
355 | |||
346 | #[test] |
|
356 | #[test] | |
347 | fn test_prefix_from_hex_errors() { |
|
357 | fn test_prefix_from_hex_errors() { | |
348 | assert!(NodePrefix::from_hex("testgr").is_err()); |
|
358 | assert!(NodePrefix::from_hex("testgr").is_err()); | |
349 | let mut long = format!("{:x}", NULL_NODE); |
|
359 | let mut long = format!("{:x}", NULL_NODE); | |
350 | long.push('c'); |
|
360 | long.push('c'); | |
351 | assert!(NodePrefix::from_hex(&long).is_err()) |
|
361 | assert!(NodePrefix::from_hex(&long).is_err()) | |
352 | } |
|
362 | } | |
353 |
|
363 | |||
354 | #[test] |
|
364 | #[test] | |
355 | fn test_is_prefix_of() -> Result<(), FromHexError> { |
|
365 | fn test_is_prefix_of() -> Result<(), FromHexError> { | |
356 | let mut node_data = [0; NODE_BYTES_LENGTH]; |
|
366 | let mut node_data = [0; NODE_BYTES_LENGTH]; | |
357 | node_data[0] = 0x12; |
|
367 | node_data[0] = 0x12; | |
358 | node_data[1] = 0xca; |
|
368 | node_data[1] = 0xca; | |
359 | let node = Node::from(node_data); |
|
369 | let node = Node::from(node_data); | |
360 | assert!(NodePrefix::from_hex("12")?.is_prefix_of(&node)); |
|
370 | assert!(NodePrefix::from_hex("12")?.is_prefix_of(&node)); | |
361 | assert!(!NodePrefix::from_hex("1a")?.is_prefix_of(&node)); |
|
371 | assert!(!NodePrefix::from_hex("1a")?.is_prefix_of(&node)); | |
362 | assert!(NodePrefix::from_hex("12c")?.is_prefix_of(&node)); |
|
372 | assert!(NodePrefix::from_hex("12c")?.is_prefix_of(&node)); | |
363 | assert!(!NodePrefix::from_hex("12d")?.is_prefix_of(&node)); |
|
373 | assert!(!NodePrefix::from_hex("12d")?.is_prefix_of(&node)); | |
364 | Ok(()) |
|
374 | Ok(()) | |
365 | } |
|
375 | } | |
366 |
|
376 | |||
367 | #[test] |
|
377 | #[test] | |
368 | fn test_get_nybble() -> Result<(), FromHexError> { |
|
378 | fn test_get_nybble() -> Result<(), FromHexError> { | |
369 | let prefix = NodePrefix::from_hex("dead6789cafe")?; |
|
379 | let prefix = NodePrefix::from_hex("dead6789cafe")?; | |
370 | assert_eq!(prefix.get_nybble(0), 13); |
|
380 | assert_eq!(prefix.get_nybble(0), 13); | |
371 | assert_eq!(prefix.get_nybble(7), 9); |
|
381 | assert_eq!(prefix.get_nybble(7), 9); | |
372 | Ok(()) |
|
382 | Ok(()) | |
373 | } |
|
383 | } | |
374 |
|
384 | |||
375 | #[test] |
|
385 | #[test] | |
376 | fn test_first_different_nybble_even_prefix() { |
|
386 | fn test_first_different_nybble_even_prefix() { | |
377 | let prefix = NodePrefix::from_hex("12ca").unwrap(); |
|
387 | let prefix = NodePrefix::from_hex("12ca").unwrap(); | |
378 | let mut node = Node::from([0; NODE_BYTES_LENGTH]); |
|
388 | let mut node = Node::from([0; NODE_BYTES_LENGTH]); | |
379 | assert_eq!(prefix.first_different_nybble(&node), Some(0)); |
|
389 | assert_eq!(prefix.first_different_nybble(&node), Some(0)); | |
380 | node.data[0] = 0x13; |
|
390 | node.data[0] = 0x13; | |
381 | assert_eq!(prefix.first_different_nybble(&node), Some(1)); |
|
391 | assert_eq!(prefix.first_different_nybble(&node), Some(1)); | |
382 | node.data[0] = 0x12; |
|
392 | node.data[0] = 0x12; | |
383 | assert_eq!(prefix.first_different_nybble(&node), Some(2)); |
|
393 | assert_eq!(prefix.first_different_nybble(&node), Some(2)); | |
384 | node.data[1] = 0xca; |
|
394 | node.data[1] = 0xca; | |
385 | // now it is a prefix |
|
395 | // now it is a prefix | |
386 | assert_eq!(prefix.first_different_nybble(&node), None); |
|
396 | assert_eq!(prefix.first_different_nybble(&node), None); | |
387 | } |
|
397 | } | |
388 |
|
398 | |||
389 | #[test] |
|
399 | #[test] | |
390 | fn test_first_different_nybble_odd_prefix() { |
|
400 | fn test_first_different_nybble_odd_prefix() { | |
391 | let prefix = NodePrefix::from_hex("12c").unwrap(); |
|
401 | let prefix = NodePrefix::from_hex("12c").unwrap(); | |
392 | let mut node = Node::from([0; NODE_BYTES_LENGTH]); |
|
402 | let mut node = Node::from([0; NODE_BYTES_LENGTH]); | |
393 | assert_eq!(prefix.first_different_nybble(&node), Some(0)); |
|
403 | assert_eq!(prefix.first_different_nybble(&node), Some(0)); | |
394 | node.data[0] = 0x13; |
|
404 | node.data[0] = 0x13; | |
395 | assert_eq!(prefix.first_different_nybble(&node), Some(1)); |
|
405 | assert_eq!(prefix.first_different_nybble(&node), Some(1)); | |
396 | node.data[0] = 0x12; |
|
406 | node.data[0] = 0x12; | |
397 | assert_eq!(prefix.first_different_nybble(&node), Some(2)); |
|
407 | assert_eq!(prefix.first_different_nybble(&node), Some(2)); | |
398 | node.data[1] = 0xca; |
|
408 | node.data[1] = 0xca; | |
399 | // now it is a prefix |
|
409 | // now it is a prefix | |
400 | assert_eq!(prefix.first_different_nybble(&node), None); |
|
410 | assert_eq!(prefix.first_different_nybble(&node), None); | |
401 | } |
|
411 | } | |
402 | } |
|
412 | } | |
403 |
|
413 | |||
404 | #[cfg(test)] |
|
414 | #[cfg(test)] | |
405 | pub use tests::hex_pad_right; |
|
415 | pub use tests::hex_pad_right; |
@@ -1,393 +1,393 b'' | |||||
1 | use std::borrow::Cow; |
|
1 | use std::borrow::Cow; | |
2 | use std::io::Read; |
|
2 | use std::io::Read; | |
3 | use std::ops::Deref; |
|
3 | use std::ops::Deref; | |
4 | use std::path::Path; |
|
4 | use std::path::Path; | |
5 |
|
5 | |||
6 | use byteorder::{BigEndian, ByteOrder}; |
|
6 | use byteorder::{BigEndian, ByteOrder}; | |
7 | use crypto::digest::Digest; |
|
7 | use crypto::digest::Digest; | |
8 | use crypto::sha1::Sha1; |
|
8 | use crypto::sha1::Sha1; | |
9 | use flate2::read::ZlibDecoder; |
|
9 | use flate2::read::ZlibDecoder; | |
10 | use micro_timer::timed; |
|
10 | use micro_timer::timed; | |
11 | use zstd; |
|
11 | use zstd; | |
12 |
|
12 | |||
13 | use super::index::Index; |
|
13 | use super::index::Index; | |
14 | use super::node::{NodePrefix, NODE_BYTES_LENGTH, NULL_NODE}; |
|
14 | use super::node::{NodePrefix, NODE_BYTES_LENGTH, NULL_NODE}; | |
15 | use super::nodemap; |
|
15 | use super::nodemap; | |
16 | use super::nodemap::{NodeMap, NodeMapError}; |
|
16 | use super::nodemap::{NodeMap, NodeMapError}; | |
17 | use super::nodemap_docket::NodeMapDocket; |
|
17 | use super::nodemap_docket::NodeMapDocket; | |
18 | use super::patch; |
|
18 | use super::patch; | |
19 | use crate::errors::HgError; |
|
19 | use crate::errors::HgError; | |
20 | use crate::repo::Repo; |
|
20 | use crate::repo::Repo; | |
21 | use crate::revlog::Revision; |
|
21 | use crate::revlog::Revision; | |
22 |
|
22 | |||
23 | #[derive(derive_more::From)] |
|
23 | #[derive(derive_more::From)] | |
24 | pub enum RevlogError { |
|
24 | pub enum RevlogError { | |
25 | InvalidRevision, |
|
25 | InvalidRevision, | |
26 | /// Found more than one entry whose ID match the requested prefix |
|
26 | /// Found more than one entry whose ID match the requested prefix | |
27 | AmbiguousPrefix, |
|
27 | AmbiguousPrefix, | |
28 | #[from] |
|
28 | #[from] | |
29 | Other(HgError), |
|
29 | Other(HgError), | |
30 | } |
|
30 | } | |
31 |
|
31 | |||
32 | impl From<NodeMapError> for RevlogError { |
|
32 | impl From<NodeMapError> for RevlogError { | |
33 | fn from(error: NodeMapError) -> Self { |
|
33 | fn from(error: NodeMapError) -> Self { | |
34 | match error { |
|
34 | match error { | |
35 | NodeMapError::MultipleResults => RevlogError::AmbiguousPrefix, |
|
35 | NodeMapError::MultipleResults => RevlogError::AmbiguousPrefix, | |
36 | NodeMapError::RevisionNotInIndex(_) => RevlogError::corrupted(), |
|
36 | NodeMapError::RevisionNotInIndex(_) => RevlogError::corrupted(), | |
37 | } |
|
37 | } | |
38 | } |
|
38 | } | |
39 | } |
|
39 | } | |
40 |
|
40 | |||
41 | impl RevlogError { |
|
41 | impl RevlogError { | |
42 | fn corrupted() -> Self { |
|
42 | fn corrupted() -> Self { | |
43 | RevlogError::Other(HgError::corrupted("corrupted revlog")) |
|
43 | RevlogError::Other(HgError::corrupted("corrupted revlog")) | |
44 | } |
|
44 | } | |
45 | } |
|
45 | } | |
46 |
|
46 | |||
47 | /// Read only implementation of revlog. |
|
47 | /// Read only implementation of revlog. | |
48 | pub struct Revlog { |
|
48 | pub struct Revlog { | |
49 | /// When index and data are not interleaved: bytes of the revlog index. |
|
49 | /// When index and data are not interleaved: bytes of the revlog index. | |
50 | /// When index and data are interleaved: bytes of the revlog index and |
|
50 | /// When index and data are interleaved: bytes of the revlog index and | |
51 | /// data. |
|
51 | /// data. | |
52 | index: Index, |
|
52 | pub(crate) index: Index, | |
53 | /// When index and data are not interleaved: bytes of the revlog data |
|
53 | /// When index and data are not interleaved: bytes of the revlog data | |
54 | data_bytes: Option<Box<dyn Deref<Target = [u8]> + Send>>, |
|
54 | data_bytes: Option<Box<dyn Deref<Target = [u8]> + Send>>, | |
55 | /// When present on disk: the persistent nodemap for this revlog |
|
55 | /// When present on disk: the persistent nodemap for this revlog | |
56 | nodemap: Option<nodemap::NodeTree>, |
|
56 | nodemap: Option<nodemap::NodeTree>, | |
57 | } |
|
57 | } | |
58 |
|
58 | |||
59 | impl Revlog { |
|
59 | impl Revlog { | |
60 | /// Open a revlog index file. |
|
60 | /// Open a revlog index file. | |
61 | /// |
|
61 | /// | |
62 | /// It will also open the associated data file if index and data are not |
|
62 | /// It will also open the associated data file if index and data are not | |
63 | /// interleaved. |
|
63 | /// interleaved. | |
64 | #[timed] |
|
64 | #[timed] | |
65 | pub fn open( |
|
65 | pub fn open( | |
66 | repo: &Repo, |
|
66 | repo: &Repo, | |
67 | index_path: impl AsRef<Path>, |
|
67 | index_path: impl AsRef<Path>, | |
68 | data_path: Option<&Path>, |
|
68 | data_path: Option<&Path>, | |
69 | ) -> Result<Self, RevlogError> { |
|
69 | ) -> Result<Self, RevlogError> { | |
70 | let index_path = index_path.as_ref(); |
|
70 | let index_path = index_path.as_ref(); | |
71 | let index_mmap = repo.store_vfs().mmap_open(&index_path)?; |
|
71 | let index_mmap = repo.store_vfs().mmap_open(&index_path)?; | |
72 |
|
72 | |||
73 | let version = get_version(&index_mmap); |
|
73 | let version = get_version(&index_mmap); | |
74 | if version != 1 { |
|
74 | if version != 1 { | |
75 | // A proper new version should have had a repo/store requirement. |
|
75 | // A proper new version should have had a repo/store requirement. | |
76 | return Err(RevlogError::corrupted()); |
|
76 | return Err(RevlogError::corrupted()); | |
77 | } |
|
77 | } | |
78 |
|
78 | |||
79 | let index = Index::new(Box::new(index_mmap))?; |
|
79 | let index = Index::new(Box::new(index_mmap))?; | |
80 |
|
80 | |||
81 | let default_data_path = index_path.with_extension("d"); |
|
81 | let default_data_path = index_path.with_extension("d"); | |
82 |
|
82 | |||
83 | // type annotation required |
|
83 | // type annotation required | |
84 | // won't recognize Mmap as Deref<Target = [u8]> |
|
84 | // won't recognize Mmap as Deref<Target = [u8]> | |
85 | let data_bytes: Option<Box<dyn Deref<Target = [u8]> + Send>> = |
|
85 | let data_bytes: Option<Box<dyn Deref<Target = [u8]> + Send>> = | |
86 | if index.is_inline() { |
|
86 | if index.is_inline() { | |
87 | None |
|
87 | None | |
88 | } else { |
|
88 | } else { | |
89 | let data_path = data_path.unwrap_or(&default_data_path); |
|
89 | let data_path = data_path.unwrap_or(&default_data_path); | |
90 | let data_mmap = repo.store_vfs().mmap_open(data_path)?; |
|
90 | let data_mmap = repo.store_vfs().mmap_open(data_path)?; | |
91 | Some(Box::new(data_mmap)) |
|
91 | Some(Box::new(data_mmap)) | |
92 | }; |
|
92 | }; | |
93 |
|
93 | |||
94 | let nodemap = NodeMapDocket::read_from_file(repo, index_path)?.map( |
|
94 | let nodemap = NodeMapDocket::read_from_file(repo, index_path)?.map( | |
95 | |(docket, data)| { |
|
95 | |(docket, data)| { | |
96 | nodemap::NodeTree::load_bytes( |
|
96 | nodemap::NodeTree::load_bytes( | |
97 | Box::new(data), |
|
97 | Box::new(data), | |
98 | docket.data_length, |
|
98 | docket.data_length, | |
99 | ) |
|
99 | ) | |
100 | }, |
|
100 | }, | |
101 | ); |
|
101 | ); | |
102 |
|
102 | |||
103 | Ok(Revlog { |
|
103 | Ok(Revlog { | |
104 | index, |
|
104 | index, | |
105 | data_bytes, |
|
105 | data_bytes, | |
106 | nodemap, |
|
106 | nodemap, | |
107 | }) |
|
107 | }) | |
108 | } |
|
108 | } | |
109 |
|
109 | |||
110 | /// Return number of entries of the `Revlog`. |
|
110 | /// Return number of entries of the `Revlog`. | |
111 | pub fn len(&self) -> usize { |
|
111 | pub fn len(&self) -> usize { | |
112 | self.index.len() |
|
112 | self.index.len() | |
113 | } |
|
113 | } | |
114 |
|
114 | |||
115 | /// Returns `true` if the `Revlog` has zero `entries`. |
|
115 | /// Returns `true` if the `Revlog` has zero `entries`. | |
116 | pub fn is_empty(&self) -> bool { |
|
116 | pub fn is_empty(&self) -> bool { | |
117 | self.index.is_empty() |
|
117 | self.index.is_empty() | |
118 | } |
|
118 | } | |
119 |
|
119 | |||
120 | /// Return the full data associated to a node. |
|
120 | /// Return the full data associated to a node. | |
121 | #[timed] |
|
121 | #[timed] | |
122 | pub fn get_node_rev( |
|
122 | pub fn get_node_rev( | |
123 | &self, |
|
123 | &self, | |
124 | node: NodePrefix, |
|
124 | node: NodePrefix, | |
125 | ) -> Result<Revision, RevlogError> { |
|
125 | ) -> Result<Revision, RevlogError> { | |
126 | if let Some(nodemap) = &self.nodemap { |
|
126 | if let Some(nodemap) = &self.nodemap { | |
127 | return nodemap |
|
127 | return nodemap | |
128 | .find_bin(&self.index, node)? |
|
128 | .find_bin(&self.index, node)? | |
129 | .ok_or(RevlogError::InvalidRevision); |
|
129 | .ok_or(RevlogError::InvalidRevision); | |
130 | } |
|
130 | } | |
131 |
|
131 | |||
132 | // Fallback to linear scan when a persistent nodemap is not present. |
|
132 | // Fallback to linear scan when a persistent nodemap is not present. | |
133 | // This happens when the persistent-nodemap experimental feature is not |
|
133 | // This happens when the persistent-nodemap experimental feature is not | |
134 | // enabled, or for small revlogs. |
|
134 | // enabled, or for small revlogs. | |
135 | // |
|
135 | // | |
136 | // TODO: consider building a non-persistent nodemap in memory to |
|
136 | // TODO: consider building a non-persistent nodemap in memory to | |
137 | // optimize these cases. |
|
137 | // optimize these cases. | |
138 | let mut found_by_prefix = None; |
|
138 | let mut found_by_prefix = None; | |
139 | for rev in (0..self.len() as Revision).rev() { |
|
139 | for rev in (0..self.len() as Revision).rev() { | |
140 | let index_entry = |
|
140 | let index_entry = | |
141 | self.index.get_entry(rev).ok_or(HgError::corrupted( |
|
141 | self.index.get_entry(rev).ok_or(HgError::corrupted( | |
142 | "revlog references a revision not in the index", |
|
142 | "revlog references a revision not in the index", | |
143 | ))?; |
|
143 | ))?; | |
144 | if node == *index_entry.hash() { |
|
144 | if node == *index_entry.hash() { | |
145 | return Ok(rev); |
|
145 | return Ok(rev); | |
146 | } |
|
146 | } | |
147 | if node.is_prefix_of(index_entry.hash()) { |
|
147 | if node.is_prefix_of(index_entry.hash()) { | |
148 | if found_by_prefix.is_some() { |
|
148 | if found_by_prefix.is_some() { | |
149 | return Err(RevlogError::AmbiguousPrefix); |
|
149 | return Err(RevlogError::AmbiguousPrefix); | |
150 | } |
|
150 | } | |
151 | found_by_prefix = Some(rev) |
|
151 | found_by_prefix = Some(rev) | |
152 | } |
|
152 | } | |
153 | } |
|
153 | } | |
154 | found_by_prefix.ok_or(RevlogError::InvalidRevision) |
|
154 | found_by_prefix.ok_or(RevlogError::InvalidRevision) | |
155 | } |
|
155 | } | |
156 |
|
156 | |||
157 | /// Returns whether the given revision exists in this revlog. |
|
157 | /// Returns whether the given revision exists in this revlog. | |
158 | pub fn has_rev(&self, rev: Revision) -> bool { |
|
158 | pub fn has_rev(&self, rev: Revision) -> bool { | |
159 | self.index.get_entry(rev).is_some() |
|
159 | self.index.get_entry(rev).is_some() | |
160 | } |
|
160 | } | |
161 |
|
161 | |||
162 | /// Return the full data associated to a revision. |
|
162 | /// Return the full data associated to a revision. | |
163 | /// |
|
163 | /// | |
164 | /// All entries required to build the final data out of deltas will be |
|
164 | /// All entries required to build the final data out of deltas will be | |
165 | /// retrieved as needed, and the deltas will be applied to the inital |
|
165 | /// retrieved as needed, and the deltas will be applied to the inital | |
166 | /// snapshot to rebuild the final data. |
|
166 | /// snapshot to rebuild the final data. | |
167 | #[timed] |
|
167 | #[timed] | |
168 | pub fn get_rev_data(&self, rev: Revision) -> Result<Vec<u8>, RevlogError> { |
|
168 | pub fn get_rev_data(&self, rev: Revision) -> Result<Vec<u8>, RevlogError> { | |
169 | // Todo return -> Cow |
|
169 | // Todo return -> Cow | |
170 | let mut entry = self.get_entry(rev)?; |
|
170 | let mut entry = self.get_entry(rev)?; | |
171 | let mut delta_chain = vec![]; |
|
171 | let mut delta_chain = vec![]; | |
172 | while let Some(base_rev) = entry.base_rev { |
|
172 | while let Some(base_rev) = entry.base_rev { | |
173 | delta_chain.push(entry); |
|
173 | delta_chain.push(entry); | |
174 | entry = self |
|
174 | entry = self | |
175 | .get_entry(base_rev) |
|
175 | .get_entry(base_rev) | |
176 | .map_err(|_| RevlogError::corrupted())?; |
|
176 | .map_err(|_| RevlogError::corrupted())?; | |
177 | } |
|
177 | } | |
178 |
|
178 | |||
179 | // TODO do not look twice in the index |
|
179 | // TODO do not look twice in the index | |
180 | let index_entry = self |
|
180 | let index_entry = self | |
181 | .index |
|
181 | .index | |
182 | .get_entry(rev) |
|
182 | .get_entry(rev) | |
183 | .ok_or(RevlogError::InvalidRevision)?; |
|
183 | .ok_or(RevlogError::InvalidRevision)?; | |
184 |
|
184 | |||
185 | let data: Vec<u8> = if delta_chain.is_empty() { |
|
185 | let data: Vec<u8> = if delta_chain.is_empty() { | |
186 | entry.data()?.into() |
|
186 | entry.data()?.into() | |
187 | } else { |
|
187 | } else { | |
188 | Revlog::build_data_from_deltas(entry, &delta_chain)? |
|
188 | Revlog::build_data_from_deltas(entry, &delta_chain)? | |
189 | }; |
|
189 | }; | |
190 |
|
190 | |||
191 | if self.check_hash( |
|
191 | if self.check_hash( | |
192 | index_entry.p1(), |
|
192 | index_entry.p1(), | |
193 | index_entry.p2(), |
|
193 | index_entry.p2(), | |
194 | index_entry.hash().as_bytes(), |
|
194 | index_entry.hash().as_bytes(), | |
195 | &data, |
|
195 | &data, | |
196 | ) { |
|
196 | ) { | |
197 | Ok(data) |
|
197 | Ok(data) | |
198 | } else { |
|
198 | } else { | |
199 | Err(RevlogError::corrupted()) |
|
199 | Err(RevlogError::corrupted()) | |
200 | } |
|
200 | } | |
201 | } |
|
201 | } | |
202 |
|
202 | |||
203 | /// Check the hash of some given data against the recorded hash. |
|
203 | /// Check the hash of some given data against the recorded hash. | |
204 | pub fn check_hash( |
|
204 | pub fn check_hash( | |
205 | &self, |
|
205 | &self, | |
206 | p1: Revision, |
|
206 | p1: Revision, | |
207 | p2: Revision, |
|
207 | p2: Revision, | |
208 | expected: &[u8], |
|
208 | expected: &[u8], | |
209 | data: &[u8], |
|
209 | data: &[u8], | |
210 | ) -> bool { |
|
210 | ) -> bool { | |
211 | let e1 = self.index.get_entry(p1); |
|
211 | let e1 = self.index.get_entry(p1); | |
212 | let h1 = match e1 { |
|
212 | let h1 = match e1 { | |
213 | Some(ref entry) => entry.hash(), |
|
213 | Some(ref entry) => entry.hash(), | |
214 | None => &NULL_NODE, |
|
214 | None => &NULL_NODE, | |
215 | }; |
|
215 | }; | |
216 | let e2 = self.index.get_entry(p2); |
|
216 | let e2 = self.index.get_entry(p2); | |
217 | let h2 = match e2 { |
|
217 | let h2 = match e2 { | |
218 | Some(ref entry) => entry.hash(), |
|
218 | Some(ref entry) => entry.hash(), | |
219 | None => &NULL_NODE, |
|
219 | None => &NULL_NODE, | |
220 | }; |
|
220 | }; | |
221 |
|
221 | |||
222 | hash(data, h1.as_bytes(), h2.as_bytes()).as_slice() == expected |
|
222 | hash(data, h1.as_bytes(), h2.as_bytes()).as_slice() == expected | |
223 | } |
|
223 | } | |
224 |
|
224 | |||
225 | /// Build the full data of a revision out its snapshot |
|
225 | /// Build the full data of a revision out its snapshot | |
226 | /// and its deltas. |
|
226 | /// and its deltas. | |
227 | #[timed] |
|
227 | #[timed] | |
228 | fn build_data_from_deltas( |
|
228 | fn build_data_from_deltas( | |
229 | snapshot: RevlogEntry, |
|
229 | snapshot: RevlogEntry, | |
230 | deltas: &[RevlogEntry], |
|
230 | deltas: &[RevlogEntry], | |
231 | ) -> Result<Vec<u8>, RevlogError> { |
|
231 | ) -> Result<Vec<u8>, RevlogError> { | |
232 | let snapshot = snapshot.data()?; |
|
232 | let snapshot = snapshot.data()?; | |
233 | let deltas = deltas |
|
233 | let deltas = deltas | |
234 | .iter() |
|
234 | .iter() | |
235 | .rev() |
|
235 | .rev() | |
236 | .map(RevlogEntry::data) |
|
236 | .map(RevlogEntry::data) | |
237 | .collect::<Result<Vec<Cow<'_, [u8]>>, RevlogError>>()?; |
|
237 | .collect::<Result<Vec<Cow<'_, [u8]>>, RevlogError>>()?; | |
238 | let patches: Vec<_> = |
|
238 | let patches: Vec<_> = | |
239 | deltas.iter().map(|d| patch::PatchList::new(d)).collect(); |
|
239 | deltas.iter().map(|d| patch::PatchList::new(d)).collect(); | |
240 | let patch = patch::fold_patch_lists(&patches); |
|
240 | let patch = patch::fold_patch_lists(&patches); | |
241 | Ok(patch.apply(&snapshot)) |
|
241 | Ok(patch.apply(&snapshot)) | |
242 | } |
|
242 | } | |
243 |
|
243 | |||
244 | /// Return the revlog data. |
|
244 | /// Return the revlog data. | |
245 | fn data(&self) -> &[u8] { |
|
245 | fn data(&self) -> &[u8] { | |
246 | match self.data_bytes { |
|
246 | match self.data_bytes { | |
247 | Some(ref data_bytes) => &data_bytes, |
|
247 | Some(ref data_bytes) => &data_bytes, | |
248 | None => panic!( |
|
248 | None => panic!( | |
249 | "forgot to load the data or trying to access inline data" |
|
249 | "forgot to load the data or trying to access inline data" | |
250 | ), |
|
250 | ), | |
251 | } |
|
251 | } | |
252 | } |
|
252 | } | |
253 |
|
253 | |||
254 | /// Get an entry of the revlog. |
|
254 | /// Get an entry of the revlog. | |
255 | fn get_entry(&self, rev: Revision) -> Result<RevlogEntry, RevlogError> { |
|
255 | fn get_entry(&self, rev: Revision) -> Result<RevlogEntry, RevlogError> { | |
256 | let index_entry = self |
|
256 | let index_entry = self | |
257 | .index |
|
257 | .index | |
258 | .get_entry(rev) |
|
258 | .get_entry(rev) | |
259 | .ok_or(RevlogError::InvalidRevision)?; |
|
259 | .ok_or(RevlogError::InvalidRevision)?; | |
260 | let start = index_entry.offset(); |
|
260 | let start = index_entry.offset(); | |
261 | let end = start + index_entry.compressed_len(); |
|
261 | let end = start + index_entry.compressed_len(); | |
262 | let data = if self.index.is_inline() { |
|
262 | let data = if self.index.is_inline() { | |
263 | self.index.data(start, end) |
|
263 | self.index.data(start, end) | |
264 | } else { |
|
264 | } else { | |
265 | &self.data()[start..end] |
|
265 | &self.data()[start..end] | |
266 | }; |
|
266 | }; | |
267 | let entry = RevlogEntry { |
|
267 | let entry = RevlogEntry { | |
268 | rev, |
|
268 | rev, | |
269 | bytes: data, |
|
269 | bytes: data, | |
270 | compressed_len: index_entry.compressed_len(), |
|
270 | compressed_len: index_entry.compressed_len(), | |
271 | uncompressed_len: index_entry.uncompressed_len(), |
|
271 | uncompressed_len: index_entry.uncompressed_len(), | |
272 | base_rev: if index_entry.base_revision() == rev { |
|
272 | base_rev: if index_entry.base_revision() == rev { | |
273 | None |
|
273 | None | |
274 | } else { |
|
274 | } else { | |
275 | Some(index_entry.base_revision()) |
|
275 | Some(index_entry.base_revision()) | |
276 | }, |
|
276 | }, | |
277 | }; |
|
277 | }; | |
278 | Ok(entry) |
|
278 | Ok(entry) | |
279 | } |
|
279 | } | |
280 | } |
|
280 | } | |
281 |
|
281 | |||
282 | /// The revlog entry's bytes and the necessary informations to extract |
|
282 | /// The revlog entry's bytes and the necessary informations to extract | |
283 | /// the entry's data. |
|
283 | /// the entry's data. | |
284 | #[derive(Debug)] |
|
284 | #[derive(Debug)] | |
285 | pub struct RevlogEntry<'a> { |
|
285 | pub struct RevlogEntry<'a> { | |
286 | rev: Revision, |
|
286 | rev: Revision, | |
287 | bytes: &'a [u8], |
|
287 | bytes: &'a [u8], | |
288 | compressed_len: usize, |
|
288 | compressed_len: usize, | |
289 | uncompressed_len: usize, |
|
289 | uncompressed_len: usize, | |
290 | base_rev: Option<Revision>, |
|
290 | base_rev: Option<Revision>, | |
291 | } |
|
291 | } | |
292 |
|
292 | |||
293 | impl<'a> RevlogEntry<'a> { |
|
293 | impl<'a> RevlogEntry<'a> { | |
294 | /// Extract the data contained in the entry. |
|
294 | /// Extract the data contained in the entry. | |
295 | pub fn data(&self) -> Result<Cow<'_, [u8]>, RevlogError> { |
|
295 | pub fn data(&self) -> Result<Cow<'_, [u8]>, RevlogError> { | |
296 | if self.bytes.is_empty() { |
|
296 | if self.bytes.is_empty() { | |
297 | return Ok(Cow::Borrowed(&[])); |
|
297 | return Ok(Cow::Borrowed(&[])); | |
298 | } |
|
298 | } | |
299 | match self.bytes[0] { |
|
299 | match self.bytes[0] { | |
300 | // Revision data is the entirety of the entry, including this |
|
300 | // Revision data is the entirety of the entry, including this | |
301 | // header. |
|
301 | // header. | |
302 | b'\0' => Ok(Cow::Borrowed(self.bytes)), |
|
302 | b'\0' => Ok(Cow::Borrowed(self.bytes)), | |
303 | // Raw revision data follows. |
|
303 | // Raw revision data follows. | |
304 | b'u' => Ok(Cow::Borrowed(&self.bytes[1..])), |
|
304 | b'u' => Ok(Cow::Borrowed(&self.bytes[1..])), | |
305 | // zlib (RFC 1950) data. |
|
305 | // zlib (RFC 1950) data. | |
306 | b'x' => Ok(Cow::Owned(self.uncompressed_zlib_data()?)), |
|
306 | b'x' => Ok(Cow::Owned(self.uncompressed_zlib_data()?)), | |
307 | // zstd data. |
|
307 | // zstd data. | |
308 | b'\x28' => Ok(Cow::Owned(self.uncompressed_zstd_data()?)), |
|
308 | b'\x28' => Ok(Cow::Owned(self.uncompressed_zstd_data()?)), | |
309 | // A proper new format should have had a repo/store requirement. |
|
309 | // A proper new format should have had a repo/store requirement. | |
310 | _format_type => Err(RevlogError::corrupted()), |
|
310 | _format_type => Err(RevlogError::corrupted()), | |
311 | } |
|
311 | } | |
312 | } |
|
312 | } | |
313 |
|
313 | |||
314 | fn uncompressed_zlib_data(&self) -> Result<Vec<u8>, RevlogError> { |
|
314 | fn uncompressed_zlib_data(&self) -> Result<Vec<u8>, RevlogError> { | |
315 | let mut decoder = ZlibDecoder::new(self.bytes); |
|
315 | let mut decoder = ZlibDecoder::new(self.bytes); | |
316 | if self.is_delta() { |
|
316 | if self.is_delta() { | |
317 | let mut buf = Vec::with_capacity(self.compressed_len); |
|
317 | let mut buf = Vec::with_capacity(self.compressed_len); | |
318 | decoder |
|
318 | decoder | |
319 | .read_to_end(&mut buf) |
|
319 | .read_to_end(&mut buf) | |
320 | .map_err(|_| RevlogError::corrupted())?; |
|
320 | .map_err(|_| RevlogError::corrupted())?; | |
321 | Ok(buf) |
|
321 | Ok(buf) | |
322 | } else { |
|
322 | } else { | |
323 | let mut buf = vec![0; self.uncompressed_len]; |
|
323 | let mut buf = vec![0; self.uncompressed_len]; | |
324 | decoder |
|
324 | decoder | |
325 | .read_exact(&mut buf) |
|
325 | .read_exact(&mut buf) | |
326 | .map_err(|_| RevlogError::corrupted())?; |
|
326 | .map_err(|_| RevlogError::corrupted())?; | |
327 | Ok(buf) |
|
327 | Ok(buf) | |
328 | } |
|
328 | } | |
329 | } |
|
329 | } | |
330 |
|
330 | |||
331 | fn uncompressed_zstd_data(&self) -> Result<Vec<u8>, RevlogError> { |
|
331 | fn uncompressed_zstd_data(&self) -> Result<Vec<u8>, RevlogError> { | |
332 | if self.is_delta() { |
|
332 | if self.is_delta() { | |
333 | let mut buf = Vec::with_capacity(self.compressed_len); |
|
333 | let mut buf = Vec::with_capacity(self.compressed_len); | |
334 | zstd::stream::copy_decode(self.bytes, &mut buf) |
|
334 | zstd::stream::copy_decode(self.bytes, &mut buf) | |
335 | .map_err(|_| RevlogError::corrupted())?; |
|
335 | .map_err(|_| RevlogError::corrupted())?; | |
336 | Ok(buf) |
|
336 | Ok(buf) | |
337 | } else { |
|
337 | } else { | |
338 | let mut buf = vec![0; self.uncompressed_len]; |
|
338 | let mut buf = vec![0; self.uncompressed_len]; | |
339 | let len = zstd::block::decompress_to_buffer(self.bytes, &mut buf) |
|
339 | let len = zstd::block::decompress_to_buffer(self.bytes, &mut buf) | |
340 | .map_err(|_| RevlogError::corrupted())?; |
|
340 | .map_err(|_| RevlogError::corrupted())?; | |
341 | if len != self.uncompressed_len { |
|
341 | if len != self.uncompressed_len { | |
342 | Err(RevlogError::corrupted()) |
|
342 | Err(RevlogError::corrupted()) | |
343 | } else { |
|
343 | } else { | |
344 | Ok(buf) |
|
344 | Ok(buf) | |
345 | } |
|
345 | } | |
346 | } |
|
346 | } | |
347 | } |
|
347 | } | |
348 |
|
348 | |||
349 | /// Tell if the entry is a snapshot or a delta |
|
349 | /// Tell if the entry is a snapshot or a delta | |
350 | /// (influences on decompression). |
|
350 | /// (influences on decompression). | |
351 | fn is_delta(&self) -> bool { |
|
351 | fn is_delta(&self) -> bool { | |
352 | self.base_rev.is_some() |
|
352 | self.base_rev.is_some() | |
353 | } |
|
353 | } | |
354 | } |
|
354 | } | |
355 |
|
355 | |||
356 | /// Format version of the revlog. |
|
356 | /// Format version of the revlog. | |
357 | pub fn get_version(index_bytes: &[u8]) -> u16 { |
|
357 | pub fn get_version(index_bytes: &[u8]) -> u16 { | |
358 | BigEndian::read_u16(&index_bytes[2..=3]) |
|
358 | BigEndian::read_u16(&index_bytes[2..=3]) | |
359 | } |
|
359 | } | |
360 |
|
360 | |||
361 | /// Calculate the hash of a revision given its data and its parents. |
|
361 | /// Calculate the hash of a revision given its data and its parents. | |
362 | fn hash(data: &[u8], p1_hash: &[u8], p2_hash: &[u8]) -> Vec<u8> { |
|
362 | fn hash(data: &[u8], p1_hash: &[u8], p2_hash: &[u8]) -> Vec<u8> { | |
363 | let mut hasher = Sha1::new(); |
|
363 | let mut hasher = Sha1::new(); | |
364 | let (a, b) = (p1_hash, p2_hash); |
|
364 | let (a, b) = (p1_hash, p2_hash); | |
365 | if a > b { |
|
365 | if a > b { | |
366 | hasher.input(b); |
|
366 | hasher.input(b); | |
367 | hasher.input(a); |
|
367 | hasher.input(a); | |
368 | } else { |
|
368 | } else { | |
369 | hasher.input(a); |
|
369 | hasher.input(a); | |
370 | hasher.input(b); |
|
370 | hasher.input(b); | |
371 | } |
|
371 | } | |
372 | hasher.input(data); |
|
372 | hasher.input(data); | |
373 | let mut hash = vec![0; NODE_BYTES_LENGTH]; |
|
373 | let mut hash = vec![0; NODE_BYTES_LENGTH]; | |
374 | hasher.result(&mut hash); |
|
374 | hasher.result(&mut hash); | |
375 | hash |
|
375 | hash | |
376 | } |
|
376 | } | |
377 |
|
377 | |||
378 | #[cfg(test)] |
|
378 | #[cfg(test)] | |
379 | mod tests { |
|
379 | mod tests { | |
380 | use super::*; |
|
380 | use super::*; | |
381 |
|
381 | |||
382 | use super::super::index::IndexEntryBuilder; |
|
382 | use super::super::index::IndexEntryBuilder; | |
383 |
|
383 | |||
384 | #[test] |
|
384 | #[test] | |
385 | fn version_test() { |
|
385 | fn version_test() { | |
386 | let bytes = IndexEntryBuilder::new() |
|
386 | let bytes = IndexEntryBuilder::new() | |
387 | .is_first(true) |
|
387 | .is_first(true) | |
388 | .with_version(1) |
|
388 | .with_version(1) | |
389 | .build(); |
|
389 | .build(); | |
390 |
|
390 | |||
391 | assert_eq!(get_version(&bytes), 1) |
|
391 | assert_eq!(get_version(&bytes), 1) | |
392 | } |
|
392 | } | |
393 | } |
|
393 | } |
@@ -1,69 +1,84 b'' | |||||
1 | use crate::error::CommandError; |
|
1 | use crate::error::CommandError; | |
2 | use clap::Arg; |
|
2 | use clap::Arg; | |
|
3 | use format_bytes::format_bytes; | |||
3 | use hg::operations::cat; |
|
4 | use hg::operations::cat; | |
4 | use hg::utils::hg_path::HgPathBuf; |
|
5 | use hg::utils::hg_path::HgPathBuf; | |
5 | use micro_timer::timed; |
|
6 | use micro_timer::timed; | |
6 | use std::convert::TryFrom; |
|
7 | use std::convert::TryFrom; | |
7 |
|
8 | |||
8 | pub const HELP_TEXT: &str = " |
|
9 | pub const HELP_TEXT: &str = " | |
9 | Output the current or given revision of files |
|
10 | Output the current or given revision of files | |
10 | "; |
|
11 | "; | |
11 |
|
12 | |||
12 | pub fn args() -> clap::App<'static, 'static> { |
|
13 | pub fn args() -> clap::App<'static, 'static> { | |
13 | clap::SubCommand::with_name("cat") |
|
14 | clap::SubCommand::with_name("cat") | |
14 | .arg( |
|
15 | .arg( | |
15 | Arg::with_name("rev") |
|
16 | Arg::with_name("rev") | |
16 | .help("search the repository as it is in REV") |
|
17 | .help("search the repository as it is in REV") | |
17 | .short("-r") |
|
18 | .short("-r") | |
18 | .long("--revision") |
|
19 | .long("--revision") | |
19 | .value_name("REV") |
|
20 | .value_name("REV") | |
20 | .takes_value(true), |
|
21 | .takes_value(true), | |
21 | ) |
|
22 | ) | |
22 | .arg( |
|
23 | .arg( | |
23 | clap::Arg::with_name("files") |
|
24 | clap::Arg::with_name("files") | |
24 | .required(true) |
|
25 | .required(true) | |
25 | .multiple(true) |
|
26 | .multiple(true) | |
26 | .empty_values(false) |
|
27 | .empty_values(false) | |
27 | .value_name("FILE") |
|
28 | .value_name("FILE") | |
28 | .help("Activity to start: activity@category"), |
|
29 | .help("Activity to start: activity@category"), | |
29 | ) |
|
30 | ) | |
30 | .about(HELP_TEXT) |
|
31 | .about(HELP_TEXT) | |
31 | } |
|
32 | } | |
32 |
|
33 | |||
33 | #[timed] |
|
34 | #[timed] | |
34 | pub fn run(invocation: &crate::CliInvocation) -> Result<(), CommandError> { |
|
35 | pub fn run(invocation: &crate::CliInvocation) -> Result<(), CommandError> { | |
35 | let rev = invocation.subcommand_args.value_of("rev"); |
|
36 | let rev = invocation.subcommand_args.value_of("rev"); | |
36 | let file_args = match invocation.subcommand_args.values_of("files") { |
|
37 | let file_args = match invocation.subcommand_args.values_of("files") { | |
37 | Some(files) => files.collect(), |
|
38 | Some(files) => files.collect(), | |
38 | None => vec![], |
|
39 | None => vec![], | |
39 | }; |
|
40 | }; | |
40 |
|
41 | |||
41 | let repo = invocation.repo?; |
|
42 | let repo = invocation.repo?; | |
42 | let cwd = hg::utils::current_dir()?; |
|
43 | let cwd = hg::utils::current_dir()?; | |
43 | let working_directory = repo.working_directory_path(); |
|
44 | let working_directory = repo.working_directory_path(); | |
44 | let working_directory = cwd.join(working_directory); // Make it absolute |
|
45 | let working_directory = cwd.join(working_directory); // Make it absolute | |
45 |
|
46 | |||
46 | let mut files = vec![]; |
|
47 | let mut files = vec![]; | |
47 | for file in file_args.iter() { |
|
48 | for file in file_args.iter() { | |
48 | // TODO: actually normalize `..` path segments etc? |
|
49 | // TODO: actually normalize `..` path segments etc? | |
49 | let normalized = cwd.join(&file); |
|
50 | let normalized = cwd.join(&file); | |
50 | let stripped = normalized |
|
51 | let stripped = normalized | |
51 | .strip_prefix(&working_directory) |
|
52 | .strip_prefix(&working_directory) | |
52 | // TODO: error message for path arguments outside of the repo |
|
53 | // TODO: error message for path arguments outside of the repo | |
53 | .map_err(|_| CommandError::abort(""))?; |
|
54 | .map_err(|_| CommandError::abort(""))?; | |
54 | let hg_file = HgPathBuf::try_from(stripped.to_path_buf()) |
|
55 | let hg_file = HgPathBuf::try_from(stripped.to_path_buf()) | |
55 | .map_err(|e| CommandError::abort(e.to_string()))?; |
|
56 | .map_err(|e| CommandError::abort(e.to_string()))?; | |
56 | files.push(hg_file); |
|
57 | files.push(hg_file); | |
57 | } |
|
58 | } | |
58 |
|
59 | |||
59 | match rev { |
|
60 | match rev { | |
60 | Some(rev) => { |
|
61 | Some(rev) => { | |
61 |
let |
|
62 | let output = cat(&repo, rev, &files).map_err(|e| (e, rev))?; | |
62 |
invocation.ui.write_stdout(& |
|
63 | invocation.ui.write_stdout(&output.concatenated)?; | |
63 | Ok(()) |
|
64 | if !output.missing.is_empty() { | |
|
65 | let short = format!("{:x}", output.node.short()).into_bytes(); | |||
|
66 | for path in &output.missing { | |||
|
67 | invocation.ui.write_stderr(&format_bytes!( | |||
|
68 | b"{}: no such file in rev {}\n", | |||
|
69 | path.as_bytes(), | |||
|
70 | short | |||
|
71 | ))?; | |||
|
72 | } | |||
|
73 | } | |||
|
74 | if output.found_any { | |||
|
75 | Ok(()) | |||
|
76 | } else { | |||
|
77 | Err(CommandError::Unsuccessful) | |||
|
78 | } | |||
64 | } |
|
79 | } | |
65 | None => Err(CommandError::unsupported( |
|
80 | None => Err(CommandError::unsupported( | |
66 | "`rhg cat` without `--rev` / `-r`", |
|
81 | "`rhg cat` without `--rev` / `-r`", | |
67 | )), |
|
82 | )), | |
68 | } |
|
83 | } | |
69 | } |
|
84 | } |
@@ -1,143 +1,146 b'' | |||||
1 | use crate::ui::utf8_to_local; |
|
1 | use crate::ui::utf8_to_local; | |
2 | use crate::ui::UiError; |
|
2 | use crate::ui::UiError; | |
3 | use crate::NoRepoInCwdError; |
|
3 | use crate::NoRepoInCwdError; | |
4 | use format_bytes::format_bytes; |
|
4 | use format_bytes::format_bytes; | |
5 | use hg::config::{ConfigError, ConfigParseError}; |
|
5 | use hg::config::{ConfigError, ConfigParseError}; | |
6 | use hg::errors::HgError; |
|
6 | use hg::errors::HgError; | |
7 | use hg::repo::RepoError; |
|
7 | use hg::repo::RepoError; | |
8 | use hg::revlog::revlog::RevlogError; |
|
8 | use hg::revlog::revlog::RevlogError; | |
9 | use hg::utils::files::get_bytes_from_path; |
|
9 | use hg::utils::files::get_bytes_from_path; | |
10 | use std::convert::From; |
|
10 | use std::convert::From; | |
11 |
|
11 | |||
12 | /// The kind of command error |
|
12 | /// The kind of command error | |
13 | #[derive(Debug)] |
|
13 | #[derive(Debug)] | |
14 | pub enum CommandError { |
|
14 | pub enum CommandError { | |
15 | /// Exit with an error message and "standard" failure exit code. |
|
15 | /// Exit with an error message and "standard" failure exit code. | |
16 | Abort { message: Vec<u8> }, |
|
16 | Abort { message: Vec<u8> }, | |
17 |
|
17 | |||
|
18 | /// Exit with a failure exit code but no message. | |||
|
19 | Unsuccessful, | |||
|
20 | ||||
18 | /// Encountered something (such as a CLI argument, repository layout, β¦) |
|
21 | /// Encountered something (such as a CLI argument, repository layout, β¦) | |
19 | /// not supported by this version of `rhg`. Depending on configuration |
|
22 | /// not supported by this version of `rhg`. Depending on configuration | |
20 | /// `rhg` may attempt to silently fall back to Python-based `hg`, which |
|
23 | /// `rhg` may attempt to silently fall back to Python-based `hg`, which | |
21 | /// may or may not support this feature. |
|
24 | /// may or may not support this feature. | |
22 | UnsupportedFeature { message: Vec<u8> }, |
|
25 | UnsupportedFeature { message: Vec<u8> }, | |
23 | } |
|
26 | } | |
24 |
|
27 | |||
25 | impl CommandError { |
|
28 | impl CommandError { | |
26 | pub fn abort(message: impl AsRef<str>) -> Self { |
|
29 | pub fn abort(message: impl AsRef<str>) -> Self { | |
27 | CommandError::Abort { |
|
30 | CommandError::Abort { | |
28 | // TODO: bytes-based (instead of Unicode-based) formatting |
|
31 | // TODO: bytes-based (instead of Unicode-based) formatting | |
29 | // of error messages to handle non-UTF-8 filenames etc: |
|
32 | // of error messages to handle non-UTF-8 filenames etc: | |
30 | // https://www.mercurial-scm.org/wiki/EncodingStrategy#Mixing_output |
|
33 | // https://www.mercurial-scm.org/wiki/EncodingStrategy#Mixing_output | |
31 | message: utf8_to_local(message.as_ref()).into(), |
|
34 | message: utf8_to_local(message.as_ref()).into(), | |
32 | } |
|
35 | } | |
33 | } |
|
36 | } | |
34 |
|
37 | |||
35 | pub fn unsupported(message: impl AsRef<str>) -> Self { |
|
38 | pub fn unsupported(message: impl AsRef<str>) -> Self { | |
36 | CommandError::UnsupportedFeature { |
|
39 | CommandError::UnsupportedFeature { | |
37 | message: utf8_to_local(message.as_ref()).into(), |
|
40 | message: utf8_to_local(message.as_ref()).into(), | |
38 | } |
|
41 | } | |
39 | } |
|
42 | } | |
40 | } |
|
43 | } | |
41 |
|
44 | |||
42 | /// For now we donβt differenciate between invalid CLI args and valid for `hg` |
|
45 | /// For now we donβt differenciate between invalid CLI args and valid for `hg` | |
43 | /// but not supported yet by `rhg`. |
|
46 | /// but not supported yet by `rhg`. | |
44 | impl From<clap::Error> for CommandError { |
|
47 | impl From<clap::Error> for CommandError { | |
45 | fn from(error: clap::Error) -> Self { |
|
48 | fn from(error: clap::Error) -> Self { | |
46 | CommandError::unsupported(error.to_string()) |
|
49 | CommandError::unsupported(error.to_string()) | |
47 | } |
|
50 | } | |
48 | } |
|
51 | } | |
49 |
|
52 | |||
50 | impl From<HgError> for CommandError { |
|
53 | impl From<HgError> for CommandError { | |
51 | fn from(error: HgError) -> Self { |
|
54 | fn from(error: HgError) -> Self { | |
52 | match error { |
|
55 | match error { | |
53 | HgError::UnsupportedFeature(message) => { |
|
56 | HgError::UnsupportedFeature(message) => { | |
54 | CommandError::unsupported(message) |
|
57 | CommandError::unsupported(message) | |
55 | } |
|
58 | } | |
56 | _ => CommandError::abort(error.to_string()), |
|
59 | _ => CommandError::abort(error.to_string()), | |
57 | } |
|
60 | } | |
58 | } |
|
61 | } | |
59 | } |
|
62 | } | |
60 |
|
63 | |||
61 | impl From<UiError> for CommandError { |
|
64 | impl From<UiError> for CommandError { | |
62 | fn from(_error: UiError) -> Self { |
|
65 | fn from(_error: UiError) -> Self { | |
63 | // If we already failed writing to stdout or stderr, |
|
66 | // If we already failed writing to stdout or stderr, | |
64 | // writing an error message to stderr about it would be likely to fail |
|
67 | // writing an error message to stderr about it would be likely to fail | |
65 | // too. |
|
68 | // too. | |
66 | CommandError::abort("") |
|
69 | CommandError::abort("") | |
67 | } |
|
70 | } | |
68 | } |
|
71 | } | |
69 |
|
72 | |||
70 | impl From<RepoError> for CommandError { |
|
73 | impl From<RepoError> for CommandError { | |
71 | fn from(error: RepoError) -> Self { |
|
74 | fn from(error: RepoError) -> Self { | |
72 | match error { |
|
75 | match error { | |
73 | RepoError::NotFound { at } => CommandError::Abort { |
|
76 | RepoError::NotFound { at } => CommandError::Abort { | |
74 | message: format_bytes!( |
|
77 | message: format_bytes!( | |
75 | b"abort: repository {} not found", |
|
78 | b"abort: repository {} not found", | |
76 | get_bytes_from_path(at) |
|
79 | get_bytes_from_path(at) | |
77 | ), |
|
80 | ), | |
78 | }, |
|
81 | }, | |
79 | RepoError::ConfigParseError(error) => error.into(), |
|
82 | RepoError::ConfigParseError(error) => error.into(), | |
80 | RepoError::Other(error) => error.into(), |
|
83 | RepoError::Other(error) => error.into(), | |
81 | } |
|
84 | } | |
82 | } |
|
85 | } | |
83 | } |
|
86 | } | |
84 |
|
87 | |||
85 | impl<'a> From<&'a NoRepoInCwdError> for CommandError { |
|
88 | impl<'a> From<&'a NoRepoInCwdError> for CommandError { | |
86 | fn from(error: &'a NoRepoInCwdError) -> Self { |
|
89 | fn from(error: &'a NoRepoInCwdError) -> Self { | |
87 | let NoRepoInCwdError { cwd } = error; |
|
90 | let NoRepoInCwdError { cwd } = error; | |
88 | CommandError::Abort { |
|
91 | CommandError::Abort { | |
89 | message: format_bytes!( |
|
92 | message: format_bytes!( | |
90 | b"abort: no repository found in '{}' (.hg not found)!", |
|
93 | b"abort: no repository found in '{}' (.hg not found)!", | |
91 | get_bytes_from_path(cwd) |
|
94 | get_bytes_from_path(cwd) | |
92 | ), |
|
95 | ), | |
93 | } |
|
96 | } | |
94 | } |
|
97 | } | |
95 | } |
|
98 | } | |
96 |
|
99 | |||
97 | impl From<ConfigError> for CommandError { |
|
100 | impl From<ConfigError> for CommandError { | |
98 | fn from(error: ConfigError) -> Self { |
|
101 | fn from(error: ConfigError) -> Self { | |
99 | match error { |
|
102 | match error { | |
100 | ConfigError::Parse(error) => error.into(), |
|
103 | ConfigError::Parse(error) => error.into(), | |
101 | ConfigError::Other(error) => error.into(), |
|
104 | ConfigError::Other(error) => error.into(), | |
102 | } |
|
105 | } | |
103 | } |
|
106 | } | |
104 | } |
|
107 | } | |
105 |
|
108 | |||
106 | impl From<ConfigParseError> for CommandError { |
|
109 | impl From<ConfigParseError> for CommandError { | |
107 | fn from(error: ConfigParseError) -> Self { |
|
110 | fn from(error: ConfigParseError) -> Self { | |
108 | let ConfigParseError { |
|
111 | let ConfigParseError { | |
109 | origin, |
|
112 | origin, | |
110 | line, |
|
113 | line, | |
111 | message, |
|
114 | message, | |
112 | } = error; |
|
115 | } = error; | |
113 | let line_message = if let Some(line_number) = line { |
|
116 | let line_message = if let Some(line_number) = line { | |
114 | format_bytes!(b":{}", line_number.to_string().into_bytes()) |
|
117 | format_bytes!(b":{}", line_number.to_string().into_bytes()) | |
115 | } else { |
|
118 | } else { | |
116 | Vec::new() |
|
119 | Vec::new() | |
117 | }; |
|
120 | }; | |
118 | CommandError::Abort { |
|
121 | CommandError::Abort { | |
119 | message: format_bytes!( |
|
122 | message: format_bytes!( | |
120 | b"config error at {}{}: {}", |
|
123 | b"config error at {}{}: {}", | |
121 | origin, |
|
124 | origin, | |
122 | line_message, |
|
125 | line_message, | |
123 | message |
|
126 | message | |
124 | ), |
|
127 | ), | |
125 | } |
|
128 | } | |
126 | } |
|
129 | } | |
127 | } |
|
130 | } | |
128 |
|
131 | |||
129 | impl From<(RevlogError, &str)> for CommandError { |
|
132 | impl From<(RevlogError, &str)> for CommandError { | |
130 | fn from((err, rev): (RevlogError, &str)) -> CommandError { |
|
133 | fn from((err, rev): (RevlogError, &str)) -> CommandError { | |
131 | match err { |
|
134 | match err { | |
132 | RevlogError::InvalidRevision => CommandError::abort(format!( |
|
135 | RevlogError::InvalidRevision => CommandError::abort(format!( | |
133 | "abort: invalid revision identifier: {}", |
|
136 | "abort: invalid revision identifier: {}", | |
134 | rev |
|
137 | rev | |
135 | )), |
|
138 | )), | |
136 | RevlogError::AmbiguousPrefix => CommandError::abort(format!( |
|
139 | RevlogError::AmbiguousPrefix => CommandError::abort(format!( | |
137 | "abort: ambiguous revision identifier: {}", |
|
140 | "abort: ambiguous revision identifier: {}", | |
138 | rev |
|
141 | rev | |
139 | )), |
|
142 | )), | |
140 | RevlogError::Other(error) => error.into(), |
|
143 | RevlogError::Other(error) => error.into(), | |
141 | } |
|
144 | } | |
142 | } |
|
145 | } | |
143 | } |
|
146 | } |
@@ -1,10 +1,13 b'' | |||||
1 | pub type ExitCode = i32; |
|
1 | pub type ExitCode = i32; | |
2 |
|
2 | |||
3 | /// Successful exit |
|
3 | /// Successful exit | |
4 | pub const OK: ExitCode = 0; |
|
4 | pub const OK: ExitCode = 0; | |
5 |
|
5 | |||
6 | /// Generic abort |
|
6 | /// Generic abort | |
7 | pub const ABORT: ExitCode = 255; |
|
7 | pub const ABORT: ExitCode = 255; | |
8 |
|
8 | |||
|
9 | /// Generic something completed but did not succeed | |||
|
10 | pub const UNSUCCESSFUL: ExitCode = 1; | |||
|
11 | ||||
9 | /// Command or feature not implemented by rhg |
|
12 | /// Command or feature not implemented by rhg | |
10 | pub const UNIMPLEMENTED: ExitCode = 252; |
|
13 | pub const UNIMPLEMENTED: ExitCode = 252; |
@@ -1,440 +1,442 b'' | |||||
1 | extern crate log; |
|
1 | extern crate log; | |
2 | use crate::ui::Ui; |
|
2 | use crate::ui::Ui; | |
3 | use clap::App; |
|
3 | use clap::App; | |
4 | use clap::AppSettings; |
|
4 | use clap::AppSettings; | |
5 | use clap::Arg; |
|
5 | use clap::Arg; | |
6 | use clap::ArgMatches; |
|
6 | use clap::ArgMatches; | |
7 | use format_bytes::{format_bytes, join}; |
|
7 | use format_bytes::{format_bytes, join}; | |
8 | use hg::config::Config; |
|
8 | use hg::config::Config; | |
9 | use hg::repo::{Repo, RepoError}; |
|
9 | use hg::repo::{Repo, RepoError}; | |
10 | use hg::utils::files::{get_bytes_from_os_str, get_path_from_bytes}; |
|
10 | use hg::utils::files::{get_bytes_from_os_str, get_path_from_bytes}; | |
11 | use hg::utils::SliceExt; |
|
11 | use hg::utils::SliceExt; | |
12 | use std::ffi::OsString; |
|
12 | use std::ffi::OsString; | |
13 | use std::path::PathBuf; |
|
13 | use std::path::PathBuf; | |
14 | use std::process::Command; |
|
14 | use std::process::Command; | |
15 |
|
15 | |||
16 | mod blackbox; |
|
16 | mod blackbox; | |
17 | mod error; |
|
17 | mod error; | |
18 | mod exitcode; |
|
18 | mod exitcode; | |
19 | mod ui; |
|
19 | mod ui; | |
20 | use error::CommandError; |
|
20 | use error::CommandError; | |
21 |
|
21 | |||
22 | fn main_with_result( |
|
22 | fn main_with_result( | |
23 | process_start_time: &blackbox::ProcessStartTime, |
|
23 | process_start_time: &blackbox::ProcessStartTime, | |
24 | ui: &ui::Ui, |
|
24 | ui: &ui::Ui, | |
25 | repo: Result<&Repo, &NoRepoInCwdError>, |
|
25 | repo: Result<&Repo, &NoRepoInCwdError>, | |
26 | config: &Config, |
|
26 | config: &Config, | |
27 | ) -> Result<(), CommandError> { |
|
27 | ) -> Result<(), CommandError> { | |
28 | check_extensions(config)?; |
|
28 | check_extensions(config)?; | |
29 |
|
29 | |||
30 | let app = App::new("rhg") |
|
30 | let app = App::new("rhg") | |
31 | .global_setting(AppSettings::AllowInvalidUtf8) |
|
31 | .global_setting(AppSettings::AllowInvalidUtf8) | |
32 | .setting(AppSettings::SubcommandRequired) |
|
32 | .setting(AppSettings::SubcommandRequired) | |
33 | .setting(AppSettings::VersionlessSubcommands) |
|
33 | .setting(AppSettings::VersionlessSubcommands) | |
34 | .arg( |
|
34 | .arg( | |
35 | Arg::with_name("repository") |
|
35 | Arg::with_name("repository") | |
36 | .help("repository root directory") |
|
36 | .help("repository root directory") | |
37 | .short("-R") |
|
37 | .short("-R") | |
38 | .long("--repository") |
|
38 | .long("--repository") | |
39 | .value_name("REPO") |
|
39 | .value_name("REPO") | |
40 | .takes_value(true) |
|
40 | .takes_value(true) | |
41 | // Both ok: `hg -R ./foo log` or `hg log -R ./foo` |
|
41 | // Both ok: `hg -R ./foo log` or `hg log -R ./foo` | |
42 | .global(true), |
|
42 | .global(true), | |
43 | ) |
|
43 | ) | |
44 | .arg( |
|
44 | .arg( | |
45 | Arg::with_name("config") |
|
45 | Arg::with_name("config") | |
46 | .help("set/override config option (use 'section.name=value')") |
|
46 | .help("set/override config option (use 'section.name=value')") | |
47 | .long("--config") |
|
47 | .long("--config") | |
48 | .value_name("CONFIG") |
|
48 | .value_name("CONFIG") | |
49 | .takes_value(true) |
|
49 | .takes_value(true) | |
50 | .global(true) |
|
50 | .global(true) | |
51 | // Ok: `--config section.key1=val --config section.key2=val2` |
|
51 | // Ok: `--config section.key1=val --config section.key2=val2` | |
52 | .multiple(true) |
|
52 | .multiple(true) | |
53 | // Not ok: `--config section.key1=val section.key2=val2` |
|
53 | // Not ok: `--config section.key1=val section.key2=val2` | |
54 | .number_of_values(1), |
|
54 | .number_of_values(1), | |
55 | ) |
|
55 | ) | |
56 | .arg( |
|
56 | .arg( | |
57 | Arg::with_name("cwd") |
|
57 | Arg::with_name("cwd") | |
58 | .help("change working directory") |
|
58 | .help("change working directory") | |
59 | .long("--cwd") |
|
59 | .long("--cwd") | |
60 | .value_name("DIR") |
|
60 | .value_name("DIR") | |
61 | .takes_value(true) |
|
61 | .takes_value(true) | |
62 | .global(true), |
|
62 | .global(true), | |
63 | ) |
|
63 | ) | |
64 | .version("0.0.1"); |
|
64 | .version("0.0.1"); | |
65 | let app = add_subcommand_args(app); |
|
65 | let app = add_subcommand_args(app); | |
66 |
|
66 | |||
67 | let matches = app.clone().get_matches_safe()?; |
|
67 | let matches = app.clone().get_matches_safe()?; | |
68 |
|
68 | |||
69 | let (subcommand_name, subcommand_matches) = matches.subcommand(); |
|
69 | let (subcommand_name, subcommand_matches) = matches.subcommand(); | |
70 | let run = subcommand_run_fn(subcommand_name) |
|
70 | let run = subcommand_run_fn(subcommand_name) | |
71 | .expect("unknown subcommand name from clap despite AppSettings::SubcommandRequired"); |
|
71 | .expect("unknown subcommand name from clap despite AppSettings::SubcommandRequired"); | |
72 | let subcommand_args = subcommand_matches |
|
72 | let subcommand_args = subcommand_matches | |
73 | .expect("no subcommand arguments from clap despite AppSettings::SubcommandRequired"); |
|
73 | .expect("no subcommand arguments from clap despite AppSettings::SubcommandRequired"); | |
74 |
|
74 | |||
75 | let invocation = CliInvocation { |
|
75 | let invocation = CliInvocation { | |
76 | ui, |
|
76 | ui, | |
77 | subcommand_args, |
|
77 | subcommand_args, | |
78 | config, |
|
78 | config, | |
79 | repo, |
|
79 | repo, | |
80 | }; |
|
80 | }; | |
81 | let blackbox = blackbox::Blackbox::new(&invocation, process_start_time)?; |
|
81 | let blackbox = blackbox::Blackbox::new(&invocation, process_start_time)?; | |
82 | blackbox.log_command_start(); |
|
82 | blackbox.log_command_start(); | |
83 | let result = run(&invocation); |
|
83 | let result = run(&invocation); | |
84 | blackbox.log_command_end(exit_code(&result)); |
|
84 | blackbox.log_command_end(exit_code(&result)); | |
85 | result |
|
85 | result | |
86 | } |
|
86 | } | |
87 |
|
87 | |||
88 | fn main() { |
|
88 | fn main() { | |
89 | // Run this first, before we find out if the blackbox extension is even |
|
89 | // Run this first, before we find out if the blackbox extension is even | |
90 | // enabled, in order to include everything in-between in the duration |
|
90 | // enabled, in order to include everything in-between in the duration | |
91 | // measurements. Reading config files can be slow if theyβre on NFS. |
|
91 | // measurements. Reading config files can be slow if theyβre on NFS. | |
92 | let process_start_time = blackbox::ProcessStartTime::now(); |
|
92 | let process_start_time = blackbox::ProcessStartTime::now(); | |
93 |
|
93 | |||
94 | env_logger::init(); |
|
94 | env_logger::init(); | |
95 | let ui = ui::Ui::new(); |
|
95 | let ui = ui::Ui::new(); | |
96 |
|
96 | |||
97 | let early_args = EarlyArgs::parse(std::env::args_os()); |
|
97 | let early_args = EarlyArgs::parse(std::env::args_os()); | |
98 |
|
98 | |||
99 | let initial_current_dir = early_args.cwd.map(|cwd| { |
|
99 | let initial_current_dir = early_args.cwd.map(|cwd| { | |
100 | let cwd = get_path_from_bytes(&cwd); |
|
100 | let cwd = get_path_from_bytes(&cwd); | |
101 | std::env::current_dir() |
|
101 | std::env::current_dir() | |
102 | .and_then(|initial| { |
|
102 | .and_then(|initial| { | |
103 | std::env::set_current_dir(cwd)?; |
|
103 | std::env::set_current_dir(cwd)?; | |
104 | Ok(initial) |
|
104 | Ok(initial) | |
105 | }) |
|
105 | }) | |
106 | .unwrap_or_else(|error| { |
|
106 | .unwrap_or_else(|error| { | |
107 | exit( |
|
107 | exit( | |
108 | &None, |
|
108 | &None, | |
109 | &ui, |
|
109 | &ui, | |
110 | OnUnsupported::Abort, |
|
110 | OnUnsupported::Abort, | |
111 | Err(CommandError::abort(format!( |
|
111 | Err(CommandError::abort(format!( | |
112 | "abort: {}: '{}'", |
|
112 | "abort: {}: '{}'", | |
113 | error, |
|
113 | error, | |
114 | cwd.display() |
|
114 | cwd.display() | |
115 | ))), |
|
115 | ))), | |
116 | ) |
|
116 | ) | |
117 | }) |
|
117 | }) | |
118 | }); |
|
118 | }); | |
119 |
|
119 | |||
120 | let non_repo_config = |
|
120 | let non_repo_config = | |
121 | Config::load(early_args.config).unwrap_or_else(|error| { |
|
121 | Config::load(early_args.config).unwrap_or_else(|error| { | |
122 | // Normally this is decided based on config, but we donβt have that |
|
122 | // Normally this is decided based on config, but we donβt have that | |
123 | // available. As of this writing config loading never returns an |
|
123 | // available. As of this writing config loading never returns an | |
124 | // "unsupported" error but that is not enforced by the type system. |
|
124 | // "unsupported" error but that is not enforced by the type system. | |
125 | let on_unsupported = OnUnsupported::Abort; |
|
125 | let on_unsupported = OnUnsupported::Abort; | |
126 |
|
126 | |||
127 | exit(&initial_current_dir, &ui, on_unsupported, Err(error.into())) |
|
127 | exit(&initial_current_dir, &ui, on_unsupported, Err(error.into())) | |
128 | }); |
|
128 | }); | |
129 |
|
129 | |||
130 | if let Some(repo_path_bytes) = &early_args.repo { |
|
130 | if let Some(repo_path_bytes) = &early_args.repo { | |
131 | lazy_static::lazy_static! { |
|
131 | lazy_static::lazy_static! { | |
132 | static ref SCHEME_RE: regex::bytes::Regex = |
|
132 | static ref SCHEME_RE: regex::bytes::Regex = | |
133 | // Same as `_matchscheme` in `mercurial/util.py` |
|
133 | // Same as `_matchscheme` in `mercurial/util.py` | |
134 | regex::bytes::Regex::new("^[a-zA-Z0-9+.\\-]+:").unwrap(); |
|
134 | regex::bytes::Regex::new("^[a-zA-Z0-9+.\\-]+:").unwrap(); | |
135 | } |
|
135 | } | |
136 | if SCHEME_RE.is_match(&repo_path_bytes) { |
|
136 | if SCHEME_RE.is_match(&repo_path_bytes) { | |
137 | exit( |
|
137 | exit( | |
138 | &initial_current_dir, |
|
138 | &initial_current_dir, | |
139 | &ui, |
|
139 | &ui, | |
140 | OnUnsupported::from_config(&non_repo_config), |
|
140 | OnUnsupported::from_config(&non_repo_config), | |
141 | Err(CommandError::UnsupportedFeature { |
|
141 | Err(CommandError::UnsupportedFeature { | |
142 | message: format_bytes!( |
|
142 | message: format_bytes!( | |
143 | b"URL-like --repository {}", |
|
143 | b"URL-like --repository {}", | |
144 | repo_path_bytes |
|
144 | repo_path_bytes | |
145 | ), |
|
145 | ), | |
146 | }), |
|
146 | }), | |
147 | ) |
|
147 | ) | |
148 | } |
|
148 | } | |
149 | } |
|
149 | } | |
150 | let repo_path = early_args.repo.as_deref().map(get_path_from_bytes); |
|
150 | let repo_path = early_args.repo.as_deref().map(get_path_from_bytes); | |
151 | let repo_result = match Repo::find(&non_repo_config, repo_path) { |
|
151 | let repo_result = match Repo::find(&non_repo_config, repo_path) { | |
152 | Ok(repo) => Ok(repo), |
|
152 | Ok(repo) => Ok(repo), | |
153 | Err(RepoError::NotFound { at }) if repo_path.is_none() => { |
|
153 | Err(RepoError::NotFound { at }) if repo_path.is_none() => { | |
154 | // Not finding a repo is not fatal yet, if `-R` was not given |
|
154 | // Not finding a repo is not fatal yet, if `-R` was not given | |
155 | Err(NoRepoInCwdError { cwd: at }) |
|
155 | Err(NoRepoInCwdError { cwd: at }) | |
156 | } |
|
156 | } | |
157 | Err(error) => exit( |
|
157 | Err(error) => exit( | |
158 | &initial_current_dir, |
|
158 | &initial_current_dir, | |
159 | &ui, |
|
159 | &ui, | |
160 | OnUnsupported::from_config(&non_repo_config), |
|
160 | OnUnsupported::from_config(&non_repo_config), | |
161 | Err(error.into()), |
|
161 | Err(error.into()), | |
162 | ), |
|
162 | ), | |
163 | }; |
|
163 | }; | |
164 |
|
164 | |||
165 | let config = if let Ok(repo) = &repo_result { |
|
165 | let config = if let Ok(repo) = &repo_result { | |
166 | repo.config() |
|
166 | repo.config() | |
167 | } else { |
|
167 | } else { | |
168 | &non_repo_config |
|
168 | &non_repo_config | |
169 | }; |
|
169 | }; | |
170 |
|
170 | |||
171 | let result = main_with_result( |
|
171 | let result = main_with_result( | |
172 | &process_start_time, |
|
172 | &process_start_time, | |
173 | &ui, |
|
173 | &ui, | |
174 | repo_result.as_ref(), |
|
174 | repo_result.as_ref(), | |
175 | config, |
|
175 | config, | |
176 | ); |
|
176 | ); | |
177 | exit( |
|
177 | exit( | |
178 | &initial_current_dir, |
|
178 | &initial_current_dir, | |
179 | &ui, |
|
179 | &ui, | |
180 | OnUnsupported::from_config(config), |
|
180 | OnUnsupported::from_config(config), | |
181 | result, |
|
181 | result, | |
182 | ) |
|
182 | ) | |
183 | } |
|
183 | } | |
184 |
|
184 | |||
185 | fn exit_code(result: &Result<(), CommandError>) -> i32 { |
|
185 | fn exit_code(result: &Result<(), CommandError>) -> i32 { | |
186 | match result { |
|
186 | match result { | |
187 | Ok(()) => exitcode::OK, |
|
187 | Ok(()) => exitcode::OK, | |
188 | Err(CommandError::Abort { .. }) => exitcode::ABORT, |
|
188 | Err(CommandError::Abort { .. }) => exitcode::ABORT, | |
|
189 | Err(CommandError::Unsuccessful) => exitcode::UNSUCCESSFUL, | |||
189 |
|
190 | |||
190 | // Exit with a specific code and no error message to let a potential |
|
191 | // Exit with a specific code and no error message to let a potential | |
191 | // wrapper script fallback to Python-based Mercurial. |
|
192 | // wrapper script fallback to Python-based Mercurial. | |
192 | Err(CommandError::UnsupportedFeature { .. }) => { |
|
193 | Err(CommandError::UnsupportedFeature { .. }) => { | |
193 | exitcode::UNIMPLEMENTED |
|
194 | exitcode::UNIMPLEMENTED | |
194 | } |
|
195 | } | |
195 | } |
|
196 | } | |
196 | } |
|
197 | } | |
197 |
|
198 | |||
198 | fn exit( |
|
199 | fn exit( | |
199 | initial_current_dir: &Option<PathBuf>, |
|
200 | initial_current_dir: &Option<PathBuf>, | |
200 | ui: &Ui, |
|
201 | ui: &Ui, | |
201 | mut on_unsupported: OnUnsupported, |
|
202 | mut on_unsupported: OnUnsupported, | |
202 | result: Result<(), CommandError>, |
|
203 | result: Result<(), CommandError>, | |
203 | ) -> ! { |
|
204 | ) -> ! { | |
204 | if let ( |
|
205 | if let ( | |
205 | OnUnsupported::Fallback { executable }, |
|
206 | OnUnsupported::Fallback { executable }, | |
206 | Err(CommandError::UnsupportedFeature { .. }), |
|
207 | Err(CommandError::UnsupportedFeature { .. }), | |
207 | ) = (&on_unsupported, &result) |
|
208 | ) = (&on_unsupported, &result) | |
208 | { |
|
209 | { | |
209 | let mut args = std::env::args_os(); |
|
210 | let mut args = std::env::args_os(); | |
210 | let executable_path = get_path_from_bytes(&executable); |
|
211 | let executable_path = get_path_from_bytes(&executable); | |
211 | let this_executable = args.next().expect("exepcted argv[0] to exist"); |
|
212 | let this_executable = args.next().expect("exepcted argv[0] to exist"); | |
212 | if executable_path == &PathBuf::from(this_executable) { |
|
213 | if executable_path == &PathBuf::from(this_executable) { | |
213 | // Avoid spawning infinitely many processes until resource |
|
214 | // Avoid spawning infinitely many processes until resource | |
214 | // exhaustion. |
|
215 | // exhaustion. | |
215 | let _ = ui.write_stderr(&format_bytes!( |
|
216 | let _ = ui.write_stderr(&format_bytes!( | |
216 | b"Blocking recursive fallback. The 'rhg.fallback-executable = {}' config \ |
|
217 | b"Blocking recursive fallback. The 'rhg.fallback-executable = {}' config \ | |
217 | points to `rhg` itself.\n", |
|
218 | points to `rhg` itself.\n", | |
218 | executable |
|
219 | executable | |
219 | )); |
|
220 | )); | |
220 | on_unsupported = OnUnsupported::Abort |
|
221 | on_unsupported = OnUnsupported::Abort | |
221 | } else { |
|
222 | } else { | |
222 | // `args` is now `argv[1..]` since weβve already consumed `argv[0]` |
|
223 | // `args` is now `argv[1..]` since weβve already consumed `argv[0]` | |
223 | let mut command = Command::new(executable_path); |
|
224 | let mut command = Command::new(executable_path); | |
224 | command.args(args); |
|
225 | command.args(args); | |
225 | if let Some(initial) = initial_current_dir { |
|
226 | if let Some(initial) = initial_current_dir { | |
226 | command.current_dir(initial); |
|
227 | command.current_dir(initial); | |
227 | } |
|
228 | } | |
228 | let result = command.status(); |
|
229 | let result = command.status(); | |
229 | match result { |
|
230 | match result { | |
230 | Ok(status) => std::process::exit( |
|
231 | Ok(status) => std::process::exit( | |
231 | status.code().unwrap_or(exitcode::ABORT), |
|
232 | status.code().unwrap_or(exitcode::ABORT), | |
232 | ), |
|
233 | ), | |
233 | Err(error) => { |
|
234 | Err(error) => { | |
234 | let _ = ui.write_stderr(&format_bytes!( |
|
235 | let _ = ui.write_stderr(&format_bytes!( | |
235 | b"tried to fall back to a '{}' sub-process but got error {}\n", |
|
236 | b"tried to fall back to a '{}' sub-process but got error {}\n", | |
236 | executable, format_bytes::Utf8(error) |
|
237 | executable, format_bytes::Utf8(error) | |
237 | )); |
|
238 | )); | |
238 | on_unsupported = OnUnsupported::Abort |
|
239 | on_unsupported = OnUnsupported::Abort | |
239 | } |
|
240 | } | |
240 | } |
|
241 | } | |
241 | } |
|
242 | } | |
242 | } |
|
243 | } | |
243 | match &result { |
|
244 | match &result { | |
244 | Ok(_) => {} |
|
245 | Ok(_) => {} | |
|
246 | Err(CommandError::Unsuccessful) => {} | |||
245 | Err(CommandError::Abort { message }) => { |
|
247 | Err(CommandError::Abort { message }) => { | |
246 | if !message.is_empty() { |
|
248 | if !message.is_empty() { | |
247 | // Ignore errors when writing to stderr, weβre already exiting |
|
249 | // Ignore errors when writing to stderr, weβre already exiting | |
248 | // with failure code so thereβs not much more we can do. |
|
250 | // with failure code so thereβs not much more we can do. | |
249 | let _ = ui.write_stderr(&format_bytes!(b"{}\n", message)); |
|
251 | let _ = ui.write_stderr(&format_bytes!(b"{}\n", message)); | |
250 | } |
|
252 | } | |
251 | } |
|
253 | } | |
252 | Err(CommandError::UnsupportedFeature { message }) => { |
|
254 | Err(CommandError::UnsupportedFeature { message }) => { | |
253 | match on_unsupported { |
|
255 | match on_unsupported { | |
254 | OnUnsupported::Abort => { |
|
256 | OnUnsupported::Abort => { | |
255 | let _ = ui.write_stderr(&format_bytes!( |
|
257 | let _ = ui.write_stderr(&format_bytes!( | |
256 | b"unsupported feature: {}\n", |
|
258 | b"unsupported feature: {}\n", | |
257 | message |
|
259 | message | |
258 | )); |
|
260 | )); | |
259 | } |
|
261 | } | |
260 | OnUnsupported::AbortSilent => {} |
|
262 | OnUnsupported::AbortSilent => {} | |
261 | OnUnsupported::Fallback { .. } => unreachable!(), |
|
263 | OnUnsupported::Fallback { .. } => unreachable!(), | |
262 | } |
|
264 | } | |
263 | } |
|
265 | } | |
264 | } |
|
266 | } | |
265 | std::process::exit(exit_code(&result)) |
|
267 | std::process::exit(exit_code(&result)) | |
266 | } |
|
268 | } | |
267 |
|
269 | |||
268 | macro_rules! subcommands { |
|
270 | macro_rules! subcommands { | |
269 | ($( $command: ident )+) => { |
|
271 | ($( $command: ident )+) => { | |
270 | mod commands { |
|
272 | mod commands { | |
271 | $( |
|
273 | $( | |
272 | pub mod $command; |
|
274 | pub mod $command; | |
273 | )+ |
|
275 | )+ | |
274 | } |
|
276 | } | |
275 |
|
277 | |||
276 | fn add_subcommand_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { |
|
278 | fn add_subcommand_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { | |
277 | app |
|
279 | app | |
278 | $( |
|
280 | $( | |
279 | .subcommand(commands::$command::args()) |
|
281 | .subcommand(commands::$command::args()) | |
280 | )+ |
|
282 | )+ | |
281 | } |
|
283 | } | |
282 |
|
284 | |||
283 | pub type RunFn = fn(&CliInvocation) -> Result<(), CommandError>; |
|
285 | pub type RunFn = fn(&CliInvocation) -> Result<(), CommandError>; | |
284 |
|
286 | |||
285 | fn subcommand_run_fn(name: &str) -> Option<RunFn> { |
|
287 | fn subcommand_run_fn(name: &str) -> Option<RunFn> { | |
286 | match name { |
|
288 | match name { | |
287 | $( |
|
289 | $( | |
288 | stringify!($command) => Some(commands::$command::run), |
|
290 | stringify!($command) => Some(commands::$command::run), | |
289 | )+ |
|
291 | )+ | |
290 | _ => None, |
|
292 | _ => None, | |
291 | } |
|
293 | } | |
292 | } |
|
294 | } | |
293 | }; |
|
295 | }; | |
294 | } |
|
296 | } | |
295 |
|
297 | |||
296 | subcommands! { |
|
298 | subcommands! { | |
297 | cat |
|
299 | cat | |
298 | debugdata |
|
300 | debugdata | |
299 | debugrequirements |
|
301 | debugrequirements | |
300 | files |
|
302 | files | |
301 | root |
|
303 | root | |
302 | config |
|
304 | config | |
303 | } |
|
305 | } | |
304 | pub struct CliInvocation<'a> { |
|
306 | pub struct CliInvocation<'a> { | |
305 | ui: &'a Ui, |
|
307 | ui: &'a Ui, | |
306 | subcommand_args: &'a ArgMatches<'a>, |
|
308 | subcommand_args: &'a ArgMatches<'a>, | |
307 | config: &'a Config, |
|
309 | config: &'a Config, | |
308 | /// References inside `Result` is a bit peculiar but allow |
|
310 | /// References inside `Result` is a bit peculiar but allow | |
309 | /// `invocation.repo?` to work out with `&CliInvocation` since this |
|
311 | /// `invocation.repo?` to work out with `&CliInvocation` since this | |
310 | /// `Result` type is `Copy`. |
|
312 | /// `Result` type is `Copy`. | |
311 | repo: Result<&'a Repo, &'a NoRepoInCwdError>, |
|
313 | repo: Result<&'a Repo, &'a NoRepoInCwdError>, | |
312 | } |
|
314 | } | |
313 |
|
315 | |||
314 | struct NoRepoInCwdError { |
|
316 | struct NoRepoInCwdError { | |
315 | cwd: PathBuf, |
|
317 | cwd: PathBuf, | |
316 | } |
|
318 | } | |
317 |
|
319 | |||
318 | /// CLI arguments to be parsed "early" in order to be able to read |
|
320 | /// CLI arguments to be parsed "early" in order to be able to read | |
319 | /// configuration before using Clap. Ideally we would also use Clap for this, |
|
321 | /// configuration before using Clap. Ideally we would also use Clap for this, | |
320 | /// see <https://github.com/clap-rs/clap/discussions/2366>. |
|
322 | /// see <https://github.com/clap-rs/clap/discussions/2366>. | |
321 | /// |
|
323 | /// | |
322 | /// These arguments are still declared when we do use Clap later, so that Clap |
|
324 | /// These arguments are still declared when we do use Clap later, so that Clap | |
323 | /// does not return an error for their presence. |
|
325 | /// does not return an error for their presence. | |
324 | struct EarlyArgs { |
|
326 | struct EarlyArgs { | |
325 | /// Values of all `--config` arguments. (Possibly none) |
|
327 | /// Values of all `--config` arguments. (Possibly none) | |
326 | config: Vec<Vec<u8>>, |
|
328 | config: Vec<Vec<u8>>, | |
327 | /// Value of the `-R` or `--repository` argument, if any. |
|
329 | /// Value of the `-R` or `--repository` argument, if any. | |
328 | repo: Option<Vec<u8>>, |
|
330 | repo: Option<Vec<u8>>, | |
329 | /// Value of the `--cwd` argument, if any. |
|
331 | /// Value of the `--cwd` argument, if any. | |
330 | cwd: Option<Vec<u8>>, |
|
332 | cwd: Option<Vec<u8>>, | |
331 | } |
|
333 | } | |
332 |
|
334 | |||
333 | impl EarlyArgs { |
|
335 | impl EarlyArgs { | |
334 | fn parse(args: impl IntoIterator<Item = OsString>) -> Self { |
|
336 | fn parse(args: impl IntoIterator<Item = OsString>) -> Self { | |
335 | let mut args = args.into_iter().map(get_bytes_from_os_str); |
|
337 | let mut args = args.into_iter().map(get_bytes_from_os_str); | |
336 | let mut config = Vec::new(); |
|
338 | let mut config = Vec::new(); | |
337 | let mut repo = None; |
|
339 | let mut repo = None; | |
338 | let mut cwd = None; |
|
340 | let mut cwd = None; | |
339 | // Use `while let` instead of `for` so that we can also call |
|
341 | // Use `while let` instead of `for` so that we can also call | |
340 | // `args.next()` inside the loop. |
|
342 | // `args.next()` inside the loop. | |
341 | while let Some(arg) = args.next() { |
|
343 | while let Some(arg) = args.next() { | |
342 | if arg == b"--config" { |
|
344 | if arg == b"--config" { | |
343 | if let Some(value) = args.next() { |
|
345 | if let Some(value) = args.next() { | |
344 | config.push(value) |
|
346 | config.push(value) | |
345 | } |
|
347 | } | |
346 | } else if let Some(value) = arg.drop_prefix(b"--config=") { |
|
348 | } else if let Some(value) = arg.drop_prefix(b"--config=") { | |
347 | config.push(value.to_owned()) |
|
349 | config.push(value.to_owned()) | |
348 | } |
|
350 | } | |
349 |
|
351 | |||
350 | if arg == b"--cwd" { |
|
352 | if arg == b"--cwd" { | |
351 | if let Some(value) = args.next() { |
|
353 | if let Some(value) = args.next() { | |
352 | cwd = Some(value) |
|
354 | cwd = Some(value) | |
353 | } |
|
355 | } | |
354 | } else if let Some(value) = arg.drop_prefix(b"--cwd=") { |
|
356 | } else if let Some(value) = arg.drop_prefix(b"--cwd=") { | |
355 | cwd = Some(value.to_owned()) |
|
357 | cwd = Some(value.to_owned()) | |
356 | } |
|
358 | } | |
357 |
|
359 | |||
358 | if arg == b"--repository" || arg == b"-R" { |
|
360 | if arg == b"--repository" || arg == b"-R" { | |
359 | if let Some(value) = args.next() { |
|
361 | if let Some(value) = args.next() { | |
360 | repo = Some(value) |
|
362 | repo = Some(value) | |
361 | } |
|
363 | } | |
362 | } else if let Some(value) = arg.drop_prefix(b"--repository=") { |
|
364 | } else if let Some(value) = arg.drop_prefix(b"--repository=") { | |
363 | repo = Some(value.to_owned()) |
|
365 | repo = Some(value.to_owned()) | |
364 | } else if let Some(value) = arg.drop_prefix(b"-R") { |
|
366 | } else if let Some(value) = arg.drop_prefix(b"-R") { | |
365 | repo = Some(value.to_owned()) |
|
367 | repo = Some(value.to_owned()) | |
366 | } |
|
368 | } | |
367 | } |
|
369 | } | |
368 | Self { config, repo, cwd } |
|
370 | Self { config, repo, cwd } | |
369 | } |
|
371 | } | |
370 | } |
|
372 | } | |
371 |
|
373 | |||
372 | /// What to do when encountering some unsupported feature. |
|
374 | /// What to do when encountering some unsupported feature. | |
373 | /// |
|
375 | /// | |
374 | /// See `HgError::UnsupportedFeature` and `CommandError::UnsupportedFeature`. |
|
376 | /// See `HgError::UnsupportedFeature` and `CommandError::UnsupportedFeature`. | |
375 | enum OnUnsupported { |
|
377 | enum OnUnsupported { | |
376 | /// Print an error message describing what feature is not supported, |
|
378 | /// Print an error message describing what feature is not supported, | |
377 | /// and exit with code 252. |
|
379 | /// and exit with code 252. | |
378 | Abort, |
|
380 | Abort, | |
379 | /// Silently exit with code 252. |
|
381 | /// Silently exit with code 252. | |
380 | AbortSilent, |
|
382 | AbortSilent, | |
381 | /// Try running a Python implementation |
|
383 | /// Try running a Python implementation | |
382 | Fallback { executable: Vec<u8> }, |
|
384 | Fallback { executable: Vec<u8> }, | |
383 | } |
|
385 | } | |
384 |
|
386 | |||
385 | impl OnUnsupported { |
|
387 | impl OnUnsupported { | |
386 | const DEFAULT: Self = OnUnsupported::Abort; |
|
388 | const DEFAULT: Self = OnUnsupported::Abort; | |
387 | const DEFAULT_FALLBACK_EXECUTABLE: &'static [u8] = b"hg"; |
|
389 | const DEFAULT_FALLBACK_EXECUTABLE: &'static [u8] = b"hg"; | |
388 |
|
390 | |||
389 | fn from_config(config: &Config) -> Self { |
|
391 | fn from_config(config: &Config) -> Self { | |
390 | match config |
|
392 | match config | |
391 | .get(b"rhg", b"on-unsupported") |
|
393 | .get(b"rhg", b"on-unsupported") | |
392 | .map(|value| value.to_ascii_lowercase()) |
|
394 | .map(|value| value.to_ascii_lowercase()) | |
393 | .as_deref() |
|
395 | .as_deref() | |
394 | { |
|
396 | { | |
395 | Some(b"abort") => OnUnsupported::Abort, |
|
397 | Some(b"abort") => OnUnsupported::Abort, | |
396 | Some(b"abort-silent") => OnUnsupported::AbortSilent, |
|
398 | Some(b"abort-silent") => OnUnsupported::AbortSilent, | |
397 | Some(b"fallback") => OnUnsupported::Fallback { |
|
399 | Some(b"fallback") => OnUnsupported::Fallback { | |
398 | executable: config |
|
400 | executable: config | |
399 | .get(b"rhg", b"fallback-executable") |
|
401 | .get(b"rhg", b"fallback-executable") | |
400 | .unwrap_or(Self::DEFAULT_FALLBACK_EXECUTABLE) |
|
402 | .unwrap_or(Self::DEFAULT_FALLBACK_EXECUTABLE) | |
401 | .to_owned(), |
|
403 | .to_owned(), | |
402 | }, |
|
404 | }, | |
403 | None => Self::DEFAULT, |
|
405 | None => Self::DEFAULT, | |
404 | Some(_) => { |
|
406 | Some(_) => { | |
405 | // TODO: warn about unknown config value |
|
407 | // TODO: warn about unknown config value | |
406 | Self::DEFAULT |
|
408 | Self::DEFAULT | |
407 | } |
|
409 | } | |
408 | } |
|
410 | } | |
409 | } |
|
411 | } | |
410 | } |
|
412 | } | |
411 |
|
413 | |||
412 | const SUPPORTED_EXTENSIONS: &[&[u8]] = &[b"blackbox", b"share"]; |
|
414 | const SUPPORTED_EXTENSIONS: &[&[u8]] = &[b"blackbox", b"share"]; | |
413 |
|
415 | |||
414 | fn check_extensions(config: &Config) -> Result<(), CommandError> { |
|
416 | fn check_extensions(config: &Config) -> Result<(), CommandError> { | |
415 | let enabled = config.get_section_keys(b"extensions"); |
|
417 | let enabled = config.get_section_keys(b"extensions"); | |
416 |
|
418 | |||
417 | let mut unsupported = enabled; |
|
419 | let mut unsupported = enabled; | |
418 | for supported in SUPPORTED_EXTENSIONS { |
|
420 | for supported in SUPPORTED_EXTENSIONS { | |
419 | unsupported.remove(supported); |
|
421 | unsupported.remove(supported); | |
420 | } |
|
422 | } | |
421 |
|
423 | |||
422 | if let Some(ignored_list) = |
|
424 | if let Some(ignored_list) = | |
423 | config.get_simple_list(b"rhg", b"ignored-extensions") |
|
425 | config.get_simple_list(b"rhg", b"ignored-extensions") | |
424 | { |
|
426 | { | |
425 | for ignored in ignored_list { |
|
427 | for ignored in ignored_list { | |
426 | unsupported.remove(ignored); |
|
428 | unsupported.remove(ignored); | |
427 | } |
|
429 | } | |
428 | } |
|
430 | } | |
429 |
|
431 | |||
430 | if unsupported.is_empty() { |
|
432 | if unsupported.is_empty() { | |
431 | Ok(()) |
|
433 | Ok(()) | |
432 | } else { |
|
434 | } else { | |
433 | Err(CommandError::UnsupportedFeature { |
|
435 | Err(CommandError::UnsupportedFeature { | |
434 | message: format_bytes!( |
|
436 | message: format_bytes!( | |
435 | b"extensions: {} (consider adding them to 'rhg.ignored-extensions' config)", |
|
437 | b"extensions: {} (consider adding them to 'rhg.ignored-extensions' config)", | |
436 | join(unsupported, b", ") |
|
438 | join(unsupported, b", ") | |
437 | ), |
|
439 | ), | |
438 | }) |
|
440 | }) | |
439 | } |
|
441 | } | |
440 | } |
|
442 | } |
General Comments 0
You need to be logged in to leave comments.
Login now