rhg: `cat` command: print error messages for missing files
authorSimon Sapin <simon.sapin@octobus.net>
Wed, 03 Mar 2021 16:40:03 +0100
changeset 46757 b1f2c2b336ec
parent 46756 84a3deca963a
child 46758 63bfcddddac1
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
rust/hg-core/src/operations/cat.rs
rust/hg-core/src/operations/mod.rs
rust/hg-core/src/revlog/changelog.rs
rust/hg-core/src/revlog/node.rs
rust/hg-core/src/revlog/revlog.rs
rust/rhg/src/commands/cat.rs
rust/rhg/src/error.rs
rust/rhg/src/exitcode.rs
rust/rhg/src/main.rs
--- 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