changeset 52882:38e16da74aea

rust-pyo3-dirstate: status This finally makes use of the latest `path` utilities. As a side note, `paths_py_list` cannot use `PyHgPathRef` itself because by calling `as_ref()` it would make the compiler believe it returns code owner by the inner `map` closure. Also, `status_path_py_list(py, &status_res.modified)?` can probably be replaced by ``` PyList::new(status_res.modified.iter().map(|p| PyHgPathRef(p)))? ``` with (granted) little benefit, but it could spare us a new utility each time there is a new collection type to return.
author Georges Racinet <georges.racinet@cloudcrane.io>
date Thu, 06 Feb 2025 15:03:58 +0100
parents 8c11ec902e73
children 0bd91b0a1a93
files rust/hg-pyo3/src/dirstate.rs rust/hg-pyo3/src/dirstate/status.rs rust/hg-pyo3/src/path.rs
diffstat 3 files changed, 276 insertions(+), 2 deletions(-) [+]
line wrap: on
line diff
--- a/rust/hg-pyo3/src/dirstate.rs	Wed Feb 05 11:20:39 2025 +0100
+++ b/rust/hg-pyo3/src/dirstate.rs	Thu Feb 06 15:03:58 2025 +0100
@@ -23,6 +23,7 @@
 use copy_map::{CopyMap, CopyMapItemsIterator, CopyMapKeysIterator};
 mod dirs_multiset;
 use dirs_multiset::{Dirs, DirsMultisetKeysIterator};
+mod status;
 
 pub fn init_module<'py>(
     py: Python<'py>,
@@ -41,5 +42,6 @@
     m.add_class::<CopyMapItemsIterator>()?;
     m.add_class::<Dirs>()?;
     m.add_class::<DirsMultisetKeysIterator>()?;
