diff --git a/rust/hg-core/src/operations/cat.rs b/rust/hg-core/src/operations/cat.rs --- a/rust/hg-core/src/operations/cat.rs +++ b/rust/hg-core/src/operations/cat.rs @@ -17,30 +17,49 @@ use crate::utils::files::get_path_from_bytes; use crate::utils::hg_path::{HgPath, HgPathBuf}; +pub struct CatOutput { + /// Whether any file in the manifest matched the paths given as CLI + /// arguments + pub found_any: bool, + /// The contents of matching files, in manifest order + pub concatenated: Vec, + /// Which of the CLI arguments did not match any manifest file + pub missing: Vec, + /// The node ID that the given revset was resolved to + pub node: Node, +} + const METADATA_DELIMITER: [u8; 2] = [b'\x01', b'\n']; -/// List files under Mercurial control at a given revision. +/// Output the given revision of files /// /// * `root`: Repository root /// * `rev`: The revision to cat the files from. /// * `files`: The files to output. -pub fn cat( +pub fn cat<'a>( repo: &Repo, revset: &str, - files: &[HgPathBuf], -) -> Result, RevlogError> { + files: &'a [HgPathBuf], +) -> Result { let rev = crate::revset::resolve_single(revset, repo)?; let changelog = Changelog::open(repo)?; let manifest = Manifest::open(repo)?; let changelog_entry = changelog.get_rev(rev)?; + let node = *changelog + .node_from_rev(rev) + .expect("should succeed when changelog.get_rev did"); let manifest_node = Node::from_hex_for_repo(&changelog_entry.manifest_node()?)?; let manifest_entry = manifest.get_node(manifest_node.into())?; let mut bytes = vec![]; + let mut matched = vec![false; files.len()]; + let mut found_any = false; for (manifest_file, node_bytes) in manifest_entry.files_with_nodes() { - for cat_file in files.iter() { + for (cat_file, is_matched) in files.iter().zip(&mut matched) { if cat_file.as_bytes() == manifest_file.as_bytes() { + *is_matched = true; + found_any = true; let index_path = store_path(manifest_file, b".i"); let data_path = store_path(manifest_file, b".d"); @@ -65,7 +84,18 @@ } } - Ok(bytes) + let missing: Vec<_> = files + .iter() + .zip(&matched) + .filter(|pair| !*pair.1) + .map(|pair| pair.0.clone()) + .collect(); + Ok(CatOutput { + found_any, + concatenated: bytes, + missing, + node, + }) } fn store_path(hg_path: &HgPath, suffix: &[u8]) -> PathBuf { diff --git a/rust/hg-core/src/operations/mod.rs b/rust/hg-core/src/operations/mod.rs --- a/rust/hg-core/src/operations/mod.rs +++ b/rust/hg-core/src/operations/mod.rs @@ -6,7 +6,7 @@ mod debugdata; mod dirstate_status; mod list_tracked_files; -pub use cat::cat; +pub use cat::{cat, CatOutput}; pub use debugdata::{debug_data, DebugDataKind}; pub use list_tracked_files::Dirstate; pub use list_tracked_files::{list_rev_tracked_files, FilesForRev}; diff --git a/rust/hg-core/src/revlog/changelog.rs b/rust/hg-core/src/revlog/changelog.rs --- a/rust/hg-core/src/revlog/changelog.rs +++ b/rust/hg-core/src/revlog/changelog.rs @@ -1,8 +1,8 @@ use crate::errors::HgError; use crate::repo::Repo; use crate::revlog::revlog::{Revlog, RevlogError}; -use crate::revlog::NodePrefix; use crate::revlog::Revision; +use crate::revlog::{Node, NodePrefix}; /// A specialized `Revlog` to work with `changelog` data format. pub struct Changelog { @@ -34,6 +34,10 @@ let bytes = self.revlog.get_rev_data(rev)?; Ok(ChangelogEntry { bytes }) } + + pub fn node_from_rev(&self, rev: Revision) -> Option<&Node> { + Some(self.revlog.index.get_entry(rev)?.hash()) + } } /// `Changelog` entry which knows how to interpret the `changelog` data bytes. diff --git a/rust/hg-core/src/revlog/node.rs b/rust/hg-core/src/revlog/node.rs --- a/rust/hg-core/src/revlog/node.rs +++ b/rust/hg-core/src/revlog/node.rs @@ -31,6 +31,9 @@ /// see also `NODES_BYTES_LENGTH` about it being private. const NODE_NYBBLES_LENGTH: usize = 2 * NODE_BYTES_LENGTH; +/// Default for UI presentation +const SHORT_PREFIX_DEFAULT_NYBBLES_LENGTH: u8 = 12; + /// Private alias for readability and to ease future change type NodeData = [u8; NODE_BYTES_LENGTH]; @@ -164,6 +167,13 @@ pub fn as_bytes(&self) -> &[u8] { &self.data } + + pub fn short(&self) -> NodePrefix { + NodePrefix { + nybbles_len: SHORT_PREFIX_DEFAULT_NYBBLES_LENGTH, + data: self.data, + } + } } /// The beginning of a binary revision SHA. diff --git a/rust/hg-core/src/revlog/revlog.rs b/rust/hg-core/src/revlog/revlog.rs --- a/rust/hg-core/src/revlog/revlog.rs +++ b/rust/hg-core/src/revlog/revlog.rs @@ -49,7 +49,7 @@ /// When index and data are not interleaved: bytes of the revlog index. /// When index and data are interleaved: bytes of the revlog index and /// data. - index: Index, + pub(crate) index: Index, /// When index and data are not interleaved: bytes of the revlog data data_bytes: Option + Send>>, /// When present on disk: the persistent nodemap for this revlog diff --git a/rust/rhg/src/commands/cat.rs b/rust/rhg/src/commands/cat.rs --- a/rust/rhg/src/commands/cat.rs +++ b/rust/rhg/src/commands/cat.rs @@ -1,5 +1,6 @@ use crate::error::CommandError; use clap::Arg; +use format_bytes::format_bytes; use hg::operations::cat; use hg::utils::hg_path::HgPathBuf; use micro_timer::timed; @@ -58,9 +59,23 @@ match rev { Some(rev) => { - let data = cat(&repo, rev, &files).map_err(|e| (e, rev))?; - invocation.ui.write_stdout(&data)?; - Ok(()) + let output = cat(&repo, rev, &files).map_err(|e| (e, rev))?; + invocation.ui.write_stdout(&output.concatenated)?; + if !output.missing.is_empty() { + let short = format!("{:x}", output.node.short()).into_bytes(); + for path in &output.missing { + invocation.ui.write_stderr(&format_bytes!( + b"{}: no such file in rev {}\n", + path.as_bytes(), + short + ))?; + } + } + if output.found_any { + Ok(()) + } else { + Err(CommandError::Unsuccessful) + } } None => Err(CommandError::unsupported( "`rhg cat` without `--rev` / `-r`", diff --git a/rust/rhg/src/error.rs b/rust/rhg/src/error.rs --- a/rust/rhg/src/error.rs +++ b/rust/rhg/src/error.rs @@ -15,6 +15,9 @@ /// Exit with an error message and "standard" failure exit code. Abort { message: Vec }, + /// Exit with a failure exit code but no message. + Unsuccessful, + /// Encountered something (such as a CLI argument, repository layout, …) /// not supported by this version of `rhg`. Depending on configuration /// `rhg` may attempt to silently fall back to Python-based `hg`, which diff --git a/rust/rhg/src/exitcode.rs b/rust/rhg/src/exitcode.rs --- a/rust/rhg/src/exitcode.rs +++ b/rust/rhg/src/exitcode.rs @@ -6,5 +6,8 @@ /// Generic abort pub const ABORT: ExitCode = 255; +/// Generic something completed but did not succeed +pub const UNSUCCESSFUL: ExitCode = 1; + /// Command or feature not implemented by rhg pub const UNIMPLEMENTED: ExitCode = 252; diff --git a/rust/rhg/src/main.rs b/rust/rhg/src/main.rs --- a/rust/rhg/src/main.rs +++ b/rust/rhg/src/main.rs @@ -186,6 +186,7 @@ match result { Ok(()) => exitcode::OK, Err(CommandError::Abort { .. }) => exitcode::ABORT, + Err(CommandError::Unsuccessful) => exitcode::UNSUCCESSFUL, // Exit with a specific code and no error message to let a potential // wrapper script fallback to Python-based Mercurial. @@ -242,6 +243,7 @@ } match &result { Ok(_) => {} + Err(CommandError::Unsuccessful) => {} Err(CommandError::Abort { message }) => { if !message.is_empty() { // Ignore errors when writing to stderr, we’re already exiting