rhg-status: add support for narrow clones
authorRaphaël Gomès <rgomes@octobus.net>
Mon, 25 Jul 2022 15:39:04 +0200
changeset 49489 7c93e38a0bbd
parent 49488 9f14126cfc4c
child 49491 c6a1beba27e9
rhg-status: add support for narrow clones
rust/hg-core/src/exit_codes.rs
rust/hg-core/src/filepatterns.rs
rust/hg-core/src/lib.rs
rust/hg-core/src/narrow.rs
rust/hg-core/src/sparse.rs
rust/rhg/src/commands/status.rs
rust/rhg/src/error.rs
tests/test-rhg-sparse-narrow.t
--- a/rust/hg-core/src/exit_codes.rs	Tue Jul 19 17:07:09 2022 +0200
+++ b/rust/hg-core/src/exit_codes.rs	Mon Jul 25 15:39:04 2022 +0200
@@ -9,6 +9,10 @@
 // Abort when there is a config related error
 pub const CONFIG_ERROR_ABORT: ExitCode = 30;
 
+/// Indicates that the operation might work if retried in a different state.
+/// Examples: Unresolved merge conflicts, unfinished operations
+pub const STATE_ERROR: ExitCode = 20;
+
 // Abort when there is an error while parsing config
 pub const CONFIG_PARSE_ERROR_ABORT: ExitCode = 10;
 
--- a/rust/hg-core/src/filepatterns.rs	Tue Jul 19 17:07:09 2022 +0200
+++ b/rust/hg-core/src/filepatterns.rs	Mon Jul 25 15:39:04 2022 +0200
@@ -314,6 +314,8 @@
         m.insert(b"rootglob".as_ref(), b"rootglob:".as_ref());
         m.insert(b"include".as_ref(), b"include:".as_ref());
         m.insert(b"subinclude".as_ref(), b"subinclude:".as_ref());
+        m.insert(b"path".as_ref(), b"path:".as_ref());
+        m.insert(b"rootfilesin".as_ref(), b"rootfilesin:".as_ref());
         m
     };
 }
--- a/rust/hg-core/src/lib.rs	Tue Jul 19 17:07:09 2022 +0200
+++ b/rust/hg-core/src/lib.rs	Mon Jul 25 15:39:04 2022 +0200
@@ -7,6 +7,7 @@
 mod ancestors;
 pub mod dagops;
 pub mod errors;
+pub mod narrow;
 pub mod sparse;
 pub use ancestors::{AncestorsIterator, MissingAncestors};
 pub mod dirstate;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rust/hg-core/src/narrow.rs	Mon Jul 25 15:39:04 2022 +0200