+    m.add_function(wrap_pyfunction!(self::status::status, &m)?)?;
     Ok(m)
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rust/hg-pyo3/src/dirstate/status.rs	Thu Feb 06 15:03:58 2025 +0100
@@ -0,0 +1,274 @@
+// status.rs
+//
+// Copyright 2019 Raphaël Gomès <rgomes@octobus.net>
+//           2025 Georges Racinet <georges.racinet@cloudcrane.io>
+//
+// This software may be used and distributed according to the terms of the
+// GNU General Public License version 2 or any later version.
+//! Bindings for the `hg::status` module provided by the
+//! `hg-core` crate. From Python, this will be seen as the
+//! `pyo3_rustext.dirstate.status` function.
+use pyo3::intern;
+use pyo3::prelude::*;
+use pyo3::types::{PyBytes, PyList, PyTuple};
+
+use hg::{
+    dirstate::status::{
+        BadMatch, DirstateStatus, StatusError, StatusOptions, StatusPath,
+    },
+    filepatterns::{
+        parse_pattern_syntax_kind, IgnorePattern, PatternError,
+        PatternFileWarning,
+    },
+    matchers::{
+        AlwaysMatcher, DifferenceMatcher, FileMatcher, IncludeMatcher,
+        IntersectionMatcher, Matcher, NeverMatcher, PatternMatcher,
+        UnionMatcher,
+    },
+    utils::{
+        files::{get_bytes_from_path, get_path_from_bytes},
+        hg_path::HgPath,
+    },
+};
+
+use super::dirstate_map::DirstateMap;
+use crate::{
+    exceptions::{to_string_value_error, FallbackError},
+    path::{paths_py_list, paths_pyiter_collect, PyHgPathRef},
+};
+
+fn status_path_py_list(
+    py: Python,
+    paths: &[StatusPath<'_>],
+) -> PyResult<Py<PyList>> {
+    paths_py_list(py, paths.iter().map(|item| &*item.path))
+}
+
+fn collect_bad_matches(
+    py: Python,
+    collection: &[(impl AsRef<HgPath>, BadMatch)],
+) -> PyResult<Py<PyList>> {
+    let get_error_message = |code: i32| -> String {
+        // hg-cpython here calling the Python interpreter
+        // using `os.strerror`. This seems to be equivalent and infallible
+        std::io::Error::from_raw_os_error(code).to_string()
+    };
+    Ok(PyList::new(
+        py,
+        collection.iter().map(|(path, bad_match)| {
+            let message = match bad_match {
+                BadMatch::OsError(code) => get_error_message(*code),
+                BadMatch::BadType(bad_type) => {
+                    format!("unsupported file type (type is {})", bad_type)
+                }
+            };
+            (PyHgPathRef(path.as_ref()), message)
+        }),
+    )?
+    .unbind())
+}
+
+fn collect_kindpats(
+    py: Python,
+    matcher: &Bound<'_, PyAny>,
+) -> PyResult<Vec<IgnorePattern>> {
+    matcher
+        .getattr(intern!(py, "_kindpats"))?
+        .try_iter()?
+        .map(|k| {
+            let k = k?;
+            let py_syntax = k.get_item(0)?;
+            let py_pattern = k.get_item(1)?;
+            let py_source = k.get_item(2)?;
+
+            Ok(IgnorePattern::new(
+                parse_pattern_syntax_kind(
+                    py_syntax.downcast::<PyBytes>()?.as_bytes(),
+                )
+                .map_err(|e| handle_fallback(StatusError::Pattern(e)))?,
+                py_pattern.downcast::<PyBytes>()?.as_bytes(),
+                get_path_from_bytes(
+                    py_source.downcast::<PyBytes>()?.as_bytes(),
+                ),
+            ))
+        })
+        .collect()
+}
+
+fn extract_matcher(
+    matcher: &Bound<'_, PyAny>,
+) -> PyResult<Box<dyn Matcher + Sync>> {
+    let py = matcher.py();
+    let tampered = matcher
+        .call_method0(intern!(py, "was_tampered_with_nonrec"))?
+        .extract::<bool>()?;
+    if tampered {
+        return Err(handle_fallback(StatusError::Pattern(
+            PatternError::UnsupportedSyntax(
+                "Pattern matcher was tampered with!".to_string(),
+            ),
+        )));
+    };
+
+    match matcher.get_type().name()?.to_str()? {
+        "alwaysmatcher" => Ok(Box::new(AlwaysMatcher)),
+        "nevermatcher" => Ok(Box::new(NeverMatcher)),
+        "exactmatcher" => {
+            let files = matcher.call_method0(intern!(py, "files"))?;
+            let files: Vec<_> = paths_pyiter_collect(&files)?;
+            Ok(Box::new(
+                FileMatcher::new(files).map_err(to_string_value_error)?,
+            ))
+        }
+        "includematcher" => {
+            // Get the patterns from Python even though most of them are
+            // redundant with those we will parse later on, as they include
+            // those passed from the command line.
+            let ignore_patterns = collect_kindpats(py, matcher)?;
+            Ok(Box::new(
+                IncludeMatcher::new(ignore_patterns)
+                    .map_err(|e| handle_fallback(e.into()))?,
+            ))
+        }
+        "unionmatcher" => {
+            let matchers: PyResult<Vec<_>> = matcher
+                .getattr("_matchers")?
+                .try_iter()?
+                .map(|py_matcher| extract_matcher(&py_matcher?))
+                .collect();
+
+            Ok(Box::new(UnionMatcher::new(matchers?)))
+        }
+        "intersectionmatcher" => {
+            let m1 = extract_matcher(&matcher.getattr("_m1")?)?;
+            let m2 = extract_matcher(&matcher.getattr("_m2")?)?;
+            Ok(Box::new(IntersectionMatcher::new(m1, m2)))
+        }
+        "differencematcher" => {
+            let m1 = extract_matcher(&matcher.getattr("_m1")?)?;
+            let m2 = extract_matcher(&matcher.getattr("_m2")?)?;
+            Ok(Box::new(DifferenceMatcher::new(m1, m2)))
+        }
+        "patternmatcher" => {
+            let patterns = collect_kindpats(py, matcher)?;
+            Ok(Box::new(
+                PatternMatcher::new(patterns)
+                    .map_err(|e| handle_fallback(e.into()))?,
+            ))
+        }
+
+        m => Err(FallbackError::new_err(format!("Unsupported matcher {m}"))),
+    }
+}
+
+fn handle_fallback(err: StatusError) -> PyErr {
+    match err {
+        StatusError::Pattern(e) => {
+            let as_string = e.to_string();
+            log::trace!("Rust status fallback, `{}`", &as_string);
+            FallbackError::new_err(as_string)
+        }
+        e => to_string_value_error(e),
+    }
+}
+
+#[pyfunction]
+#[allow(clippy::too_many_arguments)]
+pub(super) fn status(
+    py: Python,
+    dmap: &Bound<'_, DirstateMap>,
+    matcher: &Bound<'_, PyAny>,
+    root_dir: &Bound<'_, PyBytes>,
+    ignore_files: &Bound<'_, PyList>,
+    check_exec: bool,
+    list_clean: bool,
+    list_ignored: bool,
+    list_unknown: bool,
+    collect_traversed_dirs: bool,
+) -> PyResult<Py<PyTuple>> {
+    let root_dir = get_path_from_bytes(root_dir.as_bytes());
+
+    let ignore_files: PyResult<Vec<_>> = ignore_files
+        .try_iter()?
+        .map(|res| {
+            let ob = res?;
+            let file = ob.downcast::<PyBytes>()?.as_bytes();
+            Ok(get_path_from_bytes(file).to_owned())
+        })
+        .collect();
+    let ignore_files = ignore_files?;
+    // The caller may call `copymap.items()` separately
+    let list_copies = false;
+
+    let after_status = |res: Result<(DirstateStatus<'_>, _), StatusError>| {
+        let (status_res, warnings) = res.map_err(handle_fallback)?;
+        build_response(py, status_res, warnings)
+    };
+
+    let matcher = extract_matcher(matcher)?;
+    DirstateMap::with_inner_write(dmap, |_dm_ref, mut inner| {
+        inner.with_status(
+            &*matcher,
+            root_dir.to_path_buf(),
+            ignore_files,
+            StatusOptions {
+                check_exec,
+                list_clean,
+                list_ignored,
+                list_unknown,
+                list_copies,
+                collect_traversed_dirs,
+            },
+            after_status,
+        )
+    })
+}
+
+fn build_response(
+    py: Python,
+    status_res: DirstateStatus,
+    warnings: Vec<PatternFileWarning>,
+) -> PyResult<Py<PyTuple>> {
+    let modified = status_path_py_list(py, &status_res.modified)?;
+    let added = status_path_py_list(py, &status_res.added)?;
+    let removed = status_path_py_list(py, &status_res.removed)?;
+    let deleted = status_path_py_list(py, &status_res.deleted)?;
+    let clean = status_path_py_list(py, &status_res.clean)?;
+    let ignored = status_path_py_list(py, &status_res.ignored)?;
+    let unknown = status_path_py_list(py, &status_res.unknown)?;
+    let unsure = status_path_py_list(py, &status_res.unsure)?;
+    let bad = collect_bad_matches(py, &status_res.bad)?;
+    let traversed = paths_py_list(py, status_res.traversed.iter())?;
+    let py_warnings = PyList::empty(py);
+    for warning in warnings.iter() {
+        // We use duck-typing on the Python side for dispatch, good enough for
+        // now.
+        match warning {
+            PatternFileWarning::InvalidSyntax(file, syn) => {
+                py_warnings.append((
+                    PyBytes::new(py, &get_bytes_from_path(file)),
+                    PyBytes::new(py, syn),
+                ))?;
+            }
+            PatternFileWarning::NoSuchFile(file) => py_warnings
+                .append(PyBytes::new(py, &get_bytes_from_path(file)))?,
+        }
+    }
+
+    Ok((
+        unsure.into_pyobject(py)?,
+        modified.into_pyobject(py)?,
+        added.into_pyobject(py)?,
+        removed.into_pyobject(py)?,
+        deleted.into_pyobject(py)?,
+        clean.into_pyobject(py)?,
+        ignored.into_pyobject(py)?,
+        unknown.into_pyobject(py)?,
+        py_warnings.into_pyobject(py)?,
+        bad.into_pyobject(py)?,
+        traversed.into_pyobject(py)?,
+        status_res.dirty.into_pyobject(py)?,
+    )
+        .into_pyobject(py)?
+        .into())
+}
--- a/rust/hg-pyo3/src/path.rs	Wed Feb 05 11:20:39 2025 +0100
+++ b/rust/hg-pyo3/src/path.rs	Thu Feb 06 15:03:58 2025 +0100
@@ -73,7 +73,6 @@
     }
 }
 
-#[allow(dead_code)]
 pub fn paths_py_list<I, U>(
     py: Python<'_>,
     paths: impl IntoIterator<Item = I, IntoIter = U>,
@@ -91,7 +90,6 @@
     .unbind())
 }
 
-#[allow(dead_code)]
 pub fn paths_pyiter_collect<C>(paths: &Bound<'_, PyAny>) -> PyResult<C>
 where
     C: FromIterator<HgPathBuf>,