changeset 52871:9f083ff3c96c

rust-pyo3-dirstate: DirstateMap simple read-only methods This takes care of all read-only methods except: - copymap methods - methods returning iterators These two categories will be done in forthcoming changesets.
author Georges Racinet <georges.racinet@cloudcrane.io>
date Wed, 29 Jan 2025 18:26:10 +0100
parents c60f69556924
children 8f6d25439bdc
files rust/hg-pyo3/src/dirstate/dirstate_map.rs rust/hg-pyo3/src/exceptions.rs rust/hg-pyo3/src/path.rs
diffstat 3 files changed, 161 insertions(+), 8 deletions(-) [+]
line wrap: on
line diff
--- a/rust/hg-pyo3/src/dirstate/dirstate_map.rs	Thu Feb 06 11:18:28 2025 +0100
+++ b/rust/hg-pyo3/src/dirstate/dirstate_map.rs	Wed Jan 29 18:26:10 2025 +0100
@@ -8,20 +8,34 @@
 //! Bindings for the `hg::dirstate::dirstate_map` file provided by the
 //! `hg-core` package.
 
+use pyo3::exceptions::PyKeyError;
 use pyo3::prelude::*;
 use pyo3::types::{PyBytes, PyBytesMethods, PyTuple};
 use pyo3_sharedref::PyShareable;
 
-use hg::dirstate::{
-    dirstate_map::DirstateIdentity as CoreDirstateIdentity,
-    owning::OwningDirstateMap,
+use std::sync::RwLockReadGuard;
+
+use hg::{
+    dirstate::{
+        dirstate_map::{
+            DirstateIdentity as CoreDirstateIdentity, DirstateMapWriteMode,
+        },
+        owning::OwningDirstateMap,
+    },
+    utils::hg_path::HgPath,
+    DirstateParents,
 };
 
-use crate::{exceptions::dirstate_error, node::PyNode, utils::PyBytesDeref};
+use super::item::DirstateItem;
+use crate::{
+    exceptions::{dirstate_error, dirstate_v2_error, map_try_lock_error},
+    node::{node_from_py_bytes, PyNode},
+    path::PyHgPathRef,
+    utils::PyBytesDeref,
+};
 
-#[pyclass]
+#[pyclass(mapping)]
 pub struct DirstateMap {
-    #[allow(dead_code)]
     inner: PyShareable<OwningDirstateMap>,
 }
 
@@ -79,6 +93,147 @@
             inner: OwningDirstateMap::new_empty(vec![], None).into(),
         })
     }