@@ -0,0 +1,111 @@
+use std::path::Path;
+
+use crate::{
+    errors::HgError,
+    exit_codes,
+    filepatterns::parse_pattern_file_contents,
+    matchers::{
+        AlwaysMatcher, DifferenceMatcher, IncludeMatcher, Matcher,
+        NeverMatcher,
+    },
+    repo::Repo,
+    requirements::NARROW_REQUIREMENT,
+    sparse::{self, SparseConfigError, SparseWarning},
+};
+
+/// The file in .hg/store/ that indicates which paths exit in the store
+const FILENAME: &str = "narrowspec";
+/// The file in .hg/ that indicates which paths exit in the dirstate
+const DIRSTATE_FILENAME: &str = "narrowspec.dirstate";
+
+/// Pattern prefixes that are allowed in narrow patterns. This list MUST
+/// only contain patterns that are fast and safe to evaluate. Keep in mind
+/// that patterns are supplied by clients and executed on remote servers
+/// as part of wire protocol commands. That means that changes to this
+/// data structure influence the wire protocol and should not be taken
+/// lightly - especially removals.
+const VALID_PREFIXES: [&str; 2] = ["path:", "rootfilesin:"];
+
+/// Return the matcher for the current narrow spec, and all configuration
+/// warnings to display.
+pub fn matcher(
+    repo: &Repo,
+) -> Result<(Box<dyn Matcher + Sync>, Vec<SparseWarning>), SparseConfigError> {
+    let mut warnings = vec![];
+    if !repo.requirements().contains(NARROW_REQUIREMENT) {
+        return Ok((Box::new(AlwaysMatcher), warnings));
+    }
+    // Treat "narrowspec does not exist" the same as "narrowspec file exists
+    // and is empty".
+    let store_spec = repo.store_vfs().try_read(FILENAME)?.unwrap_or(vec![]);
+    let working_copy_spec =
+        repo.hg_vfs().try_read(DIRSTATE_FILENAME)?.unwrap_or(vec![]);
+    if store_spec != working_copy_spec {
+        return Err(HgError::abort(
+            "working copy's narrowspec is stale",
+            exit_codes::STATE_ERROR,
+            Some("run 'hg tracked --update-working-copy'".into()),
+        )
+        .into());
+    }
+
+    let config = sparse::parse_config(
+        &store_spec,
+        sparse::SparseConfigContext::Narrow,
+    )?;
+
+    warnings.extend(config.warnings);
+
+    if !config.profiles.is_empty() {
+        // TODO (from Python impl) maybe do something with profiles?
+        return Err(SparseConfigError::IncludesInNarrow);
+    }
+    validate_patterns(&config.includes)?;
+    validate_patterns(&config.excludes)?;
+
+    if config.includes.is_empty() {
+        return Ok((Box::new(NeverMatcher), warnings));
+    }
+
+    let (patterns, subwarnings) = parse_pattern_file_contents(
+        &config.includes,
+        Path::new(""),
+        None,
+        false,
+    )?;
+    warnings.extend(subwarnings.into_iter().map(From::from));
+
+    let mut m: Box<dyn Matcher + Sync> =
+        Box::new(IncludeMatcher::new(patterns)?);
+
+    let (patterns, subwarnings) = parse_pattern_file_contents(
+        &config.excludes,
+        Path::new(""),
+        None,
+        false,
+    )?;
+    if !patterns.is_empty() {
+        warnings.extend(subwarnings.into_iter().map(From::from));
+        let exclude_matcher = Box::new(IncludeMatcher::new(patterns)?);
+        m = Box::new(DifferenceMatcher::new(m, exclude_matcher));
+    }
+
+    Ok((m, warnings))
+}
+
+fn validate_patterns(patterns: &[u8]) -> Result<(), SparseConfigError> {
+    for pattern in patterns.split(|c| *c == b'\n') {
+        if pattern.is_empty() {
+            continue;
+        }
+        for prefix in VALID_PREFIXES.iter() {
+            if pattern.starts_with(prefix.as_bytes()) {
+                break;
+            }
+            return Err(SparseConfigError::InvalidNarrowPrefix(
+                pattern.to_owned(),
+            ));
+        }
+    }
+    Ok(())
+}
--- a/rust/hg-core/src/sparse.rs	Tue Jul 19 17:07:09 2022 +0200
+++ b/rust/hg-core/src/sparse.rs	Mon Jul 25 15:39:04 2022 +0200
@@ -54,14 +54,14 @@
 #[derive(Debug, Default)]
 pub struct SparseConfig {
     // Line-separated
-    includes: Vec<u8>,
+    pub(crate) includes: Vec<u8>,
     // Line-separated
-    excludes: Vec<u8>,
-    profiles: HashSet<Vec<u8>>,
-    warnings: Vec<SparseWarning>,
+    pub(crate) excludes: Vec<u8>,
+    pub(crate) profiles: HashSet<Vec<u8>>,
+    pub(crate) warnings: Vec<SparseWarning>,
 }
 
-/// All possible errors when reading sparse config
+/// All possible errors when reading sparse/narrow config
 #[derive(Debug, derive_more::From)]
 pub enum SparseConfigError {
     IncludesAfterExcludes {
@@ -71,6 +71,11 @@
         context: SparseConfigContext,
         line: Vec<u8>,
     },
+    /// Narrow config does not support '%include' directives
+    IncludesInNarrow,
+    /// An invalid pattern prefix was given to the narrow spec. Includes the
+    /// entire pattern for context.
+    InvalidNarrowPrefix(Vec<u8>),
     #[from]
     HgError(HgError),
     #[from]
@@ -78,7 +83,7 @@
 }
 
 /// Parse sparse config file content.
