diff --git a/rust/hg-core/src/dirstate/status.rs b/rust/hg-core/src/dirstate/status.rs --- a/rust/hg-core/src/dirstate/status.rs +++ b/rust/hg-core/src/dirstate/status.rs @@ -48,6 +48,20 @@ pub enum BadType { Unknown, } +impl ToString for BadType { + fn to_string(&self) -> String { + match self { + BadType::CharacterDevice => "character device", + BadType::BlockDevice => "block device", + BadType::FIFO => "fifo", + BadType::Socket => "socket", + BadType::Directory => "directory", + BadType::Unknown => "unknown", + } + .to_string() + } +} + /// Was explicitly matched but cannot be found/accessed #[derive(Debug)] pub enum BadMatch { diff --git a/rust/hg-cpython/src/dirstate.rs b/rust/hg-cpython/src/dirstate.rs --- a/rust/hg-cpython/src/dirstate.rs +++ b/rust/hg-cpython/src/dirstate.rs @@ -14,12 +14,15 @@ mod dirs_multiset; mod dirstate_map; mod non_normal_entries; mod status; -use crate::dirstate::{ - dirs_multiset::Dirs, dirstate_map::DirstateMap, status::status_wrapper, +use crate::{ + dirstate::{ + dirs_multiset::Dirs, dirstate_map::DirstateMap, status::status_wrapper, + }, + exceptions, }; use cpython::{ - exc, PyBytes, PyDict, PyErr, PyModule, PyObject, PyResult, PySequence, - Python, + exc, PyBytes, PyDict, PyErr, PyList, PyModule, PyObject, PyResult, + PySequence, Python, }; use hg::{ utils::hg_path::HgPathBuf, DirstateEntry, DirstateParseError, EntryState, @@ -107,6 +110,11 @@ pub fn init_module(py: Python, package: m.add(py, "__package__", package)?; m.add(py, "__doc__", "Dirstate - Rust implementation")?; + m.add( + py, + "FallbackError", + py.get_type::(), + )?; m.add_class::(py)?; m.add_class::(py)?; m.add( @@ -118,9 +126,12 @@ pub fn init_module(py: Python, package: dmap: DirstateMap, root_dir: PyObject, matcher: PyObject, - list_clean: bool, + ignorefiles: PyList, + check_exec: bool, last_normal_time: i64, - check_exec: bool + list_clean: bool, + list_ignored: bool, + list_unknown: bool ) ), )?; diff --git a/rust/hg-cpython/src/dirstate/status.rs b/rust/hg-cpython/src/dirstate/status.rs --- a/rust/hg-cpython/src/dirstate/status.rs +++ b/rust/hg-cpython/src/dirstate/status.rs @@ -9,33 +9,34 @@ //! `hg-core` crate. From Python, this will be seen as //! `rustext.dirstate.status`. -use crate::dirstate::DirstateMap; -use cpython::exc::ValueError; +use crate::{dirstate::DirstateMap, exceptions::FallbackError}; use cpython::{ - ObjectProtocol, PyBytes, PyErr, PyList, PyObject, PyResult, PyTuple, - Python, PythonObject, ToPyObject, + exc::ValueError, ObjectProtocol, PyBytes, PyErr, PyList, PyObject, + PyResult, PyTuple, Python, PythonObject, ToPyObject, }; -use hg::utils::hg_path::HgPathBuf; use hg::{ - matchers::{AlwaysMatcher, FileMatcher}, - status, - utils::{files::get_path_from_bytes, hg_path::HgPath}, - DirstateStatus, + matchers::{AlwaysMatcher, FileMatcher, IncludeMatcher}, + parse_pattern_syntax, status, + utils::{ + files::{get_bytes_from_path, get_path_from_bytes}, + hg_path::{HgPath, HgPathBuf}, + }, + BadMatch, DirstateStatus, IgnorePattern, PatternFileWarning, StatusError, + StatusOptions, }; -use std::borrow::Borrow; +use std::borrow::{Borrow, Cow}; /// This will be useless once trait impls for collection are added to `PyBytes` /// upstream. -fn collect_pybytes_list>( +fn collect_pybytes_list( py: Python, - collection: &[P], + collection: &[impl AsRef], ) -> PyList { let list = PyList::new(py, &[]); - for (i, path) in collection.iter().enumerate() { - list.insert( + for path in collection.iter() { + list.append( py, - i, PyBytes::new(py, path.as_ref().as_bytes()).into_object(), ) } @@ -43,34 +44,97 @@ fn collect_pybytes_list list } +fn collect_bad_matches( + py: Python, + collection: &[(impl AsRef, BadMatch)], +) -> PyResult { + let list = PyList::new(py, &[]); + + let os = py.import("os")?; + let get_error_message = |code: i32| -> PyResult<_> { + os.call( + py, + "strerror", + PyTuple::new(py, &[code.to_py_object(py).into_object()]), + None, + ) + }; + + for (path, bad_match) in collection.iter() { + let message = match bad_match { + BadMatch::OsError(code) => get_error_message(*code)?, + BadMatch::BadType(bad_type) => format!( + "unsupported file type (type is {})", + bad_type.to_string() + ) + .to_py_object(py) + .into_object(), + }; + list.append( + py, + (PyBytes::new(py, path.as_ref().as_bytes()), message) + .to_py_object(py) + .into_object(), + ) + } + + Ok(list) +} + +fn handle_fallback(py: Python, err: StatusError) -> PyErr { + match err { + StatusError::Pattern(e) => { + PyErr::new::(py, e.to_string()) + } + e => PyErr::new::(py, e.to_string()), + } +} + pub fn status_wrapper( py: Python, dmap: DirstateMap, matcher: PyObject, root_dir: PyObject, - list_clean: bool, + ignore_files: PyList, + check_exec: bool, last_normal_time: i64, - check_exec: bool, -) -> PyResult<(PyList, PyList, PyList, PyList, PyList, PyList, PyList)> { + list_clean: bool, + list_ignored: bool, + list_unknown: bool, +) -> PyResult { let bytes = root_dir.extract::(py)?; let root_dir = get_path_from_bytes(bytes.data(py)); let dmap: DirstateMap = dmap.to_py_object(py); let dmap = dmap.get_inner(py); + let ignore_files: PyResult> = ignore_files + .iter(py) + .map(|b| { + let file = b.extract::(py)?; + Ok(get_path_from_bytes(file.data(py)).to_owned()) + }) + .collect(); + let ignore_files = ignore_files?; + match matcher.get_type(py).name(py).borrow() { "alwaysmatcher" => { let matcher = AlwaysMatcher; - let (lookup, status_res) = status( + let ((lookup, status_res), warnings) = status( &dmap, &matcher, &root_dir, - list_clean, - last_normal_time, - check_exec, + &ignore_files, + StatusOptions { + check_exec, + last_normal_time, + list_clean, + list_ignored, + list_unknown, + }, ) - .map_err(|e| PyErr::new::(py, e.to_string()))?; - build_response(lookup, status_res, py) + .map_err(|e| handle_fallback(py, e))?; + build_response(py, lookup, status_res, warnings) } "exactmatcher" => { let files = matcher.call_method( @@ -92,16 +156,78 @@ pub fn status_wrapper( let files = files?; let matcher = FileMatcher::new(&files) .map_err(|e| PyErr::new::(py, e.to_string()))?; - let (lookup, status_res) = status( + let ((lookup, status_res), warnings) = status( &dmap, &matcher, &root_dir, - list_clean, - last_normal_time, - check_exec, + &ignore_files, + StatusOptions { + check_exec, + last_normal_time, + list_clean, + list_ignored, + list_unknown, + }, ) - .map_err(|e| PyErr::new::(py, e.to_string()))?; - build_response(lookup, status_res, py) + .map_err(|e| handle_fallback(py, e))?; + build_response(py, lookup, status_res, warnings) + } + "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: PyResult> = matcher + .getattr(py, "_kindpats")? + .iter(py)? + .map(|k| { + let k = k?; + let syntax = parse_pattern_syntax( + &[ + k.get_item(py, 0)? + .extract::(py)? + .data(py), + &b":"[..], + ] + .concat(), + ) + .map_err(|e| { + handle_fallback(py, StatusError::Pattern(e)) + })?; + let pattern = k.get_item(py, 1)?.extract::(py)?; + let pattern = pattern.data(py); + let source = k.get_item(py, 2)?.extract::(py)?; + let source = get_path_from_bytes(source.data(py)); + let new = IgnorePattern::new(syntax, pattern, source); + Ok(new) + }) + .collect(); + + let ignore_patterns = ignore_patterns?; + let mut all_warnings = vec![]; + + let (matcher, warnings) = + IncludeMatcher::new(ignore_patterns, &root_dir) + .map_err(|e| handle_fallback(py, e.into()))?; + all_warnings.extend(warnings); + + let ((lookup, status_res), warnings) = status( + &dmap, + &matcher, + &root_dir, + &ignore_files, + StatusOptions { + check_exec, + last_normal_time, + list_clean, + list_ignored, + list_unknown, + }, + ) + .map_err(|e| handle_fallback(py, e))?; + + all_warnings.extend(warnings); + + build_response(py, lookup, status_res, all_warnings) } e => { return Err(PyErr::new::( @@ -113,17 +239,56 @@ pub fn status_wrapper( } fn build_response( - lookup: Vec<&HgPath>, + py: Python, + lookup: Vec>, status_res: DirstateStatus, - py: Python, -) -> PyResult<(PyList, PyList, PyList, PyList, PyList, PyList, PyList)> { + warnings: Vec, +) -> PyResult { let modified = collect_pybytes_list(py, status_res.modified.as_ref()); let added = collect_pybytes_list(py, status_res.added.as_ref()); let removed = collect_pybytes_list(py, status_res.removed.as_ref()); let deleted = collect_pybytes_list(py, status_res.deleted.as_ref()); let clean = collect_pybytes_list(py, status_res.clean.as_ref()); + let ignored = collect_pybytes_list(py, status_res.ignored.as_ref()); + let unknown = collect_pybytes_list(py, status_res.unknown.as_ref()); let lookup = collect_pybytes_list(py, lookup.as_ref()); - let unknown = PyList::new(py, &[]); + let bad = collect_bad_matches(py, status_res.bad.as_ref())?; + let py_warnings = PyList::new(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( + py, + ( + PyBytes::new(py, &get_bytes_from_path(&file)), + PyBytes::new(py, syn), + ) + .to_py_object(py) + .into_object(), + ); + } + PatternFileWarning::NoSuchFile(file) => py_warnings.append( + py, + PyBytes::new(py, &get_bytes_from_path(&file)).into_object(), + ), + } + } - Ok((lookup, modified, added, removed, deleted, unknown, clean)) + Ok(PyTuple::new( + py, + &[ + lookup.into_object(), + modified.into_object(), + added.into_object(), + removed.into_object(), + deleted.into_object(), + clean.into_object(), + ignored.into_object(), + unknown.into_object(), + py_warnings.into_object(), + bad.into_object(), + ][..], + )) } diff --git a/rust/hg-cpython/src/exceptions.rs b/rust/hg-cpython/src/exceptions.rs --- a/rust/hg-cpython/src/exceptions.rs +++ b/rust/hg-cpython/src/exceptions.rs @@ -40,3 +40,5 @@ impl GraphError { } py_exception!(rustext, HgPathPyError, RuntimeError); +py_exception!(rustext, FallbackError, RuntimeError); +py_exception!(shared_ref, AlreadyBorrowed, RuntimeError);