+
+    #[pyo3(signature = (key, default=None))]
+    fn get(
+        slf: &Bound<'_, Self>,
+        key: &Bound<'_, PyBytes>,
+        default: Option<PyObject>,
+    ) -> PyResult<Option<PyObject>> {
+        let path = HgPath::new(key.as_bytes());
+
+        Self::with_inner_read(slf, |_self_ref, inner| {
+            match inner.get(path).map_err(dirstate_v2_error)? {
+                Some(entry) => Ok(Some(
+                    DirstateItem::new_as_py(slf.py(), entry)?.into_any(),
+                )),
+                None => Ok(default),
+            }
+        })
+    }
+
+    /// Returns suitable data for writing on disk in v1 format
+    ///
+    /// Despite the name, this is not a mutation of the object.
+    fn write_v1(
+        slf: &Bound<'_, Self>,
+        py: Python,
+        p1: &Bound<'_, PyBytes>,
+        p2: &Bound<'_, PyBytes>,
+    ) -> PyResult<Py<PyBytes>> {
+        Self::with_inner_read(slf, |_self_ref, inner| {
+            let parents = DirstateParents {
+                p1: node_from_py_bytes(p1)?,
+                p2: node_from_py_bytes(p2)?,
+            };
+            let packed = inner.pack_v1(parents).map_err(dirstate_error)?;
+            // TODO optim, see `write_v2()`
+            Ok(PyBytes::new(py, &packed).unbind())
+        })
+    }
+
+    /// Returns suitable new data for writing on disk in v2 format
+    ///
+    /// Despite the name, this is not a mutation of the object.
+    ///
+    /// The new data together with whether that data should be appended to
+    /// the existing data file whose content is at `self.on_disk` (True),
+    /// instead of written to a new data file (False).
+    fn write_v2(
+        slf: &Bound<'_, Self>,
+        py: Python,
+        write_mode: usize,
+    ) -> PyResult<Py<PyTuple>> {
+        Self::with_inner_read(slf, |_self_ref, inner| {
+            let rust_write_mode = match write_mode {
+                0 => DirstateMapWriteMode::Auto,
+                1 => DirstateMapWriteMode::ForceNewDataFile,
+                2 => DirstateMapWriteMode::ForceAppend,
+                _ => DirstateMapWriteMode::Auto, // XXX should we error out?
+            };
+            let (packed, tree_metadata, append, _old_data_size) =
+                inner.pack_v2(rust_write_mode).map_err(dirstate_error)?;
+            // TODO optim. In theory we should be able to avoid these copies,
+            // since we have full ownership of `packed` and `tree_metadata`.
+            // But the copy is done by CPython itself, in
+            // `PyBytes_FromStringAndSize()`. Perhaps something better can
+            // be done with `PyBytes_FromObject` (buffer protocol).
+            let packed = PyBytes::new(py, &packed).unbind();
+            let tree_metadata =
+                PyBytes::new(py, tree_metadata.as_bytes()).unbind();
+            Ok((packed, tree_metadata, append).into_pyobject(py)?.into())
+        })
+    }
+
+    fn __len__(slf: &Bound<'_, Self>) -> PyResult<usize> {
+        Self::with_inner_read(slf, |_self_ref, inner| Ok(inner.len()))
+    }
+
+    fn __contains__(
+        slf: &Bound<'_, Self>,
+        key: &Bound<'_, PyBytes>,
+    ) -> PyResult<bool> {
+        Self::with_inner_read(slf, |_self_ref, inner| {
+            inner
+                .contains_key(HgPath::new(key.as_bytes()))
+                .map_err(dirstate_v2_error)
+        })
+    }
+
+    fn __getitem__(
+        slf: &Bound<'_, Self>,
+        key: &Bound<'_, PyBytes>,
+    ) -> PyResult<Py<DirstateItem>> {
+        let key_bytes = key.as_bytes();
+        let path = HgPath::new(key_bytes);
+        Self::with_inner_read(slf, |_self_ref, inner| {
+            match inner.get(path).map_err(dirstate_v2_error)? {
+                Some(entry) => DirstateItem::new_as_py(slf.py(), entry),
+                None => Err(PyKeyError::new_err(
+                    String::from_utf8_lossy(key_bytes).to_string(),
+                )),
+            }
+        })
+    }
+
+    fn debug_iter(
+        slf: &Bound<'_, Self>,
+        py: Python,
+        all: bool,
+    ) -> PyResult<PyObject> {
+        Self::with_inner_read(slf, |_self_ref, inner| {
+            // the iterator returned by `debug_iter()` does not
+            // implement ExactSizeIterator, which is needed by
+            // `PyList::new()`, so we need to collect. Probably not a
+            // performance issue, as this is a debug method.
+            let as_vec: PyResult<Vec<_>> = inner
+                .debug_iter(all)
+                .map(|item| {
+                    let (path, (state, mode, size, mtime)) =
+                        item.map_err(dirstate_v2_error)?;
+                    Ok((PyHgPathRef(path), state, mode, size, mtime))
+                })
+                .collect();
+            Ok(as_vec?.into_pyobject(py)?.unbind())
+        })
+    }
+}
+
+impl DirstateMap {
+    fn with_inner_read<'py, T>(
+        slf: &Bound<'py, Self>,
+        f: impl FnOnce(
+            &PyRef<'py, Self>,
+            RwLockReadGuard<OwningDirstateMap>,
+        ) -> PyResult<T>,
+    ) -> PyResult<T> {
+        let self_ref = slf.borrow();
+        // Safety: the owner is the right one. We will anyway
+        // not actually `share` it.
+        let shareable_ref = unsafe { self_ref.inner.borrow_with_owner(slf) };
+        let guard = shareable_ref.try_read().map_err(map_try_lock_error)?;
+        f(&self_ref, guard)
+    }
 }
 
 #[pyclass]
--- a/rust/hg-pyo3/src/exceptions.rs	Thu Feb 06 11:18:28 2025 +0100
+++ b/rust/hg-pyo3/src/exceptions.rs	Wed Jan 29 18:26:10 2025 +0100
@@ -92,7 +92,6 @@
     PyOSError::new_err(format!("Dirstate error: {:?}", err))
 }
 
-#[allow(dead_code)]
 pub fn dirstate_v2_error(_err: DirstateV2ParseError) -> PyErr {
     PyValueError::new_err("corrupted dirstate-v2")
 }
--- a/rust/hg-pyo3/src/path.rs	Thu Feb 06 11:18:28 2025 +0100
+++ b/rust/hg-pyo3/src/path.rs	Wed Jan 29 18:26:10 2025 +0100
@@ -15,7 +15,6 @@
 
 use hg::utils::hg_path::{HgPath, HgPathBuf};
 
-#[allow(dead_code)]
 #[derive(Eq, Ord, PartialEq, PartialOrd, Hash, derive_more::From)]
 pub struct PyHgPathRef<'a>(pub &'a HgPath);