-fn parse_config(
+pub(crate) fn parse_config(
     raw: &[u8],
     context: SparseConfigContext,
 ) -> Result<SparseConfig, SparseConfigError> {
--- a/rust/rhg/src/commands/status.rs	Tue Jul 19 17:07:09 2022 +0200
+++ b/rust/rhg/src/commands/status.rs	Mon Jul 25 15:39:04 2022 +0200
@@ -10,7 +10,6 @@
 use crate::utils::path_utils::RelativizePaths;
 use clap::{Arg, SubCommand};
 use format_bytes::format_bytes;
-use hg;
 use hg::config::Config;
 use hg::dirstate::has_exec_bit;
 use hg::dirstate::status::StatusPath;
@@ -18,8 +17,8 @@
 use hg::errors::{HgError, IoResultExt};
 use hg::lock::LockError;
 use hg::manifest::Manifest;
+use hg::matchers::{AlwaysMatcher, IntersectionMatcher};
 use hg::repo::Repo;
-use hg::sparse::{matcher, SparseWarning};
 use hg::utils::files::get_bytes_from_os_string;
 use hg::utils::files::get_bytes_from_path;
 use hg::utils::files::get_path_from_bytes;
@@ -28,6 +27,7 @@
 use hg::PatternFileWarning;
 use hg::StatusError;
 use hg::StatusOptions;
+use hg::{self, narrow, sparse};
 use log::info;
 use std::io;
 use std::path::PathBuf;
@@ -251,12 +251,6 @@
         };
     }
 
-    if repo.has_narrow() {
-        return Err(CommandError::unsupported(
-            "rhg status is not supported for narrow clones yet",
-        ));
-    }
-
     let mut dmap = repo.dirstate_map_mut()?;
 
     let options = StatusOptions {
@@ -366,11 +360,20 @@
             filesystem_time_at_status_start,
         ))
     };
-    let (matcher, sparse_warnings) = matcher(repo)?;
+    let (narrow_matcher, narrow_warnings) = narrow::matcher(repo)?;
+    let (sparse_matcher, sparse_warnings) = sparse::matcher(repo)?;
+    let matcher = match (repo.has_narrow(), repo.has_sparse()) {
+        (true, true) => {
+            Box::new(IntersectionMatcher::new(narrow_matcher, sparse_matcher))
+        }
+        (true, false) => narrow_matcher,
+        (false, true) => sparse_matcher,
+        (false, false) => Box::new(AlwaysMatcher),
+    };
 
-    for warning in sparse_warnings {
+    for warning in narrow_warnings.into_iter().chain(sparse_warnings) {
         match &warning {
-            SparseWarning::RootWarning { context, line } => {
+            sparse::SparseWarning::RootWarning { context, line } => {
                 let msg = format_bytes!(
                     b"warning: {} profile cannot use paths \"
                     starting with /, ignoring {}\n",
@@ -379,7 +382,7 @@
                 );
                 ui.write_stderr(&msg)?;
             }
-            SparseWarning::ProfileNotFound { profile, rev } => {
+            sparse::SparseWarning::ProfileNotFound { profile, rev } => {
                 let msg = format_bytes!(
                     b"warning: sparse profile '{}' not found \"
                     in rev {} - ignoring it\n",
@@ -388,7 +391,7 @@
                 );
                 ui.write_stderr(&msg)?;
             }
-            SparseWarning::Pattern(e) => {
+            sparse::SparseWarning::Pattern(e) => {
                 ui.write_stderr(&print_pattern_file_warning(e, &repo))?;
             }
         }
--- a/rust/rhg/src/error.rs	Tue Jul 19 17:07:09 2022 +0200
+++ b/rust/rhg/src/error.rs	Mon Jul 25 15:39:04 2022 +0200
@@ -268,6 +268,19 @@
                     exit_codes::CONFIG_PARSE_ERROR_ABORT,
                 )
             }
+            SparseConfigError::InvalidNarrowPrefix(prefix) => {
+                Self::abort_with_exit_code_bytes(
+                    format_bytes!(
+                        b"invalid prefix on narrow pattern: {}",
+                        &prefix
+                    ),
+                    exit_codes::ABORT,
+                )
+            }
+            SparseConfigError::IncludesInNarrow => Self::abort(
+                "including other spec files using '%include' \
+                    is not supported in narrowspec",
+            ),
             SparseConfigError::HgError(e) => Self::from(e),
             SparseConfigError::PatternError(e) => {
                 Self::unsupported(format!("{}", e))
--- a/tests/test-rhg-sparse-narrow.t	Tue Jul 19 17:07:09 2022 +0200
+++ b/tests/test-rhg-sparse-narrow.t	Mon Jul 25 15:39:04 2022 +0200
@@ -85,15 +85,12 @@
   dir1/x
   dir1/y
 
-Hg status needs to do some filtering based on narrow spec, so we don't
-support it in rhg for narrow clones yet.
+Hg status needs to do some filtering based on narrow spec
 
   $ mkdir dir2
   $ touch dir2/q
   $ "$real_hg" status
   $ $NO_FALLBACK rhg --config rhg.status=true status
-  unsupported feature: rhg status is not supported for narrow clones yet
-  [252]
 
 Adding "orphaned" index files: