rhg: `cat` command: print error messages for missing files
And exit with an error code if no file was matched.
This matches the behavior of Python-based hg.
Differential Revision: https://phab.mercurial-scm.org/D10142
--- a/rust/hg-core/src/operations/cat.rs Mon Mar 08 19:07:29 2021 +0100
+++ b/rust/hg-core/src/operations/cat.rs Wed Mar 03 16:40:03 2021 +0100
@@ -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<u8>,
+ /// Which of the CLI arguments did not match any manifest file
+ pub missing: Vec<HgPathBuf>,
+ /// 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<Vec<u8>, RevlogError> {
+ files: &'a [HgPathBuf],
+) -> Result<CatOutput, RevlogError> {
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 {
--- a/rust/hg-core/src/operations/mod.rs Mon Mar 08 19:07:29 2021 +0100
+++ b/rust/hg-core/src/operations/mod.rs Wed Mar 03 16:40:03 2021 +0100
@@ -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};
--- a/rust/hg-core/src/revlog/changelog.rs Mon Mar 08 19:07:29 2021 +0100
+++ b/rust/hg-core/src/revlog/changelog.rs Wed Mar 03 16:40:03 2021 +0100
@@ -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.
--- a/rust/hg-core/src/revlog/node.rs Mon Mar 08 19:07:29 2021 +0100
+++ b/rust/hg-core/src/revlog/node.rs Wed Mar 03 16:40:03 2021 +0100
@@ -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.
--- a/rust/hg-core/src/revlog/revlog.rs Mon Mar 08 19:07:29 2021 +0100
+++ b/rust/hg-core/src/revlog/revlog.rs Wed Mar 03 16:40:03 2021 +0100
@@ -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<Box<dyn Deref<Target = [u8]> + Send>>,
/// When present on disk: the persistent nodemap for this revlog
--- a/rust/rhg/src/commands/cat.rs Mon Mar 08 19:07:29 2021 +0100
+++ b/rust/rhg/src/commands/cat.rs Wed Mar 03 16:40:03 2021 +0100
@@ -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`",
--- a/rust/rhg/src/error.rs Mon Mar 08 19:07:29 2021 +0100
+++ b/rust/rhg/src/error.rs Wed Mar 03 16:40:03 2021 +0100
@@ -15,6 +15,9 @@
/// Exit with an error message and "standard" failure exit code.
Abort { message: Vec<u8> },
+ /// 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
--- a/rust/rhg/src/exitcode.rs Mon Mar 08 19:07:29 2021 +0100
+++ b/rust/rhg/src/exitcode.rs Wed Mar 03 16:40:03 2021 +0100
@@ -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;
--- a/rust/rhg/src/main.rs Mon Mar 08 19:07:29 2021 +0100
+++ b/rust/rhg/src/main.rs Wed Mar 03 16:40:03 2021 +0100
@@ -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