From ace64375b192d6a59c0fdcd78022d009abf56420 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Mon, 11 May 2026 13:29:18 +0100 Subject: [PATCH 1/8] Remove crates/bazaar-py --- Cargo.lock | 47 +- crates/bazaar-py/Cargo.toml | 14 - crates/bazaar-py/src/chk_map.rs | 53 - crates/bazaar-py/src/dirstate.rs | 467 ------- crates/bazaar-py/src/groupcompress.rs | 448 ------ crates/bazaar-py/src/hashcache.rs | 237 ---- crates/bazaar-py/src/inventory.rs | 1823 ------------------------- crates/bazaar-py/src/lib.rs | 555 -------- crates/bazaar-py/src/rio.rs | 366 ----- crates/bazaar-py/src/smart.rs | 29 - crates/bazaar-py/src/versionedfile.rs | 161 --- setup.py | 3 - 12 files changed, 9 insertions(+), 4194 deletions(-) delete mode 100644 crates/bazaar-py/Cargo.toml delete mode 100644 crates/bazaar-py/src/chk_map.rs delete mode 100644 crates/bazaar-py/src/dirstate.rs delete mode 100644 crates/bazaar-py/src/groupcompress.rs delete mode 100644 crates/bazaar-py/src/hashcache.rs delete mode 100644 crates/bazaar-py/src/inventory.rs delete mode 100644 crates/bazaar-py/src/lib.rs delete mode 100644 crates/bazaar-py/src/rio.rs delete mode 100644 crates/bazaar-py/src/smart.rs delete mode 100644 crates/bazaar-py/src/versionedfile.rs diff --git a/Cargo.lock b/Cargo.lock index 2c24ed2d41..8bfba3cbe5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -126,17 +126,6 @@ dependencies = [ "xz2", ] -[[package]] -name = "bazaar-py" -version = "3.4.0" -dependencies = [ - "bazaar", - "breezy-osutils", - "chrono", - "pyo3", - "pyo3-filelike", -] - [[package]] name = "bendy" version = "0.3.3" @@ -251,14 +240,6 @@ dependencies = [ "tempfile", ] -[[package]] -name = "breezy-readdir" -version = "3.4.0" -dependencies = [ - "libc", - "nix", -] - [[package]] name = "breezy-zlib-util" version = "3.4.0" @@ -286,9 +267,9 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cc" -version = "1.2.61" +version = "1.2.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" dependencies = [ "find-msvc-tools", "shlex", @@ -755,9 +736,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.17.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "heck" @@ -1026,7 +1007,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.17.0", + "hashbrown 0.17.1", "serde", "serde_core", ] @@ -1206,9 +1187,9 @@ dependencies = [ [[package]] name = "nix" -version = "0.31.2" +version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" dependencies = [ "bitflags", "cfg-if", @@ -1565,16 +1546,6 @@ dependencies = [ "getrandom 0.3.4", ] -[[package]] -name = "readdir-py" -version = "3.4.0" -dependencies = [ - "breezy-readdir", - "libc", - "nix", - "pyo3", -] - [[package]] name = "redox_syscall" version = "0.7.5" @@ -2090,9 +2061,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.52.2" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "110a78583f19d5cdb2c5ccf321d1290344e71313c6c37d43520d386027d18386" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "bytes", "libc", diff --git a/crates/bazaar-py/Cargo.toml b/crates/bazaar-py/Cargo.toml deleted file mode 100644 index d79626a95c..0000000000 --- a/crates/bazaar-py/Cargo.toml +++ /dev/null @@ -1,14 +0,0 @@ -[package] -name = "bazaar-py" -version = { workspace = true } -edition = "2018" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -bazaar = { path = "../bazaar", features=["pyo3"] } -pyo3 = { workspace = true, features = ["extension-module", "chrono"]} -pyo3-filelike = { workspace = true } -chrono = { workspace = true } -breezy-osutils = { path = "../osutils", features = ["pyo3"] } diff --git a/crates/bazaar-py/src/chk_map.rs b/crates/bazaar-py/src/chk_map.rs deleted file mode 100644 index 98767285d5..0000000000 --- a/crates/bazaar-py/src/chk_map.rs +++ /dev/null @@ -1,53 +0,0 @@ -use bazaar::chk_map::Key; -use pyo3::prelude::*; -use pyo3::types::PyBytes; -use pyo3::wrap_pyfunction; - -#[pyfunction] -fn _search_key_16(py: Python, key: Vec>) -> Bound { - let key: Key = key.into(); - let ret = bazaar::chk_map::search_key_16(&key); - PyBytes::new(py, &ret) -} - -#[pyfunction] -fn _search_key_255(py: Python, key: Vec>) -> Bound { - let key: Key = key.into(); - let ret = bazaar::chk_map::search_key_255(&key); - PyBytes::new(py, &ret) -} - -#[pyfunction] -fn _bytes_to_text_key(py: Python, key: Vec) -> PyResult<(Bound, Bound)> { - let ret = bazaar::chk_map::bytes_to_text_key(key.as_slice()); - if ret.is_err() { - return Err(PyErr::new::( - "Invalid key", - )); - } - let ret = ret.unwrap(); - Ok((PyBytes::new(py, ret.0), PyBytes::new(py, ret.1))) -} - -#[pyfunction] -fn common_prefix_pair<'a>(py: Python<'a>, key: &'a [u8], key2: &'a [u8]) -> Bound<'a, PyBytes> { - PyBytes::new(py, bazaar::chk_map::common_prefix_pair(key, key2)) -} - -#[pyfunction] -fn common_prefix_many(py: Python, keys: Vec>) -> Option> { - let keys = keys.iter().map(|v| v.as_slice()).collect::>(); - bazaar::chk_map::common_prefix_many(keys.into_iter()) - .as_ref() - .map(|v| PyBytes::new(py, v)) -} - -pub(crate) fn _chk_map_rs(py: Python) -> PyResult> { - let m = PyModule::new(py, "chk_map")?; - m.add_wrapped(wrap_pyfunction!(_search_key_16))?; - m.add_wrapped(wrap_pyfunction!(_search_key_255))?; - m.add_wrapped(wrap_pyfunction!(_bytes_to_text_key))?; - m.add_wrapped(wrap_pyfunction!(common_prefix_pair))?; - m.add_wrapped(wrap_pyfunction!(common_prefix_many))?; - Ok(m) -} diff --git a/crates/bazaar-py/src/dirstate.rs b/crates/bazaar-py/src/dirstate.rs deleted file mode 100644 index 44a158686b..0000000000 --- a/crates/bazaar-py/src/dirstate.rs +++ /dev/null @@ -1,467 +0,0 @@ -#![allow(non_snake_case)] - -use bazaar::FileId; -use breezy_osutils::stat; -use pyo3::exceptions::PyTypeError; -use pyo3::prelude::*; -use pyo3::types::{PyBytes, PyDict, PyList, PyString, PyTuple}; -use pyo3::wrap_pyfunction; -use std::path::{Path, PathBuf}; - -#[cfg(unix)] -fn path_from_bytes(bytes: Vec) -> PathBuf { - use std::ffi::OsString; - use std::os::unix::ffi::OsStringExt; - PathBuf::from(OsString::from_vec(bytes)) -} - -#[cfg(windows)] -fn path_from_bytes(bytes: Vec) -> PathBuf { - // Windows path APIs operate on UTF-16, so interpret the byte string as - // UTF-8 (what Breezy uses for path bytes on Windows). - PathBuf::from(String::from_utf8_lossy(&bytes).into_owned()) -} - -// TODO(jelmer): Shared pyo3 utils? -fn extract_path(object: &Bound) -> PyResult { - if let Ok(path) = object.extract::>() { - Ok(path_from_bytes(path)) - } else if let Ok(path) = object.extract::() { - Ok(path) - } else { - Err(PyTypeError::new_err("path must be a string or bytes")) - } -} - -/// Compare two paths directory by directory. -/// -/// This is equivalent to doing:: -/// -/// operator.lt(path1.split('/'), path2.split('/')) -/// -/// The idea is that you should compare path components separately. This -/// differs from plain ``path1 < path2`` for paths like ``'a-b'`` and ``a/b``. -/// "a-b" comes after "a" but would come before "a/b" lexically. -/// -/// Args: -/// path1: first path -/// path2: second path -/// Returns: True if path1 comes first, otherwise False -#[pyfunction] -fn lt_by_dirs(path1: &Bound, path2: &Bound) -> PyResult { - let path1 = extract_path(path1)?; - let path2 = extract_path(path2)?; - Ok(bazaar::dirstate::lt_by_dirs(&path1, &path2)) -} - -/// Return the index where to insert path into paths. -/// -/// This uses the dirblock sorting. So all children in a directory come before -/// the children of children. For example:: -/// -/// a/ -/// b/ -/// c -/// d/ -/// e -/// b-c -/// d-e -/// a-a -/// a=c -/// -/// Will be sorted as:: -/// -/// a -/// a-a -/// a=c -/// a/b -/// a/b-c -/// a/d -/// a/d-e -/// a/b/c -/// a/d/e -/// -/// Args: -/// paths: A list of paths to search through -/// path: A single path to insert -/// Returns: An offset where 'path' can be inserted. -/// See also: bisect.bisect_left - -#[pyfunction] -fn bisect_path_left(paths: Vec>, path: &Bound) -> PyResult { - let path = extract_path(path)?; - let paths = paths - .iter() - .map(|x| extract_path(x).unwrap()) - .collect::>(); - let offset = bazaar::dirstate::bisect_path_left( - paths - .iter() - .map(|x| x.as_path()) - .collect::>() - .as_slice(), - &path, - ); - Ok(offset) -} - -/// Return the index where to insert path into paths. -/// -/// This uses a path-wise comparison so we get:: -/// a -/// a-b -/// a=b -/// a/b -/// Rather than:: -/// a -/// a-b -/// a/b -/// a=b -/// -/// Args: -/// paths: A list of paths to search through -/// path: A single path to insert -/// Returns: An offset where 'path' can be inserted. -/// See also: bisect.bisect_right -#[pyfunction] -fn bisect_path_right(paths: Vec>, path: &Bound) -> PyResult { - let path = extract_path(path)?; - let paths = paths - .iter() - .map(|x| extract_path(x).unwrap()) - .collect::>(); - let offset = bazaar::dirstate::bisect_path_right( - paths - .iter() - .map(|x| x.as_path()) - .collect::>() - .as_slice(), - &path, - ); - Ok(offset) -} - -#[pyfunction] -fn lt_path_by_dirblock(path1: &Bound, path2: &Bound) -> PyResult { - let path1 = extract_path(path1)?; - let path2 = extract_path(path2)?; - Ok(bazaar::dirstate::lt_path_by_dirblock(&path1, &path2)) -} - -#[pyfunction] -#[pyo3(signature = (dirblocks, dirname, lo=None, hi=None, cache=None))] -fn bisect_dirblock( - py: Python, - dirblocks: &Bound, - dirname: &Bound, - lo: Option, - hi: Option, - cache: Option>, -) -> PyResult { - fn split_object(obj: &Bound) -> PyResult> { - if let Ok(py_str) = obj.extract::>() { - Ok(py_str - .to_string() - .split('/') - .map(PathBuf::from) - .collect::>()) - } else if let Ok(py_bytes) = obj.extract::>() { - Ok(py_bytes - .as_bytes() - .split(|&byte| byte == b'/') - .map(|s| PathBuf::from(String::from_utf8_lossy(s).to_string())) - .collect::>()) - } else { - Err(PyTypeError::new_err("Not a PyBytes or PyString")) - } - } - - let hi = hi.unwrap_or(dirblocks.len()); - let cache = cache.unwrap_or_else(|| PyDict::new(py)); - - let dirname_split = match cache.get_item(dirname)? { - Some(item) => item.extract::>()?, - None => { - let split = split_object(dirname)?; - cache.set_item(dirname.clone(), split.clone())?; - split - } - }; - - let mut lo = lo.unwrap_or(0); - let mut hi = hi; - - while lo < hi { - let mid = (lo + hi) / 2; - let dirblock = dirblocks.get_item(mid)?.cast_into::()?; - let cur = dirblock.get_item(0)?; - - let cur_split = match cache.get_item(&cur)? { - Some(item) => item.extract::>()?, - None => { - let split = split_object(&cur)?; - cache.set_item(cur, split.clone())?; - split - } - }; - - if cur_split < dirname_split { - lo = mid + 1; - } else { - hi = mid; - } - } - Ok(lo) -} - -// TODO(jelmer): Move this into a more central place? -#[pyclass] -struct StatResult { - metadata: std::fs::Metadata, -} - -#[pymethods] -impl StatResult { - #[getter] - fn st_size(&self) -> PyResult { - Ok(self.metadata.len()) - } - - #[getter] - fn st_mtime(&self) -> PyResult { - let modified = self - .metadata - .modified() - .map_err(PyErr::new::)?; - let since_epoch = modified - .duration_since(std::time::UNIX_EPOCH) - .map_err(|e| PyErr::new::(e.to_string()))?; - Ok(since_epoch.as_secs()) - } - - #[getter] - fn st_ctime(&self) -> PyResult { - let created = self - .metadata - .created() - .map_err(PyErr::new::)?; - let since_epoch = created - .duration_since(std::time::UNIX_EPOCH) - .map_err(|e| PyErr::new::(e.to_string()))?; - Ok(since_epoch.as_secs()) - } - - #[getter] - fn st_mode(&self) -> PyResult { - Ok(stat::mode(&self.metadata)) - } - - #[getter] - fn st_dev(&self) -> PyResult { - Ok(stat::dev(&self.metadata)) - } - - #[getter] - fn st_ino(&self) -> PyResult { - Ok(stat::ino(&self.metadata)) - } -} - -#[pyclass] -struct SHA1Provider { - provider: Box, -} - -#[pymethods] -impl SHA1Provider { - fn sha1<'a>(&mut self, py: Python<'a>, path: &Bound) -> PyResult> { - let path = extract_path(path)?; - let sha1 = self - .provider - .sha1(&path) - .map_err(PyErr::new::)?; - Ok(PyBytes::new(py, sha1.as_bytes())) - } - - fn stat_and_sha1<'a>( - &mut self, - py: Python<'a>, - path: &Bound, - ) -> PyResult<(Py, Bound<'a, PyBytes>)> { - let path = extract_path(path)?; - let (md, sha1) = self.provider.stat_and_sha1(&path)?; - let pmd = StatResult { metadata: md }; - Ok(( - pmd.into_pyobject(py)?.unbind().into(), - PyBytes::new(py, sha1.as_bytes()), - )) - } -} - -#[pyfunction] -fn DefaultSHA1Provider() -> PyResult { - Ok(SHA1Provider { - provider: Box::new(bazaar::dirstate::DefaultSHA1Provider::new()), - }) -} - -fn extract_fs_time(obj: &Bound) -> PyResult { - if let Ok(u) = obj.extract::() { - Ok(u) - } else if let Ok(u) = obj.extract::() { - Ok(u as u64) - } else { - Err(PyTypeError::new_err("Not a float or int")) - } -} - -#[pyfunction] -fn pack_stat<'a>(stat_result: &'a Bound<'a, PyAny>) -> PyResult> { - let size = stat_result.getattr("st_size")?.extract::()?; - let mtime = extract_fs_time(&stat_result.getattr("st_mtime")?)?; - let ctime = extract_fs_time(&stat_result.getattr("st_ctime")?)?; - let dev = stat_result.getattr("st_dev")?.extract::()?; - let ino = stat_result.getattr("st_ino")?.extract::()?; - let mode = stat_result.getattr("st_mode")?.extract::()?; - let s = bazaar::dirstate::pack_stat(size, mtime, ctime, dev, ino, mode); - Ok(PyBytes::new(stat_result.py(), s.as_bytes())) -} - -#[pyfunction] -fn fields_per_entry(num_present_parents: usize) -> usize { - bazaar::dirstate::fields_per_entry(num_present_parents) -} - -#[pyfunction] -fn get_ghosts_line(py: Python, ghost_ids: Vec>) -> PyResult> { - let ghost_ids = ghost_ids - .iter() - .map(|x| x.as_slice()) - .collect::>(); - let bs = bazaar::dirstate::get_ghosts_line(ghost_ids.as_slice()); - Ok(PyBytes::new(py, bs.as_slice())) -} - -#[pyfunction] -fn get_parents_line(py: Python, parent_ids: Vec>) -> PyResult> { - let parent_ids = parent_ids - .iter() - .map(|x| x.as_slice()) - .collect::>(); - let bs = bazaar::dirstate::get_parents_line(parent_ids.as_slice()); - Ok(PyBytes::new(py, bs.as_slice())) -} - -#[pyclass] -struct IdIndex(bazaar::dirstate::IdIndex); - -#[pymethods] -impl IdIndex { - #[new] - fn new() -> Self { - IdIndex(bazaar::dirstate::IdIndex::new()) - } - - fn add(&mut self, entry: (Vec, Vec, FileId)) -> PyResult<()> { - self.0.add((&entry.0, &entry.1, &entry.2)); - Ok(()) - } - - fn remove(&mut self, entry: (Vec, Vec, FileId)) -> PyResult<()> { - self.0.remove((&entry.0, &entry.1, &entry.2)); - Ok(()) - } - - fn get<'a>( - &self, - py: Python<'a>, - file_id: FileId, - ) -> PyResult, Bound<'a, PyBytes>, Bound<'a, PyBytes>)>> { - let ret = self.0.get(&file_id); - ret.iter() - .map(|(a, b, c)| { - Ok(( - PyBytes::new(py, a), - PyBytes::new(py, b), - c.into_pyobject(py)?, - )) - }) - .collect::>>() - } - - fn iter_all<'py>( - &self, - py: Python<'py>, - ) -> PyResult< - Vec<( - Bound<'py, PyBytes>, - Bound<'py, PyBytes>, - Bound<'py, PyBytes>, - )>, - > { - let ret = self.0.iter_all(); - ret.map(|(a, b, c)| { - Ok(( - PyBytes::new(py, a), - PyBytes::new(py, b), - c.into_pyobject(py)?, - )) - }) - .collect::>>() - } - - fn file_ids<'a>(&self, py: Python<'a>) -> PyResult>> { - self.0.file_ids().map(|x| x.into_pyobject(py)).collect() - } -} - -#[pyfunction] -fn inv_entry_to_details<'a>( - py: Python<'a>, - e: &'a crate::inventory::InventoryEntry, -) -> ( - Bound<'a, PyBytes>, - Bound<'a, PyBytes>, - u64, - bool, - Bound<'a, PyBytes>, -) { - let ret = bazaar::dirstate::inv_entry_to_details(&e.0); - - ( - PyBytes::new(py, &[ret.0]), - PyBytes::new(py, ret.1.as_slice()), - ret.2, - ret.3, - PyBytes::new(py, ret.4.as_slice()), - ) -} - -#[pyfunction] -fn get_output_lines(py: Python<'_>, lines: Vec>) -> Vec> { - let lines = lines.iter().map(|x| x.as_slice()).collect::>(); - bazaar::dirstate::get_output_lines(lines) - .into_iter() - .map(|x| PyBytes::new(py, x.as_slice())) - .collect() -} - -/// Helpers for the dirstate module. -pub fn _dirstate_rs(py: Python) -> PyResult> { - let m = PyModule::new(py, "dirstate")?; - m.add_wrapped(wrap_pyfunction!(lt_by_dirs))?; - m.add_wrapped(wrap_pyfunction!(bisect_path_left))?; - m.add_wrapped(wrap_pyfunction!(bisect_path_right))?; - m.add_wrapped(wrap_pyfunction!(lt_path_by_dirblock))?; - m.add_wrapped(wrap_pyfunction!(bisect_dirblock))?; - m.add_wrapped(wrap_pyfunction!(DefaultSHA1Provider))?; - m.add_wrapped(wrap_pyfunction!(pack_stat))?; - m.add_wrapped(wrap_pyfunction!(fields_per_entry))?; - m.add_wrapped(wrap_pyfunction!(get_ghosts_line))?; - m.add_wrapped(wrap_pyfunction!(get_parents_line))?; - m.add_class::()?; - m.add_wrapped(wrap_pyfunction!(inv_entry_to_details))?; - m.add_wrapped(wrap_pyfunction!(get_output_lines))?; - - Ok(m) -} diff --git a/crates/bazaar-py/src/groupcompress.rs b/crates/bazaar-py/src/groupcompress.rs deleted file mode 100644 index 07364809ec..0000000000 --- a/crates/bazaar-py/src/groupcompress.rs +++ /dev/null @@ -1,448 +0,0 @@ -use bazaar::groupcompress::compressor::GroupCompressor; -use bazaar::versionedfile::Key; -use pyo3::exceptions::{PyRuntimeError, PyValueError}; -use pyo3::prelude::*; -use pyo3::types::PyBytes; -use pyo3::wrap_pyfunction; -use std::borrow::Cow; -use std::convert::TryInto; - -#[pyfunction] -fn encode_base128_int(py: Python, value: u128) -> PyResult> { - let mut ret = Vec::new(); - bazaar::groupcompress::delta::write_base128_int(&mut ret, value) - .map_err(|e| PyValueError::new_err(format!("Failed to encode: {}", e)))?; - Ok(PyBytes::new(py, &ret)) -} - -#[pyfunction] -fn decode_base128_int(value: Vec) -> PyResult<(u128, usize)> { - let mut cursor = std::io::Cursor::new(&value); - let val = bazaar::groupcompress::delta::read_base128_int(&mut cursor) - .map_err(|e| PyValueError::new_err(format!("Failed to decode: {}", e)))?; - Ok((val, cursor.position() as usize)) -} - -#[pyfunction] -fn apply_delta(py: Python, basis: Vec, delta: Vec) -> PyResult> { - bazaar::groupcompress::delta::apply_delta(&basis, &delta) - .map_err(|e| PyErr::new::(format!("Invalid delta: {}", e))) - .map(|x| PyBytes::new(py, &x)) -} - -#[pyfunction] -fn decode_copy_instruction(data: Vec, cmd: u8, pos: usize) -> PyResult<(usize, usize, usize)> { - let mut cursor = std::io::Cursor::new(&data[pos..]); - let (offset, length) = bazaar::groupcompress::delta::read_copy_instruction(&mut cursor, cmd) - .map_err(|_| PyValueError::new_err("Invalid copy instruction"))?; - Ok((offset, length, pos + cursor.position() as usize)) -} - -#[pyfunction] -#[pyo3(signature = (source, delta_start, delta_end))] -fn apply_delta_to_source<'a>( - py: Python<'a>, - source: &'a [u8], - delta_start: usize, - delta_end: usize, -) -> PyResult> { - if delta_start >= source.len() { - return Err(PyValueError::new_err("Invalid delta: delta starts after source")); - } - if delta_end > source.len() { - return Err(PyValueError::new_err("Invalid delta: delta ends after source")); - } - if delta_start >= delta_end { - return Err(PyValueError::new_err("Invalid delta: delta starts after it ends")); - } - let delta_bytes = &source[delta_start..delta_end]; - bazaar::groupcompress::delta::apply_delta(source, delta_bytes) - .map_err(|e| PyValueError::new_err(format!("Invalid delta: {}", e))) - .map(|x| PyBytes::new(py, &x)) -} - -#[pyfunction] -fn encode_copy_instruction(py: Python, offset: usize, length: usize) -> PyResult> { - let ret = bazaar::groupcompress::delta::encode_copy_instruction(offset, length); - Ok(PyBytes::new(py, &ret)) -} - -#[pyfunction] -fn make_line_delta<'a>( - py: Python<'a>, - source_bytes: &'a [u8], - target_bytes: &'a [u8], -) -> Bound<'a, PyBytes> { - PyBytes::new( - py, - bazaar::groupcompress::line_delta::make_delta(source_bytes, target_bytes) - .flat_map(|x| x.into_owned()) - .collect::>() - .as_slice(), - ) -} - -#[pyfunction] -fn make_rabin_delta<'a>( - py: Python<'a>, - source_bytes: &'a [u8], - target_bytes: &'a [u8], -) -> Bound<'a, PyBytes> { - PyBytes::new( - py, - bazaar::groupcompress::rabin_delta::make_delta(source_bytes, target_bytes).as_slice(), - ) -} - -#[pyclass] -pub struct LinesDeltaIndex(bazaar::groupcompress::line_delta::LinesDeltaIndex); - -#[pymethods] -impl LinesDeltaIndex { - #[new] - fn new(lines: Vec>) -> Self { - let index = bazaar::groupcompress::line_delta::LinesDeltaIndex::new(lines); - Self(index) - } - - #[getter] - fn lines<'a>(&self, py: Python<'a>) -> Vec> { - self.0 - .lines() - .iter() - .map(|x| PyBytes::new(py, x.as_ref())) - .collect() - } - - #[pyo3(signature = (source, bytes_length, soft = None))] - fn make_delta<'a>( - &'a self, - py: Python<'a>, - source: Vec>>, - bytes_length: usize, - soft: Option, - ) -> (Vec>, Vec) { - let source: Vec> = source - .iter() - .map(|x| Cow::Owned(x.iter().flatten().copied().collect::>())) - .collect::>(); - let (delta, index) = self.0.make_delta(source.as_slice(), bytes_length, soft); - ( - delta - .into_iter() - .map(|x| PyBytes::new(py, x.as_ref())) - .collect(), - index, - ) - } - - fn extend_lines(&mut self, lines: Vec>, index: Vec) -> PyResult<()> { - self.0.extend_lines(lines.as_slice(), index.as_slice()); - Ok(()) - } - - #[getter] - fn endpoint(&self) -> usize { - self.0.endpoint() - } -} - -#[pyclass(unsendable)] -struct GroupCompressBlock(bazaar::groupcompress::block::GroupCompressBlock); - -#[pymethods] -impl GroupCompressBlock { - #[new] - fn new() -> Self { - Self(bazaar::groupcompress::block::GroupCompressBlock::new()) - } - - fn __len__(&self) -> usize { - self.0.len() - } - - #[getter] - fn _z_content<'a>(&mut self, py: Python<'a>) -> PyResult> { - let ret = self.0.z_content(); - Ok(PyBytes::new(py, &ret)) - } - - #[getter] - fn _content<'a>(&mut self, py: Python<'a>) -> PyResult>> { - let ret = self.0.content(); - Ok(ret.map(|x| PyBytes::new(py, x))) - } - - #[getter] - fn _content_length(&self) -> Option { - self.0.content_length() - } - - #[classmethod] - fn from_bytes(_type: &pyo3::Bound, data: &[u8]) -> PyResult { - let ret = bazaar::groupcompress::block::GroupCompressBlock::from_bytes(data); - if ret.is_err() { - return Err(PyErr::new::( - "Invalid block", - )); - } - Ok(Self(ret.unwrap())) - } - - fn extract<'a>( - &mut self, - py: Python<'a>, - _key: Py, - offset: usize, - length: usize, - ) -> PyResult>> { - let chunks = self - .0 - .extract(offset, length) - .map_err(|e| PyValueError::new_err(format!("Error during extract: {:?}", e)))?; - Ok(chunks - .into_iter() - .map(|x| PyBytes::new(py, x.as_ref())) - .collect()) - } - - fn set_chunked_content(&mut self, data: Vec>, length: usize) -> PyResult<()> { - self.0.set_chunked_content(data.as_slice(), length); - Ok(()) - } - - #[pyo3(signature = (kind = None))] - fn to_chunks<'a>( - &mut self, - py: Python<'a>, - kind: Option, - ) -> (usize, Vec>) { - let (size, chunks) = self.0.to_chunks(kind); - - let chunks = chunks - .into_iter() - .map(|x| PyBytes::new(py, x.as_ref())) - .collect(); - - (size, chunks) - } - - fn to_bytes<'a>(&mut self, py: Python<'a>) -> PyResult> { - let ret = self.0.to_bytes(); - Ok(PyBytes::new(py, &ret)) - } - - #[pyo3(signature = (size = None))] - fn _ensure_content(&mut self, size: Option) -> PyResult<()> { - self.0.ensure_content(size); - Ok(()) - } - - #[pyo3(signature = (include_text = None))] - fn _dump(&mut self, py: Python, include_text: Option) -> PyResult> { - let ret = self - .0 - .dump(include_text) - .map_err(|e| PyValueError::new_err(format!("Error during dump: {:?}", e)))?; - - Ok(ret - .into_iter() - .map(|x| match x { - bazaar::groupcompress::block::DumpInfo::Fulltext(text) => ( - PyBytes::new(py, b"f"), - 0usize, - text.map(|x| PyBytes::new(py, x.as_ref()).unbind().into()), - ), - bazaar::groupcompress::block::DumpInfo::Delta(decomp_len, info) => ( - PyBytes::new(py, b"d"), - decomp_len, - Some( - info.into_iter() - .map(|x| match x { - bazaar::groupcompress::block::DeltaInfo::Copy( - offset, - len, - text, - ) => ( - offset, - len, - text.map(|x| PyBytes::new(py, x.as_ref()).unbind()), - ) - .into_pyobject(py) - .unwrap() - .unbind(), - bazaar::groupcompress::block::DeltaInfo::Insert(len, data) => ( - 0usize, - len, - data.map(|x| PyBytes::new(py, x.as_slice()).unbind()), - ) - .into_pyobject(py) - .unwrap() - .unbind(), - }) - .collect::>() - .into_pyobject(py) - .unwrap() - .unbind(), - ), - ), - }) - .collect::>() - .into_pyobject(py)? - .unbind()) - } -} - -#[pyclass] -struct TraditionalGroupCompressor( - Option, -); - -#[pymethods] -impl TraditionalGroupCompressor { - #[new] - #[allow(unused_variables)] - #[pyo3(signature = (settings = None))] - fn new(settings: Option>) -> Self { - Self(Some( - bazaar::groupcompress::compressor::TraditionalGroupCompressor::new(), - )) - } - - #[getter] - fn chunks<'a>(&self, py: Python<'a>) -> PyResult>> { - if let Some(c) = self.0.as_ref() { - Ok(c.chunks() - .iter() - .map(|x| PyBytes::new(py, x.as_ref())) - .collect()) - } else { - Err(PyRuntimeError::new_err("Compressor is already finalized")) - } - } - - #[getter] - fn endpoint(&self) -> PyResult { - if let Some(c) = self.0.as_ref() { - Ok(c.endpoint()) - } else { - Err(PyRuntimeError::new_err("Compressor is already finalized")) - } - } - - fn ratio(&self) -> PyResult { - if let Some(c) = self.0.as_ref() { - Ok(c.ratio()) - } else { - Err(PyRuntimeError::new_err("Compressor is already finalized")) - } - } - - fn extract<'a>( - &self, - py: Python<'a>, - key: Vec>, - ) -> PyResult<(Vec>, Bound<'a, PyBytes>)> { - if let Some(c) = self.0.as_ref() { - let (data, hash) = c - .extract(&key) - .map_err(|e| PyValueError::new_err(format!("Error during extract: {:?}", e)))?; - Ok(( - data.iter().map(|x| PyBytes::new(py, x.as_ref())).collect(), - PyBytes::new(py, hash.as_bytes()), - )) - } else { - Err(PyRuntimeError::new_err("Compressor is already finalized")) - } - } - - fn flush<'a>(&mut self, py: Python<'a>) -> PyResult<(Vec>, usize)> { - if let Some(c) = self.0.take() { - let (chunks, endpoint) = c.flush(); - Ok(( - chunks - .into_iter() - .map(|x| PyBytes::new(py, x.as_ref())) - .collect(), - endpoint, - )) - } else { - Err(PyRuntimeError::new_err("Compressor is already finalized")) - } - } - - fn flush_without_last<'a>( - &mut self, - py: Python<'a>, - ) -> PyResult<(Vec>, usize)> { - if let Some(c) = self.0.take() { - let (chunks, endpoint) = c.flush_without_last(); - Ok(( - chunks - .into_iter() - .map(|x| PyBytes::new(py, x.as_ref())) - .collect(), - endpoint, - )) - } else { - Err(PyRuntimeError::new_err("Compressor is already finalized")) - } - } - - #[pyo3(signature = (key, chunks, length, expected_sha = None, nostore_sha = None, soft = None))] - fn compress<'a>( - &mut self, - py: Python<'a>, - key: Key, - chunks: Vec>, - length: usize, - expected_sha: Option, - nostore_sha: Option, - soft: Option, - ) -> PyResult<(Bound<'a, PyBytes>, usize, usize, &'a str)> { - let chunks_l = chunks.iter().map(|x| x.as_slice()).collect::>(); - if let Some(c) = self.0.as_mut() { - c.compress( - &key, - chunks_l.as_slice(), - length, - expected_sha, - nostore_sha, - soft, - ) - .map_err(|e| PyValueError::new_err(format!("Error during compress: {:?}", e))) - .map(|(hash, size, chunks, kind)| (PyBytes::new(py, hash.as_ref()), size, chunks, kind)) - } else { - Err(PyRuntimeError::new_err("Compressor is already finalized")) - } - } -} - -#[pyfunction] -fn rabin_hash(data: Vec) -> PyResult { - Ok(bazaar::groupcompress::rabin_delta::rabin_hash( - data.try_into() - .map_err(|e| PyValueError::new_err(format!("Error during rabin_hash: {:?}", e)))?, - ) - .into()) -} - -pub(crate) fn _groupcompress_rs(py: Python) -> PyResult> { - let m = PyModule::new(py, "groupcompress")?; - m.add_wrapped(wrap_pyfunction!(encode_base128_int))?; - m.add_wrapped(wrap_pyfunction!(decode_base128_int))?; - m.add_wrapped(wrap_pyfunction!(apply_delta))?; - m.add_wrapped(wrap_pyfunction!(decode_copy_instruction))?; - m.add_wrapped(wrap_pyfunction!(encode_copy_instruction))?; - m.add_wrapped(wrap_pyfunction!(apply_delta_to_source))?; - m.add_wrapped(wrap_pyfunction!(make_line_delta))?; - m.add_wrapped(wrap_pyfunction!(make_rabin_delta))?; - m.add_wrapped(wrap_pyfunction!(rabin_hash))?; - m.add_class::()?; - m.add_class::()?; - m.add( - "NULL_SHA1", - pyo3::types::PyBytes::new(py, &bazaar::groupcompress::NULL_SHA1), - )?; - Ok(m) -} diff --git a/crates/bazaar-py/src/hashcache.rs b/crates/bazaar-py/src/hashcache.rs deleted file mode 100644 index 99d93159fd..0000000000 --- a/crates/bazaar-py/src/hashcache.rs +++ /dev/null @@ -1,237 +0,0 @@ -use bazaar::filters::ContentFilter; -use pyo3::prelude::*; -use pyo3::types::PyBytes; -use std::fs::Permissions; -use std::io::Error; -use std::path::Path; - -#[cfg(unix)] -fn permissions_from_mode(mode: u32) -> Option { - use std::os::unix::fs::PermissionsExt; - Some(Permissions::from_mode(mode)) -} - -#[cfg(windows)] -fn permissions_from_mode(_mode: u32) -> Option { - // Windows has no POSIX file mode; ignore the value. - None -} - -#[pyclass] -struct HashCache { - hashcache: Box, -} - -pub struct PyContentFilter { - content_filter: Py, -} - -#[pyclass] -struct PyChunkIterator { - input: Box, Error>> + Send + Sync>, -} - -#[pymethods] -impl PyChunkIterator { - fn __next__<'py>(&mut self, py: Python<'py>) -> PyResult>> { - match self.input.next() { - Some(Ok(item)) => Ok(Some(PyBytes::new(py, &item))), - Some(Err(e)) => Err(e.into()), - None => Ok(None), - } - } -} - -fn map_py_err_to_io_err(e: PyErr) -> Error { - Error::new(std::io::ErrorKind::Other, e.to_string()) -} - -fn map_py_err_to_iter_io_err( - e: PyErr, -) -> Box, Error>> + Send + Sync> { - Box::new(std::iter::once(Err(map_py_err_to_io_err(e)))) -} - -impl PyContentFilter { - fn _impl( - &self, - input: Box, Error>> + Send + Sync>, - worker: &str, - ) -> Box, Error>> + Send + Sync> { - Python::attach(|py| { - let worker = self.content_filter.getattr(py, worker); - let py_input = PyChunkIterator { input }; - let py_output = worker.unwrap().call1(py, (py_input,)); - if let Err(e) = py_output { - return map_py_err_to_iter_io_err(e); - } - let py_output = py_output.unwrap(); - let next = move || { - Python::attach(|py| { - let item = py_output.call_method0(py, "__next__"); - match item { - Err(e) => Some(Err(map_py_err_to_io_err(e))), - Ok(item) => { - if item.is_none(py) { - None - } else { - Some(Ok(item.extract(py).map_err(map_py_err_to_io_err).unwrap())) - } - } - } - }) - }; - Box::new(std::iter::from_fn(next)) - }) - } -} - -impl ContentFilter for PyContentFilter { - fn reader( - &self, - input: Box, Error>> + Send + Sync>, - ) -> Box, Error>> + Send + Sync> { - self._impl(input, "reader") - } - - fn writer( - &self, - input: Box, Error>> + Send + Sync>, - ) -> Box, Error>> + Send + Sync> { - self._impl(input, "worker") - } -} - -fn content_filter_to_fn( - content_filter_provider: Py, -) -> Box Box + Send + Sync> { - Box::new(move |path, ctime| { - Python::attach(|py| { - Box::new(PyContentFilter { - content_filter: content_filter_provider.call1(py, (path, ctime)).unwrap(), - }) - }) - }) -} - -fn extract_fs_time(obj: &Bound) -> Result { - if let Ok(val) = obj.extract::() { - Ok(val) - } else if let Ok(val) = obj.extract::() { - Ok(val as i64) - } else { - Err(PyErr::new::( - "Expected int or float", - )) - } -} - -#[pymethods] -impl HashCache { - #[new] - #[pyo3(signature = ( - root, - cache_file_name, - mode = None, - content_filter_provider = None - ))] - fn new( - root: &str, - cache_file_name: &str, - mode: Option, - content_filter_provider: Option>, - ) -> Self { - Self { - hashcache: Box::new(bazaar::hashcache::HashCache::new( - Path::new(root), - Path::new(cache_file_name), - mode.and_then(permissions_from_mode), - content_filter_provider.map(content_filter_to_fn), - )), - } - } - - fn cache_file_name(&self) -> &str { - self.hashcache.cache_file_name().to_str().unwrap() - } - - fn clear(&mut self) { - self.hashcache.clear(); - } - - fn scan(&mut self) { - self.hashcache.scan(); - } - - #[pyo3(signature = (path, stat_value = None))] - fn get_sha1<'a>( - &mut self, - py: Python<'a>, - path: &str, - stat_value: Option>, - ) -> PyResult> { - let sha1; - if let Some(stat_value) = stat_value { - let fp = bazaar::hashcache::Fingerprint { - size: stat_value.getattr("st_size")?.extract()?, - mtime: extract_fs_time(&stat_value.getattr("st_mtime")?)?, - ctime: extract_fs_time(&stat_value.getattr("st_ctime")?)?, - ino: stat_value.getattr("st_ino")?.extract()?, - dev: stat_value.getattr("st_dev")?.extract()?, - mode: stat_value.getattr("st_mode")?.extract()?, - }; - sha1 = self - .hashcache - .get_sha1_by_fingerprint(Path::new(path), &fp)?; - } else { - let ret = self.hashcache.get_sha1(Path::new(path), None)?; - if let Some(s) = ret { - sha1 = s; - } else { - return Ok(py.None().into_bound(py)); - } - } - Ok(PyBytes::new(py, sha1.as_bytes()).into_any()) - } - - fn write(&mut self) -> PyResult<()> { - self.hashcache.write().map_err(|e| e.into()) - } - - fn read(&mut self) -> PyResult<()> { - self.hashcache.read().map_err(|e| e.into()) - } - - fn cutoff_time(&self) -> i64 { - self.hashcache.cutoff_time() - } - - fn set_cutoff_offset(&mut self, offset: i64) { - self.hashcache.set_cutoff_offset(offset); - } - - #[getter] - fn miss_count(&self) -> u32 { - self.hashcache.miss_count() - } - - #[getter] - fn hit_count(&self) -> u32 { - self.hashcache.hit_count() - } - - #[getter] - fn needs_write(&self) -> bool { - self.hashcache.needs_write() - } - - fn fingerprint(&self, abspath: &str) -> Option<(u64, i64, i64, u64, u64, u32)> { - let fp = self.hashcache.fingerprint(Path::new(abspath), None); - fp.map(|fp| (fp.size, fp.mtime, fp.ctime, fp.ino, fp.dev, fp.mode)) - } -} - -pub(crate) fn hashcache(m: &Bound) -> PyResult<()> { - m.add_class::()?; - Ok(()) -} diff --git a/crates/bazaar-py/src/inventory.rs b/crates/bazaar-py/src/inventory.rs deleted file mode 100644 index f31d5a5ad3..0000000000 --- a/crates/bazaar-py/src/inventory.rs +++ /dev/null @@ -1,1823 +0,0 @@ -use bazaar::inventory::{describe_change, detect_changes, Entry, Error, Inventory as _}; -use bazaar::inventory_delta::{ - InventoryDeltaEntry, InventoryDeltaInconsistency, InventoryDeltaParseError, - InventoryDeltaSerializeError, -}; -use bazaar::{FileId, RevisionId}; -use breezy_osutils::Kind; -use pyo3::class::basic::CompareOp; -use pyo3::exceptions::{ - PyIndexError, PyKeyError, PyNotImplementedError, PyTypeError, PyValueError, -}; -use pyo3::prelude::*; -use pyo3::pyclass_init::PyClassInitializer; - -type PyObject = Py; -use pyo3::types::{PyBytes, PyDict, PyString}; -use pyo3::wrap_pyfunction; -use pyo3::{create_exception, import_exception}; -use std::collections::HashMap; -use std::collections::HashSet; -use std::collections::VecDeque; - -use std::iter::FromIterator; - -import_exception!(breezy.bzr.inventory, InvalidEntryName); -import_exception!(breezy.bzr.inventory, DuplicateFileId); -import_exception!(breezy.errors, NoSuchId); -import_exception!(breezy.errors, BzrCheckError); -import_exception!(breezy.errors, InvalidNormalization); -import_exception!(breezy.errors, InconsistentDelta); -import_exception!(breezy.errors, AlreadyVersionedError); -import_exception!(breezy.errors, BzrError); -import_exception!(breezy.errors, NotVersionedError); -create_exception!(breezy.inventory_delta, IncompatibleInventoryDelta, BzrError); -create_exception!(breezy.inventory_delta, InventoryDeltaError, BzrError); - -fn kind_from_str(kind: &str) -> Option { - match kind { - "file" => Some(Kind::File), - "directory" => Some(Kind::Directory), - "tree-reference" => Some(Kind::TreeReference), - "symlink" => Some(Kind::Symlink), - _ => None, - } -} - -fn check_name(name: &str) -> PyResult<()> { - if !is_valid_name(name) { - Err(InvalidEntryName::new_err((name.to_string(),))) - } else { - Ok(()) - } -} - -fn common_ie_check( - slf: PyObject, - ie: &Entry, - py: Python, - checker: &PyObject, - rev_id: &RevisionId, - inv: PyObject, -) -> PyResult<()> { - if let Some(parent_id) = ie.parent_id() { - let present = inv - .call_method1(py, "has_id", (parent_id,))? - .extract::(py)?; - if !present { - return Err(BzrCheckError::new_err(format!( - "missing parent {{{}}} in inventory for revision {{{}}}", - parent_id, rev_id - ))); - } - } - - checker.call_method1(py, "_add_entry_to_text_key_references", (inv, slf))?; - - Ok(()) -} - -#[pyclass(subclass)] -pub struct InventoryEntry(pub Entry); - -#[pymethods] -impl InventoryEntry { - fn has_text(&self) -> bool { - matches!(&self.0, Entry::File { .. }) - } - - fn kind_character(&self) -> &'static str { - self.0.kind().marker() - } - - #[getter] - fn kind(&self) -> &'static str { - self.0.kind().to_string() - } - - #[getter] - fn get_name(&self) -> &str { - match &self.0 { - Entry::File { name, .. } => name, - Entry::Directory { name, .. } => name, - Entry::TreeReference { name, .. } => name, - Entry::Link { name, .. } => name, - Entry::Root { .. } => "", - } - } - - #[getter] - fn get_file_id<'a>(&self, py: Python<'a>) -> PyResult> { - let file_id = self.0.file_id(); - - file_id.into_pyobject(py) - } - - #[getter] - fn get_parent_id<'py>(&self, py: Python<'py>) -> Option> { - let parent_id = self.0.parent_id(); - - parent_id.map(|parent_id| parent_id.into_pyobject(py).unwrap()) - } - - #[getter] - fn get_revision<'py>(&self, py: Python<'py>) -> Option> { - let revision = self.0.revision(); - - revision - .as_ref() - .map(|revision| revision.into_pyobject(py).unwrap()) - } - - #[staticmethod] - fn versionable_kind(kind: &str) -> bool { - if let Some(kind) = kind_from_str(kind) { - bazaar::inventory::versionable_kind(kind) - } else { - false - } - } - - #[getter] - fn get_executable(&self) -> bool { - match &self.0 { - Entry::File { executable, .. } => *executable, - _ => false, - } - } - - fn is_unmodified(&self, other: &InventoryEntry) -> bool { - self.0.is_unmodified(&other.0) - } - - fn detect_changes(&self, other: &InventoryEntry) -> (bool, bool) { - detect_changes(&self.0, &other.0) - } - - #[staticmethod] - #[pyo3(signature = (slf=None, other=None))] - fn describe_change(slf: Option<&InventoryEntry>, other: Option<&InventoryEntry>) -> String { - describe_change(slf.map(|s| &s.0), other.map(|o| &o.0)).to_string() - } - - fn __richcmp__(&self, other: &InventoryEntry, op: CompareOp) -> PyResult { - match op { - CompareOp::Eq => Ok(self.0 == other.0), - CompareOp::Ne => Ok(self.0 != other.0), - _ => Err(PyNotImplementedError::new_err("")), - } - } - - fn _unchanged(&self, other: &InventoryEntry) -> bool { - self.0.unchanged(&other.0) - } - - #[pyo3(signature = (revision=None, name=None, parent_id=None))] - fn derive( - &self, - revision: Option, - name: Option, - parent_id: Option, - ) -> InventoryEntry { - let mut entry = self.0.clone(); - let revision = revision.or_else(|| entry.revision().cloned()); - let name = name.unwrap_or_else(|| entry.name().to_string()); - let parent_id = parent_id.or_else(|| entry.parent_id().cloned()); - match &mut entry { - Entry::File { - revision: r, - name: n, - parent_id: p, - .. - } => { - *r = revision; - *n = name; - *p = parent_id.unwrap(); - } - Entry::Directory { - revision: r, - name: n, - parent_id: p, - .. - } => { - *r = revision; - *n = name; - *p = parent_id.unwrap(); - } - Entry::TreeReference { - revision: r, - name: n, - parent_id: p, - .. - } => { - *r = revision; - *n = name; - *p = parent_id.unwrap(); - } - Entry::Link { - revision: r, - name: n, - parent_id: p, - .. - } => { - *r = revision; - *n = name; - *p = parent_id.unwrap(); - } - Entry::Root { revision: r, .. } => { - *r = revision; - } - } - InventoryEntry(entry) - } - - /// Find possible per-file graph parents. - /// - /// This is currently defined by: - /// Select the last changed revision in the parent inventory. - /// Do deal with a short lived bug in bzr 0.8's development two entries - /// that have the same last changed but different 'x' bit settings are - /// changed in-place. - fn parent_candidates<'py>( - &self, - py: Python<'py>, - previous_inventories: Vec, - ) -> PyResult> { - // revision:ie mapping for each ie found in previous_inventories - let mut candidates: HashMap<&RevisionId, PyObject> = HashMap::new(); - // identify candidate head revision ids - for inv in previous_inventories { - match inv.call_method1(py, "get_entry", (self.get_file_id(py)?,)) { - Ok(py_entry) => { - if let Ok(mut entry) = py_entry.extract::>(py) { - if let Some(revision) = entry.0.revision() { - if let Some(candidate) = candidates.get_mut(revision) { - // same revision value in two different inventories: - // correct possible inconsistencies: - // * there was a bug in revision updates with executable bit support - let mut candidate = - candidate.extract::>(py)?; - if let ( - Entry::File { - executable: candidate_executable, - .. - }, - Entry::File { - executable: entry_executable, - .. - }, - ) = (&mut candidate.0, &mut entry.0) - { - if candidate_executable != entry_executable { - *entry_executable = false; - *candidate_executable = false; - } - } - } else { - // add this revision as a candidate. - //candidates.insert(revision, py_entry); - } - } - } - } - Err(e) if e.is_instance_of::(py) => {} - Err(e) => { - return Err(e); - } - } - } - let ret = PyDict::new(py); - for (revision, entry) in candidates.into_iter() { - ret.set_item(revision, entry)?; - } - Ok(ret) - } -} - -#[pyclass(subclass,extends=InventoryEntry)] -struct InventoryFile(); - -#[pymethods] -impl InventoryFile { - #[new] - #[pyo3(signature = (file_id, name, parent_id, revision=None, text_sha1=None, text_size=None, executable=None, text_id=None))] - fn new( - file_id: FileId, - name: String, - parent_id: FileId, - revision: Option, - text_sha1: Option>, - text_size: Option, - executable: Option, - text_id: Option>, - ) -> PyResult<(Self, InventoryEntry)> { - let executable = executable.unwrap_or(false); - check_name(name.as_str())?; - let entry = Entry::File { - file_id, - name, - parent_id, - revision, - text_sha1, - text_size, - text_id, - executable, - }; - Ok((Self(), InventoryEntry(entry))) - } - - #[getter] - fn get_executable(slf: PyRef) -> bool { - match slf.into_super().0 { - Entry::File { executable, .. } => executable, - _ => false, - } - } - - #[getter] - fn get_text_sha1(slf: PyRef, py: Python) -> Option { - let s = slf.into_super(); - match &s.0 { - Entry::File { text_sha1, .. } => text_sha1 - .as_ref() - .map(|text_sha1| PyBytes::new(py, text_sha1.as_ref()).into()), - _ => panic!("Not a file"), - } - } - - #[getter] - fn get_text_size(slf: PyRef) -> Option { - let s = slf.into_super(); - match &s.0 { - Entry::File { text_size, .. } => *text_size, - _ => panic!("Not a file"), - } - } - - #[getter] - fn get_text_id(slf: PyRef, py: Python) -> Option { - let s = slf.into_super(); - match &s.0 { - Entry::File { text_id, .. } => text_id - .as_ref() - .map(|text_id| PyBytes::new(py, text_id).into()), - _ => panic!("Not a file"), - } - } - - #[getter] - fn get_reference_revision(_slf: PyRef, py: Python) -> PyObject { - py.None() - } - - fn copy<'a>(slf: PyRef<'a, Self>, py: Python<'a>) -> PyResult> { - let s = slf.into_super(); - let init = PyClassInitializer::from(InventoryEntry(s.0.clone())); - let init = init.add_subclass(Self()); - Bound::new(py, init) - } - - fn __repr__(slf: PyRef, py: Python) -> PyResult { - let s = slf.into_super(); - Ok(match &s.0 { - Entry::File { - name, - file_id, - parent_id, - text_sha1, - text_size, - revision, - .. - } => format!( - "InventoryFile({}, {}, parent_id={}, sha1={}, len={}, revision={})", - file_id.into_pyobject(py).unwrap().repr()?, - name.into_pyobject(py).unwrap().repr()?, - parent_id.into_pyobject(py).unwrap().repr()?, - text_sha1 - .as_ref() - .map(|s| PyBytes::new(py, s.as_slice()).repr()) - .unwrap_or_else(|| Ok(PyString::new(py, "None")))?, - text_size.into_pyobject(py).unwrap().repr()?, - revision - .as_ref() - .map(|r| r.into_pyobject(py).unwrap()) - .into_pyobject(py) - .unwrap() - .repr()?, - ), - _ => panic!("Not a file"), - }) - } - - fn check( - slf: &Bound, - py: Python, - checker: PyObject, - rev_id: RevisionId, - inv: PyObject, - ) -> PyResult<()> { - let spr = slf.borrow().into_super(); - common_ie_check( - slf.clone().unbind().into(), - &spr.0, - py, - &checker, - &rev_id, - inv, - )?; - - let (file_id, revision, text_sha1, text_size) = match spr.0 { - Entry::File { - ref text_sha1, - ref file_id, - ref revision, - text_size, - .. - } => (file_id, revision, text_sha1, text_size), - _ => panic!("Not a file"), - }; - - checker.call_method1( - py, - "add_pending_item", - ( - &rev_id, - ("texts", &file_id, &revision), - PyBytes::new(py, b"text"), - PyBytes::new(py, text_sha1.as_ref().unwrap()), - ), - )?; - - if text_size.is_none() { - checker.getattr(py, "_report_items")?.call_method1( - py, - "append", - (format!( - "fileid {{{}}} in {{{}}} has None for text_size", - file_id, rev_id - ),), - )?; - } - - Ok(()) - } -} - -#[pyclass(subclass,extends=InventoryEntry)] -struct InventoryDirectory(); - -#[pymethods] -impl InventoryDirectory { - #[new] - #[pyo3(signature = (file_id, name, parent_id=None, revision=None))] - fn new( - file_id: FileId, - name: String, - parent_id: Option, - revision: Option, - ) -> PyResult<(Self, InventoryEntry)> { - check_name(name.as_str())?; - let entry = if let Some(parent_id) = parent_id { - Entry::Directory { - file_id, - name, - parent_id, - revision, - } - } else { - Entry::Root { file_id, revision } - }; - Ok((Self(), InventoryEntry(entry))) - } - - fn copy<'py>(slf: PyRef, py: Python<'py>) -> PyResult> { - let s = slf.into_super(); - let init = PyClassInitializer::from(InventoryEntry(s.0.clone())); - let init = init.add_subclass(Self()); - Bound::new(py, init) - } - - #[getter] - fn get_text_size(&self, py: Python) -> PyObject { - py.None() - } - - #[getter] - fn get_text_sha1(&self, py: Python) -> PyObject { - py.None() - } - - fn __repr__(slf: PyRef, py: Python) -> PyResult { - let s = slf.into_super(); - Ok(match &s.0 { - Entry::Directory { - name, - file_id, - parent_id, - revision, - .. - } => format!( - "InventoryDirectory({}, {}, parent_id={}, revision={})", - file_id.into_pyobject(py).unwrap().repr()?, - name.into_pyobject(py).unwrap().repr()?, - parent_id.into_pyobject(py).unwrap().repr()?, - revision.into_pyobject(py).unwrap().repr()?, - ), - Entry::Root { - file_id, revision, .. - } => format!( - "InventoryDirectory({}, \"\", parent_id=None, revision={})", - file_id.into_pyobject(py).unwrap().repr()?, - revision.into_pyobject(py).unwrap().repr()?, - ), - _ => panic!("Not a directory"), - }) - } - - fn check( - slf: &Bound, - py: Python, - checker: PyObject, - rev_id: RevisionId, - inv: PyObject, - ) -> PyResult<()> { - let spr = slf.borrow().into_super(); - common_ie_check( - slf.clone().unbind().into(), - &spr.0, - py, - &checker, - &rev_id, - inv, - )?; - - // In non rich root repositories we do not expect a file graph for the - // root. - if spr.0.name().is_empty() && !checker.getattr(py, "rich_roots")?.extract::(py)? { - return Ok(()); - } - // Directories are stored as an empty file, but the file should exist - // to provide a per-fileid log. The hash of every directory content is - // "da..." below (the sha1sum of ''). - checker.call_method1( - py, - "add_pending_item", - ( - &rev_id, - ("texts", spr.0.file_id(), spr.0.revision()), - PyBytes::new(py, b"text"), - PyBytes::new(py, b"da39a3ee5e6b4b0d3255bfef95601890afd80709"), - ), - )?; - - Ok(()) - } -} - -#[pyclass(subclass,extends=InventoryEntry)] -struct TreeReference(); - -#[pymethods] -impl TreeReference { - #[new] - #[pyo3(signature = (file_id, name, parent_id, revision=None, reference_revision=None))] - fn new( - file_id: FileId, - name: String, - parent_id: FileId, - revision: Option, - reference_revision: Option, - ) -> PyResult<(Self, InventoryEntry)> { - check_name(name.as_str())?; - let entry = Entry::TreeReference { - file_id, - name, - parent_id, - revision, - reference_revision, - }; - Ok((Self(), InventoryEntry(entry))) - } - - #[getter] - fn get_reference_revision<'a>( - slf: PyRef<'a, Self>, - py: Python<'a>, - ) -> Option> { - let s = slf.into_super(); - match &s.0 { - Entry::TreeReference { - reference_revision, .. - } => reference_revision - .as_ref() - .map(|reference_revision| reference_revision.into_pyobject(py).unwrap()), - _ => panic!("Not a tree reference"), - } - } - - fn copy<'py>(slf: PyRef, py: Python<'py>) -> PyResult> { - let s = slf.into_super(); - let init = PyClassInitializer::from(InventoryEntry(s.0.clone())); - let init = init.add_subclass(Self()); - Bound::new(py, init) - } -} - -#[pyclass(subclass,extends=InventoryEntry)] -struct InventoryLink(); - -#[pymethods] -impl InventoryLink { - #[new] - #[pyo3(signature = (file_id, name, parent_id, revision=None, symlink_target=None))] - fn new( - file_id: FileId, - name: String, - parent_id: FileId, - revision: Option, - symlink_target: Option, - ) -> PyResult<(Self, InventoryEntry)> { - check_name(name.as_str())?; - let entry = Entry::Link { - file_id, - name, - parent_id, - symlink_target, - revision, - }; - Ok((Self(), InventoryEntry(entry))) - } - - #[getter] - fn get_symlink_target(slf: PyRef) -> Option { - let s = slf.into_super(); - match s.0 { - Entry::Link { - ref symlink_target, .. - } => symlink_target.clone(), - _ => panic!("Not a link"), - } - } - - fn copy<'py>(slf: PyRef, py: Python<'py>) -> PyResult> { - let s = slf.into_super(); - let init = PyClassInitializer::from(InventoryEntry(s.0.clone())); - let init = init.add_subclass(Self()); - Bound::new(py, init) - } - - #[getter] - fn get_text_size(&self, py: Python) -> PyObject { - py.None() - } - - #[getter] - fn get_text_sha1(&self, py: Python) -> PyObject { - py.None() - } - - fn check( - slf: &Bound, - py: Python, - checker: PyObject, - rev_id: RevisionId, - inv: PyObject, - ) -> PyResult<()> { - let spr = slf.borrow().into_super(); - common_ie_check( - slf.clone().unbind().into(), - &spr.0, - py, - &checker, - &rev_id, - inv, - )?; - - if spr.0.symlink_target().is_none() { - let report_items = checker.getattr(py, "_report_items")?; - report_items.call_method1( - py, - "append", - (format!( - "symlink {} has no target in revision {}", - spr.0.file_id(), - spr.0 - .revision() - .map_or_else(|| String::from("None"), |p| p.to_string()) - ),), - )?; - } - - // Symlinks are stored as '' - checker.call_method1( - py, - "add_pending_item", - ( - &rev_id, - ("texts", spr.0.file_id(), spr.0.revision()), - PyBytes::new(py, b"text"), - PyBytes::new(py, b"da39a3ee5e6b4b0d3255bfef95601890afd80709"), - ), - )?; - Ok(()) - } -} - -fn entry_to_py(py: Python, e: Entry) -> PyResult> { - let kind = e.kind(); - let init = PyClassInitializer::from(InventoryEntry(e)); - match kind { - Kind::File => { - let init = init.add_subclass(InventoryFile()); - Ok(Bound::new(py, init)?.into_any()) - } - Kind::Directory => { - let init = init.add_subclass(InventoryDirectory()); - Ok(Bound::new(py, init)?.into_any()) - } - Kind::TreeReference => { - let init = init.add_subclass(TreeReference()); - Ok(Bound::new(py, init)?.into_any()) - } - Kind::Symlink => { - let init = init.add_subclass(InventoryLink()); - Ok(Bound::new(py, init)?.into_any()) - } - } -} - -#[pyfunction] -#[allow(clippy::too_many_arguments)] -#[pyo3(signature = (kind, name, parent_id=None, revision=None, file_id=None, text_sha1=None, text_size=None, executable=None, text_id=None, symlink_target=None, reference_revision=None))] -fn make_entry<'a>( - py: Python<'a>, - kind: &'a str, - name: &'a str, - parent_id: Option, - revision: Option, - file_id: Option, - text_sha1: Option>, - text_size: Option, - executable: Option, - text_id: Option>, - symlink_target: Option, - reference_revision: Option, -) -> PyResult> { - let kind = match kind { - "file" => Kind::File, - "directory" => Kind::Directory, - "tree-reference" => Kind::TreeReference, - "symlink" => Kind::Symlink, - _ => panic!("Unknown kind"), - }; - entry_to_py( - py, - bazaar::inventory::make_entry( - kind, - name.to_string(), - file_id, - parent_id, - revision, - text_sha1, - text_size, - executable, - text_id, - symlink_target, - reference_revision, - ) - .map_err(|e| inventory_err_to_py_err(e, py))?, - ) -} - -#[pyfunction] -fn is_valid_name(name: &str) -> bool { - bazaar::inventory::is_valid_name(name) -} - -#[pyfunction] -fn ensure_normalized_name(name: std::path::PathBuf) -> PyResult { - let path = bazaar::inventory::ensure_normalized_name(name.as_path()) - .map_err(|_e| InvalidNormalization::new_err(name.clone()))?; - - path.to_str().map(|s| s.to_string()).ok_or_else(|| { - PyValueError::new_err(format!( - "Invalid normalization for path: {}", - name.display() - )) - }) -} - -fn delta_err_to_py_err(py: Python, e: InventoryDeltaInconsistency) -> PyErr { - match e { - InventoryDeltaInconsistency::NoPath => { - InconsistentDelta::new_err(("", "", "No path in entry")) - } - InventoryDeltaInconsistency::DuplicateFileId(ref path, ref fid) => { - InconsistentDelta::new_err((path.clone(), fid.clone(), "repeated file_id")) - } - InventoryDeltaInconsistency::DuplicateOldPath(path, fid) => { - InconsistentDelta::new_err((path, fid, "repeated path")) - } - InventoryDeltaInconsistency::DuplicateNewPath(path, fid) => { - InconsistentDelta::new_err((path, fid, "repeated path")) - } - InventoryDeltaInconsistency::MismatchedId(path, fid1, fid2) => { - InconsistentDelta::new_err((path, fid1, format!("mismatched id with entry {}", fid2))) - } - InventoryDeltaInconsistency::EntryWithoutPath(path, fid) => { - InconsistentDelta::new_err((path, fid, "Entry with no new_path")) - } - InventoryDeltaInconsistency::PathWithoutEntry(path, fid) => { - InconsistentDelta::new_err((path, fid, "new_path with no entry")) - } - InventoryDeltaInconsistency::OrphanedChild(fid) => { - InconsistentDelta::new_err(("", fid, "orphaned child")) - } - InventoryDeltaInconsistency::NoSuchId(fid) => NoSuchId::new_err((py.None(), fid)), - InventoryDeltaInconsistency::PathMismatch(fid, path1, path2) => { - InconsistentDelta::new_err((path1, fid, format!("path mismatch != {}", path2))) - } - InventoryDeltaInconsistency::ParentMissing(fid) => { - InconsistentDelta::new_err(("", fid, "parent missing")) - } - InventoryDeltaInconsistency::InvalidEntryName(name) => InvalidEntryName::new_err((name,)), - InventoryDeltaInconsistency::FileIdCycle(fid, path, parent_path) => { - InconsistentDelta::new_err((path, fid, format!("file_id cycle with {}", parent_path))) - } - InventoryDeltaInconsistency::ParentNotDirectory(path, fid) => { - InconsistentDelta::new_err((path, fid, "parent is not a directory")) - } - InventoryDeltaInconsistency::PathAlreadyVersioned(name, parent_path) => { - InconsistentDelta::new_err((name, parent_path, "path already versioned")) - } - } -} - -#[pyclass] -struct InventoryDelta(bazaar::inventory_delta::InventoryDelta); - -#[pymethods] -impl InventoryDelta { - #[new] - #[allow(clippy::type_complexity)] - #[pyo3(signature = (delta=None))] - fn new( - _py: Python, - delta: Option< - Vec<( - Option, - Option, - FileId, - Option>, - )>, - >, - ) -> PyResult { - let delta = delta.unwrap_or_default(); - let delta = delta - .into_iter() - .map(|(old_name, new_name, file_id, entry)| { - let old_name = old_name.as_deref(); - let new_name = new_name.as_deref(); - let entry = entry.as_ref().map(|e| e.0.clone()); - InventoryDeltaEntry { - old_path: old_name.map(|s| s.to_string()), - new_path: new_name.map(|s| s.to_string()), - file_id, - new_entry: entry, - } - }) - .collect::>(); - Ok(Self(bazaar::inventory_delta::InventoryDelta::from(delta))) - } - - fn __nonzero__(slf: PyRef) -> bool { - !slf.0.is_empty() - } - - fn sort(&mut self) { - self.0.sort(); - } - - fn __len__(&self) -> usize { - self.0.len() - } - - fn __richcmp__(&self, other: PyRef, op: CompareOp) -> PyResult> { - match op { - CompareOp::Eq => Ok(Some(self.0 == other.0)), - CompareOp::Ne => Ok(Some(self.0 != other.0)), - _ => Err(PyNotImplementedError::new_err( - "Only == and != are supported", - )), - } - } - - fn __getitem__<'a>( - &self, - py: Python<'a>, - index: isize, - ) -> PyResult<(Option, Option, FileId, Bound<'a, PyAny>)> { - let index: usize = if index < 0 { - (self.0.len() as isize + index) as usize - } else { - index as usize - }; - let entry = self - .0 - .get(index) - .ok_or(PyIndexError::new_err("Index out of bounds"))?; - Ok(( - entry.old_path.clone(), - entry.new_path.clone(), - entry.file_id.clone(), - entry.new_entry.as_ref().map_or_else( - || Ok(py.None().into_bound(py)), - |e| entry_to_py(py, e.clone()), - )?, - )) - } - - fn check(&self, py: Python) -> PyResult<()> { - self.0.check().map_err(|e| match e { - InventoryDeltaInconsistency::NoPath => { - InconsistentDelta::new_err(("", "", "No path in entry")) - } - InventoryDeltaInconsistency::DuplicateFileId(ref path, ref fid) => { - InconsistentDelta::new_err((path.clone(), fid.clone(), "repeated file_id")) - } - InventoryDeltaInconsistency::DuplicateOldPath(path, fid) => { - InconsistentDelta::new_err((path, fid, "repeated path")) - } - InventoryDeltaInconsistency::DuplicateNewPath(path, fid) => { - InconsistentDelta::new_err((path, fid, "repeated path")) - } - InventoryDeltaInconsistency::MismatchedId(path, fid1, fid2) => { - InconsistentDelta::new_err(( - path, - fid1, - format!("mismatched id with entry {}", fid2), - )) - } - InventoryDeltaInconsistency::PathMismatch(fid, path1, path2) => { - InconsistentDelta::new_err(( - path1, - fid, - format!("mismatched path with entry {}", path2), - )) - } - InventoryDeltaInconsistency::OrphanedChild(fid) => { - InconsistentDelta::new_err(("", fid, "orphaned child")) - } - InventoryDeltaInconsistency::ParentNotDirectory(path, fid) => { - InconsistentDelta::new_err((path, fid, "parent not directory")) - } - InventoryDeltaInconsistency::ParentMissing(fid) => { - InconsistentDelta::new_err(("", fid, "parent missing")) - } - InventoryDeltaInconsistency::NoSuchId(fid) => NoSuchId::new_err((py.None(), fid)), - InventoryDeltaInconsistency::InvalidEntryName(n) => InvalidEntryName::new_err((n,)), - InventoryDeltaInconsistency::FileIdCycle(fid, path, parent_path) => { - InconsistentDelta::new_err(( - path, - fid, - format!("file_id cycle with {}", parent_path), - )) - } - InventoryDeltaInconsistency::PathAlreadyVersioned(path, fid) => { - InconsistentDelta::new_err((path, fid, "path already versioned")) - } - InventoryDeltaInconsistency::EntryWithoutPath(path, fid) => { - InconsistentDelta::new_err((path, fid, "Entry with no new_path")) - } - InventoryDeltaInconsistency::PathWithoutEntry(path, fid) => { - InconsistentDelta::new_err((path, fid, "new_path with no entry")) - } - }) - } - - fn __repr__(&self) -> String { - format!("{:?}", self.0) - } -} - -fn inventory_err_to_py_err(e: Error, py: Python) -> PyErr { - match e { - Error::InvalidEntryName(name) => InvalidEntryName::new_err((name,)), - Error::InvalidNormalization(n, _) => InvalidNormalization::new_err((n,)), - Error::DuplicateFileId(fid, path) => DuplicateFileId::new_err((fid, path)), - Error::NoSuchId(fid) => NoSuchId::new_err((py.None(), fid)), - Error::ParentNotDirectory(path, fid) => { - InconsistentDelta::new_err((path, fid, "parent not directory")) - } - Error::FileIdCycle(fid, path, parent_path) => { - InconsistentDelta::new_err((path, fid, format!("file_id cycle with {}", parent_path))) - } - Error::ParentMissing(fid) => InconsistentDelta::new_err(("", fid, "parent missing")), - Error::PathAlreadyVersioned(name, parent_path) => { - AlreadyVersionedError::new_err(format!("{}/{}", parent_path, name)) - } - Error::ParentNotVersioned(path) => { - NotVersionedError::new_err(format!("parent not versioned: {}", path)) - } - } -} - -#[pyclass] -struct Inventory(bazaar::inventory::MutableInventory); - -#[pymethods] -impl Inventory { - #[new] - #[pyo3(signature = (root_id=b"TREE_ROOT".to_vec(), revision_id=None, root_revision=None))] - fn new( - root_id: Option>, - revision_id: Option, - root_revision: Option, - ) -> PyResult { - let root_id = root_id.map(bazaar::FileId::from); - let mut inv = Inventory(bazaar::inventory::MutableInventory::new()); - - if let Some(root_id) = root_id { - let root = bazaar::inventory::Entry::root(root_id, root_revision); - inv.0.add(root).unwrap(); - } else if root_revision.is_some() { - return Err(PyTypeError::new_err("root_revision requires root_id")); - } - inv.0.revision_id = revision_id; - Ok(inv) - } - - #[getter] - fn root<'py>(&self, py: Python<'py>) -> PyResult> { - if let Some(root) = self.0.root() { - entry_to_py(py, root.clone()) - } else { - Ok(py.None().into_bound(py)) - } - } - - fn add(&mut self, py: Python, entry: &InventoryEntry) -> PyResult<()> { - self.0 - .add(entry.0.clone()) - .map_err(|e| inventory_err_to_py_err(e, py))?; - Ok(()) - } - - #[pyo3(signature = (relpath, kind, file_id=None, revision=None, text_sha1=None, text_size=None, executable=None, text_id=None, symlink_target=None, reference_revision=None))] - fn add_path<'py>( - &mut self, - py: Python<'py>, - relpath: &str, - kind: breezy_osutils::Kind, - file_id: Option, - revision: Option, - text_sha1: Option>, - text_size: Option, - executable: Option, - text_id: Option>, - symlink_target: Option, - reference_revision: Option, - ) -> PyResult> { - let file_id = self - .0 - .add_path( - relpath, - kind, - file_id, - revision, - text_sha1, - text_size, - executable, - text_id, - symlink_target, - reference_revision, - ) - .map_err(|e| inventory_err_to_py_err(e, py))?; - self.get_entry(py, file_id) - } - - #[getter] - fn get_revision_id(&self) -> Option { - self.0.revision_id.as_ref().cloned() - } - - #[setter] - fn set_revision_id(&mut self, revision_id: Option) { - self.0.revision_id = revision_id; - } - - fn id2path(&self, py: Python, file_id: FileId) -> PyResult { - self.0 - .id2path(&file_id) - .map_err(|e| inventory_err_to_py_err(e, py)) - } - - fn path2id(&self, path: &str) -> Option { - self.0.path2id(path).cloned() - } - - fn is_root(&self, file_id: FileId) -> PyResult { - Ok(self.0.is_root(file_id)) - } - - fn has_filename(&self, name: &str) -> PyResult { - Ok(self.0.has_filename(name)) - } - - fn get_children<'py>( - &self, - py: Python<'py>, - file_id: FileId, - ) -> PyResult>> { - let children = self.0.get_children(&file_id); - if children.is_none() { - return Err(NoSuchId::new_err((py.None(), file_id))); - } - let children = children.unwrap(); - let mut result = HashMap::with_capacity(children.len()); - for (name, child) in children { - result.insert(name.to_string(), entry_to_py(py, child.clone())?); - } - Ok(result) - } - - fn entries<'py>(&self, py: Python<'py>) -> PyResult)>> { - let entries = self.0.entries(); - let mut result = Vec::with_capacity(entries.len()); - for (name, entry) in entries { - result.push((name, entry_to_py(py, entry.clone())?)); - } - Ok(result) - } - - fn rename_id(&mut self, py: Python, old_file_id: FileId, new_file_id: FileId) -> PyResult<()> { - self.0 - .rename_id(&old_file_id, &new_file_id) - .map_err(|e| inventory_err_to_py_err(e, py)) - } - - fn path2id_segments(&self, names: Vec) -> Option { - let names = names.iter().map(|s| s.as_str()).collect::>(); - self.0.path2id_segments(names.as_slice()).cloned() - } - - fn filter(&self, py: Python, specific_fileids: HashSet) -> PyResult { - let result = self - .0 - .filter(&specific_fileids.iter().collect()) - .map_err(|e| inventory_err_to_py_err(e, py))?; - Ok(Self(result)) - } - - fn get_entry_by_path_partial<'py>( - &self, - py: Python<'py>, - relpath: PyObject, - ) -> PyResult<( - Option>, - Option>, - Option>, - )> { - let ret = if let Ok(relpath) = relpath.extract::(py) { - self.0.get_entry_by_path_partial(&relpath) - } else if let Ok(segments) = relpath.extract::>(py) { - let segments = segments.iter().map(|s| s.as_str()).collect::>(); - self.0 - .get_entry_by_path_segments_partial(segments.as_slice()) - } else { - return Err(PyTypeError::new_err("expected str or list of str")); - }; - - if let Some((e, segments, missing)) = ret { - Ok(( - Some(entry_to_py(py, e.clone())?), - Some(segments), - Some(missing), - )) - } else { - Ok((None, None, None)) - } - } - - fn get_entry_by_path<'py>( - &self, - py: Python<'py>, - relpath: PyObject, - ) -> PyResult>> { - if let Ok(relpath) = relpath.extract::(py) { - Ok(self - .0 - .get_entry_by_path(&relpath) - .map(|entry| entry_to_py(py, entry.clone()).unwrap())) - } else if let Ok(segments) = relpath.extract::>(py) { - let segments = segments.iter().map(|s| s.as_str()).collect::>(); - Ok(self - .0 - .get_entry_by_path_segments(segments.as_slice()) - .map(|entry| entry_to_py(py, entry.clone()).unwrap())) - } else { - Err(PyTypeError::new_err("expected str or list of str")) - } - } - - #[pyo3(signature = (delta))] - fn apply_delta( - &mut self, - py: Python, - delta: Vec<( - Option, - Option, - FileId, - Option>, - )>, - ) -> PyResult<()> { - let delta = bazaar::inventory_delta::InventoryDelta::from_iter(delta.into_iter().map( - |(old_name, new_name, file_id, entry)| InventoryDeltaEntry { - old_path: old_name, - new_path: new_name, - file_id, - new_entry: entry.map(|entry| entry.0.clone()), - }, - )); - self.0 - .apply_delta(&delta) - .map_err(|e| delta_err_to_py_err(py, e)) - } - - #[pyo3(signature = (delta, new_revision_id))] - fn create_by_apply_delta( - &self, - py: Python, - delta: Vec<( - Option, - Option, - FileId, - Option>, - )>, - new_revision_id: RevisionId, - ) -> PyResult { - let delta = bazaar::inventory_delta::InventoryDelta::from_iter(delta.into_iter().map( - |(old_name, new_name, file_id, entry)| InventoryDeltaEntry { - old_path: old_name, - new_path: new_name, - file_id, - new_entry: entry.map(|entry| entry.0.clone()), - }, - )); - let result = self - .0 - .create_by_apply_delta(&delta, new_revision_id) - .map_err(|e| delta_err_to_py_err(py, e))?; - Ok(Self(result)) - } - - fn __len__(&self) -> usize { - self.0.len() - } - - fn get_entry<'py>(&self, py: Python<'py>, file_id: FileId) -> PyResult> { - self.0 - .get_entry(&file_id) - .map(|entry| entry_to_py(py, entry.clone()).unwrap()) - .ok_or_else(|| NoSuchId::new_err((py.None(), file_id))) - } - - fn get_file_kind(&self, file_id: FileId) -> Option<&str> { - self.0.get_file_kind(&file_id).map(|kind| kind.to_string()) - } - - fn has_id(&self, file_id: FileId) -> bool { - self.0.has_id(&file_id) - } - - fn get_child<'py>( - &self, - py: Python<'py>, - file_id: FileId, - name: &str, - ) -> Option> { - self.0 - .get_child(&file_id, name) - .map(|entry| entry_to_py(py, entry.clone()).unwrap()) - } - - fn delete(&mut self, py: Python, file_id: FileId) -> PyResult<()> { - self.0 - .delete(&file_id) - .map_err(|e| inventory_err_to_py_err(e, py)) - } - - fn _make_delta<'py>( - &self, - py: Python<'py>, - old: &Inventory, - ) -> PyResult> { - let inventory_delta = self.0.make_delta(&old.0); - Bound::new(py, InventoryDelta(inventory_delta)) - } - - fn remove_recursive_id<'a>( - &mut self, - py: Python<'a>, - file_id: FileId, - ) -> PyResult>> { - self.0 - .remove_recursive_id(&file_id) - .into_iter() - .map(|entry| entry_to_py(py, entry)) - .collect::>>() - } - - fn rename( - &mut self, - py: Python, - file_id: FileId, - new_parent_id: FileId, - new_name: &str, - ) -> PyResult<()> { - self.0 - .rename(&file_id, &new_parent_id, new_name) - .map_err(|e| inventory_err_to_py_err(e, py)) - } - - fn iter_sorted_children<'a>( - &self, - py: Python<'a>, - file_id: FileId, - ) -> PyResult>> { - let children = self.0.iter_sorted_children(&file_id); - if children.is_none() { - return Err(NoSuchId::new_err((py.None(), file_id))); - } - children - .unwrap() - .map(|(_n, e)| Ok(entry_to_py(py, e.clone())?.into_any())) - .collect::>>() - } - - fn iter_all_ids<'a>(&self, py: Python<'a>) -> PyResult> { - let ids = self.0.iter_all_ids(); - ids.into_iter() - .collect::>() - .into_pyobject(py)? - .call_method0("__iter__") - } - - #[pyo3(signature = (from_dir=None, recursive=true))] - fn iter_entries( - slf: Py, - py: Python, - from_dir: Option, - recursive: Option, - ) -> PyResult> { - let recursive = recursive.unwrap_or(true); - - Bound::new(py, IterEntriesIterator::new(py, slf, from_dir, recursive)?) - } - - #[pyo3(signature = (from_dir=None, specific_file_ids=None))] - fn iter_entries_by_dir( - slf: Py, - py: Python, - from_dir: Option, - specific_file_ids: Option>, - ) -> PyResult> { - Bound::new( - py, - IterEntriesByDirIterator::new(py, slf, from_dir, specific_file_ids)?, - ) - } - - fn change_root_id(&mut self, new_root_id: FileId) -> PyResult<()> { - self.0.change_root_id(new_root_id); - Ok(()) - } - - fn copy(&self) -> Self { - Self(self.0.clone()) - } - - #[pyo3(signature = (kind, name, parent_id=None, file_id=None, revision=None, text_sha1=None, text_size=None, text_id=None, executable=None, symlink_target=None, reference_revision=None))] - #[allow(clippy::too_many_arguments)] - fn make_entry<'a>( - &self, - py: Python<'a>, - kind: &str, - name: &str, - parent_id: Option, - file_id: Option, - revision: Option, - text_sha1: Option>, - text_size: Option, - text_id: Option>, - executable: Option, - symlink_target: Option, - reference_revision: Option, - ) -> PyResult> { - let kind = match kind { - "directory" => Kind::Directory, - "file" => Kind::File, - "symlink" => Kind::Symlink, - "tree-reference" => Kind::TreeReference, - _ => return Err(PyValueError::new_err(format!("Unknown kind: {}", kind))), - }; - let entry = bazaar::inventory::make_entry( - kind, - name.to_string(), - parent_id, - file_id, - revision, - text_sha1, - text_size, - executable, - text_id, - symlink_target, - reference_revision, - ) - .map_err(|e| inventory_err_to_py_err(e, py))?; - entry_to_py(py, entry) - } - - pub fn __richcmp__(&self, other: PyRef, op: CompareOp) -> PyResult { - match op { - CompareOp::Eq => Ok(self.0 == other.0), - CompareOp::Ne => Ok(self.0 != other.0), - _ => Err(PyNotImplementedError::new_err( - "Only == and != are implemented", - )), - } - } -} - -#[pyclass] -struct IterEntriesByDirIterator { - inv: Py, - parents: Option>, - stack: Vec<(String, FileId)>, - children: VecDeque<(String, Entry)>, - specific_file_ids: Option>, -} - -impl IterEntriesByDirIterator { - fn new( - py: Python, - inv: Py, - from_dir: Option, - specific_file_ids: Option>, - ) -> PyResult { - let parents = specific_file_ids.as_ref().map(|specific_file_ids| { - bazaar::inventory::find_interesting_parents( - &inv.borrow(py).0, - &specific_file_ids.iter().collect(), - ) - .into_iter() - .cloned() - .collect() - }); - - let mut stack: Vec<(String, FileId)> = vec![]; - let from_dir = if let Some(from_dir) = from_dir { - let inv = &inv.borrow(py).0; - let e = inv.get_entry(&from_dir); - - if e.is_none() { - return Err(NoSuchId::new_err((py.None(), from_dir))); - } - - let e = e.unwrap(); - - if e.kind() != Kind::Directory { - return Err(pyo3::exceptions::PyNotADirectoryError::new_err( - format!("{:?} is not a directory", from_dir), - )); - } - Some(from_dir) - } else { - inv.borrow(py).0.root().map(|e| e.file_id().clone()) - }; - - let mut children = VecDeque::new(); - - if let Some(from_dir) = from_dir.as_ref() { - assert!( - inv.borrow(py).0.get_children(from_dir).is_some(), - "from_dir {:?} must be a directory", - from_dir - ); - stack.push(("".to_string(), from_dir.clone())); - if specific_file_ids.is_none() || specific_file_ids.as_ref().unwrap().contains(from_dir) - { - children.push_front(( - "".to_string(), - inv.borrow(py).0.get_entry(from_dir).unwrap().clone(), - )); - } - } - - Ok(Self { - inv, - parents, - children, - stack, - specific_file_ids, - }) - } -} - -#[pymethods] -impl IterEntriesByDirIterator { - fn __iter__(slf: PyRef) -> PyResult> { - Ok(slf.into()) - } - - fn __next__<'py>(&mut self, py: Python<'py>) -> PyResult)>> { - loop { - if let Some((relpath, ie)) = self.children.pop_front() { - return Ok(Some((relpath, entry_to_py(py, ie)?))); - } - if let Some((cur_relpath, cur_dir)) = self.stack.pop() { - let mut child_dirs = Vec::new(); - let inv = &self.inv.borrow(py).0; - for (child_name, child_ie) in inv - .iter_sorted_children(&cur_dir) - .expect("should be known directory") - { - let child_relpath = cur_relpath.to_string() + child_name; - - if self.specific_file_ids.is_none() - || self - .specific_file_ids - .as_ref() - .unwrap() - .contains(child_ie.file_id()) - { - self.children - .push_back((child_relpath.clone(), child_ie.clone())); - } - - if child_ie.kind() == Kind::Directory - && (self.parents.is_none() - || self.parents.as_ref().unwrap().contains(child_ie.file_id())) - { - assert!(self - .inv - .borrow(py) - .0 - .get_children(child_ie.file_id()) - .is_some()); - child_dirs.push((child_relpath + "/", child_ie.file_id())) - } - } - self.stack - .extend(child_dirs.into_iter().rev().map(|(n, f)| (n, f.clone()))); - } else { - return Ok(None); - } - } - } -} - -#[pyclass] -struct IterEntriesIterator { - inv: Py, - stack: VecDeque<(String, VecDeque<(String, Entry)>)>, - recursive: bool, - first_entry: Option, -} - -impl IterEntriesIterator { - fn new( - py: Python<'_>, - inv: Py, - mut from_dir: Option, - recursive: bool, - ) -> PyResult { - let mut stack = VecDeque::new(); - - let first_entry = if from_dir.is_none() { - from_dir = inv.borrow(py).0.root().map(|e| e.file_id().clone()); - inv.borrow(py).0.root().cloned() - } else { - None - }; - - if let Some(from_dir) = from_dir.as_ref() { - let inv = &inv.borrow(py).0; - let children = inv.iter_sorted_children(from_dir); - if children.is_none() { - return Err(NoSuchId::new_err((py.None(), from_dir.clone()))); - } - stack.push_back(( - String::new(), - children - .unwrap() - .map(|(p, ie)| (p.to_string(), ie.clone())) - .collect::>(), - )); - } - - Ok(Self { - inv, - stack, - recursive, - first_entry, - }) - } -} - -#[pymethods] -impl IterEntriesIterator { - fn __iter__(slf: PyRef) -> PyResult> { - Ok(slf.into()) - } - - fn __next__<'py>(&mut self, py: Python<'py>) -> PyResult)>> { - if let Some(first_entry) = self.first_entry.take() { - return Ok(Some((String::new(), entry_to_py(py, first_entry)?))); - } - loop { - if let Some((base, children)) = self.stack.back_mut() { - if let Some((name, ie)) = children.pop_front() { - let path = if base.is_empty() { - name - } else { - format!("{}/{}", base, name) - }; - if ie.kind() == Kind::Directory && self.recursive { - let children = self - .inv - .borrow(py) - .0 - .iter_sorted_children(ie.file_id()) - .unwrap() - .map(|(p, ie)| (p.to_string(), ie.clone())) - .collect::>(); - self.stack.push_back((path.clone(), children)); - } - return Ok(Some((path, entry_to_py(py, ie)?))); - } else { - self.stack.pop_back(); - } - } else { - return Ok(None); - } - } - } -} - -#[pyfunction] -#[pyo3(signature = (lines, allow_versioned_root=None, allow_tree_references=None))] -fn parse_inventory_delta( - py: Python, - lines: Vec>, - allow_versioned_root: Option, - allow_tree_references: Option, -) -> PyResult<( - Bound, - Bound, - bool, - bool, - Bound, -)> { - let (parent, version, versioned_root, tree_references, result) = - bazaar::inventory_delta::parse_inventory_delta( - lines - .iter() - .map(|x| x.as_slice()) - .collect::>() - .as_slice(), - allow_versioned_root, - allow_tree_references, - ) - .map_err(|e| match e { - InventoryDeltaParseError::Invalid(m) => InventoryDeltaError::new_err((m,)), - InventoryDeltaParseError::Incompatible(m) => IncompatibleInventoryDelta::new_err((m,)), - })?; - - let result = Bound::new(py, InventoryDelta(result))?; - - Ok(( - parent.into_pyobject(py)?, - version.into_pyobject(py)?, - versioned_root, - tree_references, - result, - )) -} - -#[pyfunction(signature = (file_id, name, parent_id, revision, lines))] -fn parse_inventory_entry( - file_id: FileId, - name: String, - parent_id: Option, - revision: Option, - lines: &[u8], -) -> InventoryEntry { - InventoryEntry(bazaar::inventory_delta::parse_inventory_entry( - file_id, name, parent_id, revision, lines, - )) -} - -#[pyfunction] -fn serialize_inventory_entry<'a>( - py: Python<'a>, - entry: &'a InventoryEntry, -) -> PyResult> { - Ok(PyBytes::new( - py, - bazaar::inventory_delta::serialize_inventory_entry(&entry.0) - .map_err(|e| match e { - InventoryDeltaSerializeError::Invalid(m) => InventoryDeltaError::new_err((m,)), - InventoryDeltaSerializeError::UnsupportedKind(k) => PyKeyError::new_err((k,)), - })? - .as_slice(), - )) -} - -#[pyfunction] -fn serialize_inventory_delta<'a>( - py: Python<'a>, - old_name: RevisionId, - new_name: RevisionId, - delta_to_new: &'a InventoryDelta, - versioned_root: bool, - tree_references: bool, -) -> PyResult>> { - Ok(bazaar::inventory_delta::serialize_inventory_delta( - &old_name, - &new_name, - &delta_to_new.0, - versioned_root, - tree_references, - ) - .map_err(|e| match e { - InventoryDeltaSerializeError::Invalid(m) => InventoryDeltaError::new_err((m,)), - InventoryDeltaSerializeError::UnsupportedKind(m) => PyKeyError::new_err((m,)), - })? - .into_iter() - .map(|x| PyBytes::new(py, x.as_slice())) - .collect()) -} - -#[pyfunction] -fn chk_inventory_entry_to_bytes<'a>( - py: Python<'a>, - entry: &'a InventoryEntry, -) -> PyResult> { - Ok(PyBytes::new( - py, - bazaar::chk_inventory::chk_inventory_entry_to_bytes(&entry.0).as_slice(), - )) -} - -#[pyfunction] -pub fn chk_inventory_bytes_to_entry<'py>( - py: Python<'py>, - data: &[u8], -) -> PyResult> { - entry_to_py( - py, - bazaar::chk_inventory::chk_inventory_bytes_to_entry(data), - ) -} - -#[pyfunction] -fn chk_inventory_bytes_to_utf8name_key<'py>( - py: Python<'py>, - data: &[u8], -) -> PyResult<(Bound<'py, PyBytes>, FileId, RevisionId)> { - let (name, file_id, revision_id) = - bazaar::chk_inventory::chk_inventory_bytes_to_utf8_name_key(data); - - Ok((PyBytes::new(py, name), file_id, revision_id)) -} - -pub fn _inventory_rs(py: Python) -> PyResult> { - let m = PyModule::new(py, "inventory")?; - - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_wrapped(wrap_pyfunction!(make_entry))?; - m.add_wrapped(wrap_pyfunction!(is_valid_name))?; - m.add_wrapped(wrap_pyfunction!(ensure_normalized_name))?; - m.add_class::()?; - - m.add_class::()?; - m.add_wrapped(wrap_pyfunction!(parse_inventory_delta))?; - m.add_wrapped(wrap_pyfunction!(parse_inventory_entry))?; - m.add_wrapped(wrap_pyfunction!(serialize_inventory_delta))?; - m.add_wrapped(wrap_pyfunction!(serialize_inventory_entry))?; - m.add("InventoryDeltaError", py.get_type::())?; - m.add( - "IncompatibleInventoryDelta", - py.get_type::(), - )?; - m.add_wrapped(wrap_pyfunction!(chk_inventory_entry_to_bytes))?; - m.add_wrapped(wrap_pyfunction!(chk_inventory_bytes_to_entry))?; - m.add_wrapped(wrap_pyfunction!(chk_inventory_bytes_to_utf8name_key))?; - - Ok(m) -} diff --git a/crates/bazaar-py/src/lib.rs b/crates/bazaar-py/src/lib.rs deleted file mode 100644 index 9ca38412b5..0000000000 --- a/crates/bazaar-py/src/lib.rs +++ /dev/null @@ -1,555 +0,0 @@ -use bazaar::RevisionId; -use chrono::NaiveDateTime; -use pyo3::class::basic::CompareOp; -use pyo3::exceptions::{PyNotImplementedError, PyRuntimeError, PyTypeError, PyValueError}; -use pyo3::import_exception; -use pyo3::prelude::*; -use pyo3::types::{PyBytes, PyString}; -use pyo3_filelike::PyBinaryFile; -use std::collections::HashMap; - -mod chk_map; -mod dirstate; -mod groupcompress; -mod inventory; -mod smart; -mod versionedfile; - -import_exception!(breezy.errors, ReservedId); - -/// Create a new file id suffix that is reasonably unique. -/// -/// On the first call we combine the current time with 64 bits of randomness to -/// give a highly probably globally unique number. Then each call in the same -/// process adds 1 to a serial number we append to that unique value. -#[pyfunction] -#[pyo3(signature = (suffix = None))] -fn _next_id_suffix<'py>(py: Python<'py>, suffix: Option<&str>) -> Bound<'py, PyBytes> { - PyBytes::new(py, bazaar::gen_ids::next_id_suffix(suffix).as_slice()) -} - -/// Return new file id for the basename 'name'. -/// -/// The uniqueness is supplied from _next_id_suffix. -#[pyfunction] -fn gen_file_id(name: &str) -> bazaar::FileId { - bazaar::FileId::generate(name) -} - -/// Return a new tree-root file id. -#[pyfunction] -fn gen_root_id() -> bazaar::FileId { - bazaar::FileId::generate_root_id() -} - -/// Return new revision-id. -/// -/// Args: -/// username: The username of the committer, in the format returned by -/// config.username(). This is typically a real name, followed by an -/// email address. If found, we will use just the email address portion. -/// Otherwise we flatten the real name, and use that. -/// Returns: A new revision id. -#[pyfunction] -#[pyo3(signature = (username, timestamp = None))] -fn gen_revision_id( - py: Python, - username: &str, - timestamp: Option>, -) -> PyResult { - let timestamp = match timestamp { - Some(timestamp) => { - if let Ok(timestamp) = timestamp.extract::(py) { - Some(timestamp as u64) - } else if let Ok(timestamp) = timestamp.extract::(py) { - Some(timestamp) - } else { - return Err(PyTypeError::new_err( - "timestamp must be a float or an int".to_string(), - )); - } - } - None => None, - }; - Ok(bazaar::RevisionId::generate(username, timestamp)) -} - -#[pyfunction] -fn normalize_pattern(pattern: &str) -> String { - bazaar::globbing::normalize_pattern(pattern) -} - -#[pyclass] -struct Replacer { - replacer: bazaar::globbing::Replacer, -} - -#[pymethods] -impl Replacer { - #[new] - #[pyo3(signature = (source = None))] - fn new(source: Option<&Self>) -> Self { - Self { - replacer: bazaar::globbing::Replacer::new(source.map(|p| &p.replacer)), - } - } - - /// Add a pattern and replacement. - /// - /// The pattern must not contain capturing groups. - /// The replacement might be either a string template in which \& will be - /// replaced with the match, or a function that will get the matching text - /// as argument. It does not get match object, because capturing is - /// forbidden anyway. - fn add(&mut self, py: Python, pattern: &str, func: Py) -> PyResult<()> { - if let Ok(func) = func.extract::(py) { - self.replacer - .add(pattern, bazaar::globbing::Replacement::String(func)); - Ok(()) - } else { - let callable = Box::new(move |t: String| -> String { - Python::attach(|py| match func.call1(py, (t,)) { - Ok(result) => result.extract::(py).unwrap(), - Err(e) => { - e.restore(py); - String::new() - } - }) - }); - self.replacer - .add(pattern, bazaar::globbing::Replacement::Closure(callable)); - Ok(()) - } - } - - /// Add all patterns from another replacer. - /// - /// All patterns and replacements from replacer are appended to the ones - /// already defined. - fn add_replacer(&mut self, replacer: &Self) { - self.replacer.add_replacer(&replacer.replacer) - } - - fn __call__(&mut self, py: Python, text: &str) -> PyResult { - let ret = self - .replacer - .replace(text) - .map_err(|e| PyValueError::new_err(e.to_string()))?; - if PyErr::occurred(py) { - Err(PyErr::fetch(py)) - } else { - Ok(ret) - } - } -} - -#[pyclass(subclass)] -struct Revision(bazaar::revision::Revision); - -/// Single revision on a branch. -/// -/// Revisions may know their revision_hash, but only once they've been -/// written out. This is not stored because you cannot write the hash -/// into the file it describes. -/// -/// Attributes: -/// parent_ids: List of parent revision_ids -/// -/// properties: -/// Dictionary of revision properties. These are attached to the -/// revision as extra metadata. The name must be a single -/// word; the value can be an arbitrary string. -#[pymethods] -impl Revision { - #[new] - #[pyo3(signature = (revision_id, parent_ids, committer, message, properties, inventory_sha1, timestamp, timezone))] - fn new( - py: Python, - revision_id: RevisionId, - parent_ids: Vec, - committer: Option, - message: String, - properties: Option>>, - inventory_sha1: Option>, - timestamp: f64, - timezone: Option, - ) -> PyResult { - let mut cproperties: HashMap> = HashMap::new(); - for (k, v) in properties.unwrap_or_default() { - if let Ok(s) = v.extract::>(py) { - cproperties.insert(k, s.as_bytes().to_vec()); - } else if let Ok(s) = v.extract::>(py) { - let s = s - .call_method1("encode", ("utf-8", "surrogateescape"))? - .extract::>()?; - cproperties.insert(k, s.as_bytes().to_vec()); - } else { - return Err(PyTypeError::new_err( - "properties must be a dictionary of strings", - )); - } - } - - if !bazaar::revision::validate_properties(&cproperties) { - return Err(PyValueError::new_err( - "properties must be a dictionary of strings", - )); - } - Ok(Self(bazaar::revision::Revision { - revision_id, - parent_ids, - committer, - message, - properties: cproperties, - inventory_sha1, - timestamp, - timezone, - })) - } - - fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult { - match op { - CompareOp::Eq => Ok(self.0 == other.0), - CompareOp::Ne => Ok(self.0 != other.0), - _ => Err(PyNotImplementedError::new_err( - "only == and != are supported", - )), - } - } - - fn __repr__(self_: PyRef) -> String { - format!("", self_.0.revision_id) - } - - #[getter] - fn revision_id(&self) -> &bazaar::RevisionId { - &self.0.revision_id - } - - #[getter] - fn parent_ids(&self) -> &Vec { - &self.0.parent_ids - } - - #[getter] - fn committer(&self) -> Option { - self.0.committer.clone() - } - - #[getter] - fn message(&self) -> String { - self.0.message.clone() - } - - #[getter] - fn properties(&self) -> HashMap { - self.0 - .properties - .iter() - .map(|(k, v)| (k.clone(), String::from_utf8_lossy(v).into())) - .collect() - } - - #[getter] - fn get_inventory_sha1<'py>(&self, py: Python<'py>) -> Bound<'py, PyAny> { - if let Some(sha1) = &self.0.inventory_sha1 { - PyBytes::new(py, sha1).into_any() - } else { - py.None().into_bound(py) - } - } - - #[setter] - fn set_inventory_sha1(&mut self, py: Python, value: Py) -> PyResult<()> { - if let Ok(value) = value.extract::>(py) { - self.0.inventory_sha1 = Some(value.as_bytes().to_vec()); - Ok(()) - } else if value.is_none(py) { - self.0.inventory_sha1 = None; - Ok(()) - } else { - Err(PyTypeError::new_err("expected bytes or None")) - } - } - - #[getter] - fn timestamp(&self) -> f64 { - self.0.timestamp - } - - #[getter] - fn timezone(&self) -> Option { - self.0.timezone - } - - fn datetime(&self) -> NaiveDateTime { - self.0.datetime() - } - - fn check_properties(&self) -> PyResult<()> { - if self.0.check_properties() { - Ok(()) - } else { - Err(PyValueError::new_err("invalid properties")) - } - } - - fn get_summary(&self) -> String { - self.0.get_summary() - } - - fn get_apparent_authors(&self) -> Vec { - self.0.get_apparent_authors() - } - - fn bug_urls(&self) -> Vec { - self.0.bug_urls() - } -} - -fn serializer_err_to_py_err(e: bazaar::serializer::Error) -> PyErr { - PyRuntimeError::new_err(format!("serializer error: {:?}", e)) -} - -#[pyclass(subclass)] -struct RevisionSerializer(Box); - -#[pyclass(subclass,extends=RevisionSerializer)] -struct BEncodeRevisionSerializerv1; - -#[pymethods] -impl BEncodeRevisionSerializerv1 { - #[new] - fn new() -> (Self, RevisionSerializer) { - ( - Self {}, - RevisionSerializer(Box::new( - bazaar::bencode_serializer::BEncodeRevisionSerializer1, - )), - ) - } -} - -#[pyclass(subclass,extends=RevisionSerializer)] -struct XMLRevisionSerializer8; - -#[pymethods] -impl XMLRevisionSerializer8 { - #[new] - fn new() -> (Self, RevisionSerializer) { - ( - Self {}, - RevisionSerializer(Box::new(bazaar::xml_serializer::XMLRevisionSerializer8)), - ) - } -} - -#[pyclass(subclass,extends=RevisionSerializer)] -struct XMLRevisionSerializer5; - -#[pymethods] -impl XMLRevisionSerializer5 { - #[new] - fn new() -> (Self, RevisionSerializer) { - ( - Self {}, - RevisionSerializer(Box::new(bazaar::xml_serializer::XMLRevisionSerializer5)), - ) - } -} - -#[pymethods] -impl RevisionSerializer { - #[getter] - fn format_name(&self) -> String { - self.0.format_name().to_string() - } - - #[getter] - fn squashes_xml_invalid_characters(&self) -> bool { - self.0.squashes_xml_invalid_characters() - } - - fn read_revision(&self, py: Python, file: Py) -> PyResult { - py.detach(|| { - let mut file = PyBinaryFile::from(file); - Ok(Revision( - self.0 - .read_revision(&mut file) - .map_err(serializer_err_to_py_err)?, - )) - }) - } - - fn write_revision_to_string<'py>( - &self, - py: Python<'py>, - revision: &Revision, - ) -> PyResult> { - Ok(PyBytes::new( - py, - py.detach(|| self.0.write_revision_to_string(&revision.0)) - .map_err(serializer_err_to_py_err)? - .as_slice(), - )) - } - - fn write_revision_to_lines<'a>( - &self, - py: Python<'a>, - revision: &Revision, - ) -> PyResult>> { - self.0 - .write_revision_to_lines(&revision.0) - .map(|s| -> PyResult> { - Ok(PyBytes::new( - py, - s.map_err(serializer_err_to_py_err)?.as_slice(), - )) - }) - .collect::>>>() - } - - fn read_revision_from_string(&self, py: Python, string: &[u8]) -> PyResult { - Ok(Revision( - py.detach(|| self.0.read_revision_from_string(string)) - .map_err(serializer_err_to_py_err)?, - )) - } -} - -#[pyfunction(name = "is_null")] -fn is_null_revision(revision_id: RevisionId) -> bool { - revision_id.is_null() -} - -#[pyfunction(name = "is_reserved_id")] -fn is_reserved_revision_id(revision_id: RevisionId) -> bool { - revision_id.is_reserved() -} - -#[pyfunction(name = "check_not_reserved_id")] -fn check_not_reserved_id(_py: Python, revision_id: Bound) -> PyResult<()> { - if revision_id.is_none() { - return Ok(()); - } - if let Ok(revision_id) = revision_id.extract::() { - if revision_id.is_reserved() { - Err(ReservedId::new_err((revision_id,))) - } else { - Ok(()) - } - } else { - // For now, just ignore other types.. - Ok(()) - } -} - -#[pyfunction] -#[pyo3(signature = (message = None))] -fn escape_invalid_chars(message: Option<&str>) -> (Option, usize) { - if let Some(message) = message { - ( - Some(bazaar::xml_serializer::escape_invalid_chars(message)), - message.len(), - ) - } else { - (None, 0) - } -} - -#[pyfunction] -fn encode_and_escape(py: Python, unicode_or_utf8_str: Py) -> PyResult> { - let ret = if let Ok(text) = unicode_or_utf8_str.extract::(py) { - bazaar::xml_serializer::encode_and_escape_string(&text) - } else if let Ok(bytes) = unicode_or_utf8_str.extract::>(py) { - bazaar::xml_serializer::encode_and_escape_bytes(&bytes) - } else { - return Err(PyTypeError::new_err("expected str or bytes")); - }; - - Ok(PyBytes::new(py, ret.as_bytes())) -} - -mod hashcache; -mod rio; - -#[pymodule] -fn _bzr_rs(py: Python, m: &Bound) -> PyResult<()> { - m.add_wrapped(wrap_pyfunction!(_next_id_suffix))?; - m.add_wrapped(wrap_pyfunction!(gen_file_id))?; - m.add_wrapped(wrap_pyfunction!(gen_root_id))?; - m.add_wrapped(wrap_pyfunction!(gen_revision_id))?; - let m_globbing = PyModule::new(py, "globbing")?; - m_globbing.add_wrapped(wrap_pyfunction!(normalize_pattern))?; - m_globbing.add_class::()?; - m.add_submodule(&m_globbing)?; - m.add_class::()?; - let inventorym = inventory::_inventory_rs(py)?; - m.add_submodule(&inventorym)?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add( - "revision_bencode_serializer", - m.getattr("BEncodeRevisionSerializerv1")?.call0()?, - )?; - m.add( - "revision_serializer_v8", - m.getattr("XMLRevisionSerializer8")?.call0()?, - )?; - m.add( - "revision_serializer_v5", - m.getattr("XMLRevisionSerializer5")?.call0()?, - )?; - m.add("CURRENT_REVISION", bazaar::CURRENT_REVISION)?; - m.add("NULL_REVISION", bazaar::NULL_REVISION)?; - m.add("ROOT_ID", bazaar::inventory::ROOT_ID)?; - m.add_wrapped(wrap_pyfunction!(is_null_revision))?; - m.add_wrapped(wrap_pyfunction!(is_reserved_revision_id))?; - m.add_wrapped(wrap_pyfunction!(check_not_reserved_id))?; - m.add_wrapped(wrap_pyfunction!(escape_invalid_chars))?; - m.add_wrapped(wrap_pyfunction!(encode_and_escape))?; - - let riom = PyModule::new(py, "rio")?; - rio::rio(&riom)?; - m.add_submodule(&riom)?; - - let hashcachem = PyModule::new(py, "hashcache")?; - hashcache::hashcache(&hashcachem)?; - m.add_submodule(&hashcachem)?; - - let dirstatem = dirstate::_dirstate_rs(py)?; - m.add_submodule(&dirstatem)?; - - let groupcompressm = groupcompress::_groupcompress_rs(py)?; - m.add_submodule(&groupcompressm)?; - - let chk_mapm = chk_map::_chk_map_rs(py)?; - m.add_submodule(&chk_mapm)?; - - let smartm = smart::_smart_rs(py)?; - m.add_submodule(&smartm)?; - - let versionedfilem = versionedfile::_versionedfile_rs(py)?; - m.add_submodule(&versionedfilem)?; - - // PyO3 submodule hack for proper import support - let sys = py.import("sys")?; - let modules = sys.getattr("modules")?; - let module_name = m.name()?; - - // Register submodules in sys.modules for dotted import support - modules.set_item(format!("{}.globbing", module_name), &m_globbing)?; - modules.set_item(format!("{}.inventory", module_name), &inventorym)?; - modules.set_item(format!("{}.rio", module_name), &riom)?; - modules.set_item(format!("{}.hashcache", module_name), &hashcachem)?; - modules.set_item(format!("{}.dirstate", module_name), &dirstatem)?; - modules.set_item(format!("{}.groupcompress", module_name), &groupcompressm)?; - modules.set_item(format!("{}.chk_map", module_name), &chk_mapm)?; - modules.set_item(format!("{}.smart", module_name), &smartm)?; - modules.set_item(format!("{}.versionedfile", module_name), &versionedfilem)?; - - Ok(()) -} diff --git a/crates/bazaar-py/src/rio.rs b/crates/bazaar-py/src/rio.rs deleted file mode 100644 index c78bc1719d..0000000000 --- a/crates/bazaar-py/src/rio.rs +++ /dev/null @@ -1,366 +0,0 @@ -use pyo3::prelude::*; - -use pyo3::wrap_pyfunction; - -use pyo3::exceptions::{PyIOError, PyNotImplementedError, PyTypeError, PyValueError}; -use pyo3::types::{PyBytes, PyDict, PyIterator, PyList, PyString, PyType}; - -use pyo3::class::basic::CompareOp; - -use std::io::BufReader; - -use pyo3_filelike::PyBinaryFile; - -#[pyfunction] -fn valid_tag(tag: &str) -> bool { - bazaar::rio::valid_tag(tag) -} - -#[pyclass(from_py_object)] -#[derive(Clone, PartialEq)] -struct Stanza { - stanza: bazaar::rio::Stanza, -} - -#[pymethods] -impl Stanza { - #[new] - #[pyo3(signature = (**kwargs))] - fn new(kwargs: Option<&Bound>) -> PyResult { - let mut obj = Stanza { - stanza: bazaar::rio::Stanza::new(), - }; - - if let Some(kwargs) = kwargs { - let items = kwargs.items(); - items.sort()?; - for item in items.iter() { - let (key, value) = item.extract::<(String, Bound)>()?; - obj.add(&key.to_string(), &value)?; - } - } - - Ok(obj) - } - - fn __richcmp__(&self, other: &Bound, op: CompareOp) -> PyResult { - match op { - CompareOp::Eq => { - let other_stanza = other.extract::(); - if other_stanza.is_err() { - Ok(false) - } else { - Ok(self.stanza.eq(&other_stanza.unwrap().stanza)) - } - } - _ => Err(PyErr::new::("Not implemented")), - } - } - - fn __repr__(&self) -> PyResult { - Ok(format!("{:?}", self.stanza)) - } - - fn get<'py>(&self, tag: &str, py: Python<'py>) -> PyResult>> { - if let Some(value) = self.stanza.get(tag) { - match value { - bazaar::rio::StanzaValue::String(v) => Ok(Some(PyString::new(py, v).into_any())), - bazaar::rio::StanzaValue::Stanza(v) => Ok(Some( - Bound::new(py, Stanza { stanza: *v.clone() })?.into_any(), - )), - } - } else { - Ok(None) - } - } - - /// Returns true if the stanza contains the given tag. - fn __contains__(&self, tag: &str) -> PyResult { - Ok(self.stanza.contains(tag)) - } - - fn __len__(&self) -> PyResult { - Ok(self.stanza.len()) - } - - fn to_bytes<'a>(&self, py: Python<'a>) -> PyResult> { - let ret: Bound = PyBytes::new(py, self.stanza.to_bytes().as_slice()); - Ok(ret) - } - - fn to_string<'a>(&self, py: Python<'a>) -> PyResult> { - self.to_bytes(py) - } - - fn to_lines(&self, py: Python) -> PyResult> { - let ret = PyList::empty(py); - for line in self.stanza.to_lines() { - ret.append(PyBytes::new(py, line.as_bytes()))?; - } - Ok(ret.into()) - } - - /// Add a tag and value to the stanza. - fn add(&mut self, tag: &str, value: &Bound) -> PyResult<()> { - if !valid_tag(tag) { - return Err(PyErr::new::("Invalid tag")); - } - // If the type of value is PyString, then extract it as a String and add it to the stanza. - // Otherwise, if the type of value is Stanza, then extract it as a Stanza and add it to the stanza. - // Otherwise, return an error. - let ret = if let Ok(val) = value.extract::() { - self.stanza - .add(tag.to_string(), bazaar::rio::StanzaValue::String(val)) - } else if let Ok(val) = value.extract::() { - self.stanza.add( - tag.to_string(), - bazaar::rio::StanzaValue::Stanza(Box::new(val.stanza)), - ) - } else { - return Err(PyErr::new::(format!( - "Invalid value: {}", - value.repr()? - ))); - }; - if let Err(e) = ret { - if let bazaar::rio::Error::Io(e) = e { - return Err(PyErr::new::(format!("IO error: {}", e))); - } else { - return Err(PyErr::new::(format!( - "Invalid value: {}", - value.repr()? - ))); - } - } - Ok(()) - } - - /// Create a stanza from a list of pairs. - #[classmethod] - fn from_pairs(_cls: &Bound, pairs: Vec<(String, Bound)>) -> PyResult { - let mut ret = Stanza::new(None)?; - for (tag, value) in pairs { - ret.add(tag.as_str(), &value)?; - } - Ok(ret) - } - - // TODO: This is a hack to get around the fact that PyO3 doesn't support returning an iterator. - fn iter_pairs<'a>(&self, py: Python<'a>) -> PyResult> { - let ret = PyList::empty(py); - for (tag, value) in self.stanza.iter_pairs() { - match value { - bazaar::rio::StanzaValue::String(v) => { - ret.append((tag.to_string(), v.to_string()))? - } - bazaar::rio::StanzaValue::Stanza(v) => { - let sub: Stanza = Stanza { stanza: *v.clone() }; - ret.append((tag.to_string(), sub))?; - } - } - } - PyIterator::from_object(&ret) - } - - fn as_dict(&self, py: Python) -> PyResult> { - let ret = PyDict::new(py); - for (tag, value) in self.stanza.iter_pairs() { - match value { - bazaar::rio::StanzaValue::String(v) => ret.set_item(tag, v.to_string())?, - bazaar::rio::StanzaValue::Stanza(v) => { - let sub: Stanza = Stanza { stanza: *v.clone() }; - ret.set_item(tag, sub)?; - } - } - } - Ok(ret.into()) - } - - fn get_all(&self, tag: &str, py: Python) -> PyResult> { - let ret = PyList::empty(py); - for value in self.stanza.get_all(tag) { - match value { - bazaar::rio::StanzaValue::String(v) => ret.append(v.to_string())?, - bazaar::rio::StanzaValue::Stanza(v) => { - let sub: Stanza = Stanza { stanza: *v.clone() }; - ret.append(sub.into_pyobject(py)?)?; - } - } - } - Ok(ret.into()) - } - - fn write(&self, file: Py) -> PyResult<()> { - let mut writer = PyBinaryFile::from(file); - self.stanza.write(&mut writer)?; - Ok(()) - } -} - -#[pyclass] -struct RioWriter { - writer: bazaar::rio::RioWriter, -} - -#[pymethods] -impl RioWriter { - #[new] - fn new(file: Py) -> PyResult { - let fw = PyBinaryFile::from(file); - let writer = bazaar::rio::RioWriter::new(fw); - Ok(RioWriter { writer }) - } - - fn write_stanza(&mut self, stanza: &Stanza) -> PyResult<()> { - self.writer.write_stanza(&stanza.stanza)?; - Ok(()) - } -} - -#[pyfunction] -fn read_stanza_file(file: Py) -> PyResult> { - let reader = PyBinaryFile::from(file); - - let mut reader = BufReader::new(reader); - - let stanza = bazaar::rio::read_stanza_file(&mut reader).map_err(|e| match e { - bazaar::rio::Error::Io(e) => { - PyErr::new::(format!("Error reading stanza file: {}", e)) - } - _ => PyErr::new::("Error reading stanza file".to_string()), - })?; - - if let Some(stanza) = stanza { - Ok(Some(Stanza { stanza })) - } else { - Ok(None) - } -} - -#[pyfunction] -fn read_stanza(file: &Bound) -> PyResult> { - let mut py_iter = file.try_iter()?; - let mut pyerr: Option = None; - let line_iter = std::iter::from_fn(|| -> Option, bazaar::rio::Error>> { - let line = py_iter.next()?; - if let Err(e) = line { - pyerr = Some(e); - Some(Err(bazaar::rio::Error::Other("Python error".to_string()))) - } else { - let line = line.unwrap(); - let line = line.extract::>(); - if let Err(e) = line { - pyerr = Some(e); - Some(Err(bazaar::rio::Error::Other("invalid input".to_string()))) - } else { - Some(Ok(line.unwrap())) - } - } - }); - - let stanza = bazaar::rio::read_stanza(line_iter).map_err(|e| { - if let Some(e) = pyerr { - return e; - } - match e { - bazaar::rio::Error::Io(e) => { - PyErr::new::(format!("Error reading stanza: {}", e)) - } - _ => PyErr::new::("Error reading stanza".to_string()), - } - })?; - - if let Some(stanza) = stanza { - Ok(Some(Stanza { stanza })) - } else { - Ok(None) - } -} - -#[pyfunction] -fn read_stanzas(file: Py) -> PyResult> { - Python::attach(|py| { - let reader = PyBinaryFile::from(file); - let ret = PyList::empty(py); - - let mut reader = BufReader::new(reader); - - let stanzas = bazaar::rio::read_stanzas(&mut reader).map_err(|e| match e { - bazaar::rio::Error::Io(e) => { - PyErr::new::(format!("Error reading stanza file: {}", e)) - } - _ => PyErr::new::("Error reading stanza file: ".to_string()), - })?; - for stanza in stanzas { - ret.append(Stanza { stanza })?; - } - Ok(ret.into()) - }) -} - -#[pyclass] -struct RioReader { - reader: bazaar::rio::RioReader>, -} - -#[pymethods] -impl RioReader { - #[new] - fn new(file: Py) -> PyResult { - let reader = PyBinaryFile::from(file); - let reader = BufReader::new(reader); - let reader = bazaar::rio::RioReader::new(reader); - - Ok(RioReader { reader }) - } - - fn __iter__<'a>(&mut self, py: Python<'a>) -> PyResult> { - let ret = PyList::empty(py); - for stanza in self.reader.iter() { - let stanza = stanza.map_err(|e| match e { - bazaar::rio::Error::Io(e) => { - PyErr::new::(format!("Error reading stanza file: {}", e)) - } - _ => PyErr::new::("Error reading stanza file: ".to_string()), - })?; - ret.append(Stanza { - stanza: stanza.unwrap(), - })?; - } - PyIterator::from_object(&ret) - } -} - -#[pyfunction] -#[pyo3(signature = (stanzas, header = None))] -fn rio_iter<'a>( - py: Python<'a>, - stanzas: &'a Bound<'a, PyAny>, - header: Option>, -) -> PyResult> { - let ret = PyList::empty(py); - let pyiter = stanzas.try_iter()?; - let mut stanzas = Vec::new(); - for stanza in pyiter { - let stanza = stanza?; - stanzas.push(stanza.extract::()?.stanza); - } - for line in bazaar::rio::rio_iter(stanzas.into_iter(), header) { - let line = line.as_slice(); - ret.append(PyBytes::new(py, line))?; - } - PyIterator::from_object(&ret) -} - -pub(crate) fn rio(m: &Bound) -> PyResult<()> { - m.add_wrapped(wrap_pyfunction!(valid_tag))?; - m.add_wrapped(wrap_pyfunction!(read_stanza))?; - m.add_wrapped(wrap_pyfunction!(read_stanza_file))?; - m.add_wrapped(wrap_pyfunction!(read_stanzas))?; - m.add_wrapped(wrap_pyfunction!(rio_iter))?; - - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - Ok(()) -} diff --git a/crates/bazaar-py/src/smart.rs b/crates/bazaar-py/src/smart.rs deleted file mode 100644 index e18d4418aa..0000000000 --- a/crates/bazaar-py/src/smart.rs +++ /dev/null @@ -1,29 +0,0 @@ -use bazaar::smart::protocol::{ - MESSAGE_VERSION_THREE, REQUEST_VERSION_THREE, REQUEST_VERSION_TWO, RESPONSE_VERSION_THREE, - RESPONSE_VERSION_TWO, -}; -use pyo3::prelude::*; -use pyo3::types::PyBytes; - -pub(crate) fn _smart_rs(py: Python) -> PyResult> { - let m = PyModule::new(py, "smart")?; - m.add("REQUEST_VERSION_TWO", PyBytes::new(py, REQUEST_VERSION_TWO))?; - m.add( - "REQUEST_VERSION_THREE", - PyBytes::new(py, REQUEST_VERSION_THREE), - )?; - m.add( - "RESPONSE_VERSION_TWO", - PyBytes::new(py, RESPONSE_VERSION_TWO), - )?; - m.add( - "RESPONSE_VERSION_THREE", - PyBytes::new(py, RESPONSE_VERSION_THREE), - )?; - m.add( - "MESSAGE_VERSION_THREE", - PyBytes::new(py, MESSAGE_VERSION_THREE), - )?; - - Ok(m) -} diff --git a/crates/bazaar-py/src/versionedfile.rs b/crates/bazaar-py/src/versionedfile.rs deleted file mode 100644 index 15ec8ccab9..0000000000 --- a/crates/bazaar-py/src/versionedfile.rs +++ /dev/null @@ -1,161 +0,0 @@ -use bazaar::versionedfile::{ContentFactory, Key}; -use pyo3::prelude::*; -use pyo3::types::PyBytes; - -#[pyclass(subclass)] -struct AbstractContentFactory(Box); - -pyo3::import_exception!(breezy.bzr.versionedfile, UnavailableRepresentation); - -#[pymethods] -impl AbstractContentFactory { - #[getter] - fn sha1(&self, py: Python) -> Option> { - self.0.sha1().map(|x| PyBytes::new(py, &x).into()) - } - - #[getter] - fn key(&self) -> Key { - self.0.key() - } - - #[getter] - fn parents(&self) -> Option> { - self.0.parents() - } - - #[getter] - fn storage_kind(&self) -> String { - self.0.storage_kind() - } - - #[getter] - fn size(&self) -> Option { - self.0.size() - } - - fn get_bytes_as(&self, py: Python, storage_kind: &str) -> PyResult> { - if self.0.storage_kind() == "absent" { - return Err(UnavailableRepresentation::new_err( - "Absent content has no bytes".to_string(), - )); - } - match storage_kind { - "fulltext" => Ok(PyBytes::new(py, self.0.to_fulltext().as_ref()).into()), - "lines" => Ok(self - .0 - .to_lines() - .map(|b| PyBytes::new(py, b.as_ref())) - .map(|b| b.unbind().into()) - .collect::>>() - .into_pyobject(py)? - .unbind()), - "chunked" => Ok(self - .0 - .to_chunks() - .map(|b| PyBytes::new(py, b.as_ref())) - .map(|b| b.unbind().into()) - .collect::>>() - .into_pyobject(py)? - .unbind()), - _ => Err(UnavailableRepresentation::new_err(format!( - "Unsupported storage kind: {}", - storage_kind - ))), - } - } - - fn map_key(&mut self, py: Python, cb: Py) -> PyResult<()> { - self.0 - .map_key(&|k| cb.call1(py, (k,)).unwrap().extract::(py).unwrap()); - Ok(()) - } -} - -#[pyclass(extends=AbstractContentFactory)] -struct FulltextContentFactory; - -#[pymethods] -impl FulltextContentFactory { - #[new] - #[pyo3(signature = (key, parents, sha1, text))] - fn new( - key: Key, - parents: Option>, - sha1: Option>, - text: Vec, - ) -> PyResult<(Self, AbstractContentFactory)> { - let of = bazaar::versionedfile::FulltextContentFactory::new(sha1, key, parents, text); - - Ok((FulltextContentFactory, AbstractContentFactory(Box::new(of)))) - } -} - -#[pyclass(extends=AbstractContentFactory)] -struct ChunkedContentFactory; - -#[pymethods] -impl ChunkedContentFactory { - #[new] - #[pyo3(signature = (key, parents, sha1, chunks))] - fn new( - key: Key, - parents: Option>, - sha1: Option>, - chunks: Vec>, - ) -> PyResult<(Self, AbstractContentFactory)> { - let of = bazaar::versionedfile::ChunkedContentFactory::new(sha1, key, parents, chunks); - - Ok((ChunkedContentFactory, AbstractContentFactory(Box::new(of)))) - } -} - -#[pyfunction] -pub fn record_to_fulltext_bytes(py: Python, record: Py) -> PyResult> { - let record = record.extract::(py)?; - - let mut s = Vec::new(); - - bazaar::versionedfile::record_to_fulltext_bytes(record, &mut s)?; - - Ok(PyBytes::new(py, &s).into()) -} - -#[pyclass(extends=AbstractContentFactory)] -struct AbsentContentFactory; - -#[pymethods] -impl AbsentContentFactory { - #[new] - fn new(key: Key) -> PyResult<(Self, AbstractContentFactory)> { - let of = bazaar::versionedfile::AbsentContentFactory::new(key); - - Ok((AbsentContentFactory, AbstractContentFactory(Box::new(of)))) - } -} - -#[pyfunction] -fn fulltext_network_to_record<'a>( - py: Python<'a>, - _kind: &'a str, - bytes: &'a [u8], - line_end: usize, -) -> Vec> { - let record = bazaar::versionedfile::fulltext_network_to_record(bytes, line_end); - - let sub = PyClassInitializer::from(AbstractContentFactory(Box::new(record))) - .add_subclass(FulltextContentFactory); - - vec![Bound::new(py, sub).unwrap()] -} - -pub(crate) fn _versionedfile_rs(py: Python) -> PyResult> { - let m = PyModule::new(py, "versionedfile")?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_function(wrap_pyfunction!(record_to_fulltext_bytes, &m)?)?; - m.add_function(wrap_pyfunction!(fulltext_network_to_record, &m)?)?; - Ok(m) -} diff --git a/setup.py b/setup.py index 5713441d57..04034dbec1 100755 --- a/setup.py +++ b/setup.py @@ -260,9 +260,6 @@ def add_cython_extension(module_name, libraries=None, extra_source=None): RustExtension( "breezy.zlib_util", "crates/zlib-util-py/Cargo.toml", binding=Binding.PyO3 ), - RustExtension( - "breezy._bzr_rs", "crates/bazaar-py/Cargo.toml", binding=Binding.PyO3 - ), RustExtension("breezy._git_rs", "crates/git-py/Cargo.toml", binding=Binding.PyO3), ] From 6aed8b318df22c8c23354ef2f4cd67268145c995 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Wed, 13 May 2026 11:35:34 +0100 Subject: [PATCH 2/8] Adapt to dromedary HttpTransport._post(relpath, body) signature --- breezy/bzr/smart/http.py | 6 ++++-- breezy/tests/test_http.py | 6 +++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/breezy/bzr/smart/http.py b/breezy/bzr/smart/http.py index 33a2b2e055..b1b9ee7411 100644 --- a/breezy/bzr/smart/http.py +++ b/breezy/bzr/smart/http.py @@ -58,14 +58,16 @@ def remote_path_from_transport(self, transport): def send_http_smart_request(self, bytes): """POST ``bytes`` as a smart request body and return the response body.""" + from io import BytesIO + try: t = self._http_transport_ref() - code, body_filelike = t._post(bytes) + code, body = t._post(".bzr/smart", bytes) if code != 200: raise UnexpectedHttpStatus(t._remote_path(".bzr/smart"), code) except (InvalidHttpResponse, ConnectionResetError) as e: raise SmartProtocolError(str(e)) from e - return body_filelike + return BytesIO(body) def _report_activity(self, bytes, direction): # Does nothing; the underlying plain HTTP transport will report the diff --git a/breezy/tests/test_http.py b/breezy/tests/test_http.py index 1d00b1f7eb..3071e4cf4a 100644 --- a/breezy/tests/test_http.py +++ b/breezy/tests/test_http.py @@ -445,7 +445,7 @@ def test_post_body_is_received(self): url = server.get_url() # FIXME: needs a cleanup -- vila 20100611 http_transport = transport.get_transport_from_url(url) - _code, _response = http_transport._post(b"abc def end-of-body") + _code, _response = http_transport._post(".bzr/smart", b"abc def end-of-body") self.assertTrue(server.received_bytes.startswith(b"POST /.bzr/smart HTTP/1.")) self.assertIn(b"content-length: 19\r", server.received_bytes.lower()) self.assertIn( @@ -2098,8 +2098,8 @@ def test_post(self): t = self.get_transport() # We must send a single line of body bytes, see # PredefinedRequestHandler._handle_one_request - _code, f = t._post(b"abc def end-of-body\n") - self.assertEqual(b"lalala whatever as long as itsssss\n", f.read()) + _code, body = t._post(".bzr/smart", b"abc def end-of-body\n") + self.assertEqual(b"lalala whatever as long as itsssss\n", body) self.assertActivitiesMatch() From 3d06616c67e8c3e23c16245ea0096266597c4de2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Wed, 13 May 2026 14:51:13 +0100 Subject: [PATCH 3/8] Convert TestPlainKnitContent / TestAnnotatedKnitContent fixtures to bytes --- breezy/bzr/tests/test_knit.py | 66 ++++++++++++++++++----------------- 1 file changed, 34 insertions(+), 32 deletions(-) diff --git a/breezy/bzr/tests/test_knit.py b/breezy/bzr/tests/test_knit.py index b8f7f35bd3..0c5d563faf 100644 --- a/breezy/bzr/tests/test_knit.py +++ b/breezy/bzr/tests/test_knit.py @@ -119,13 +119,13 @@ def assertDerivedBlocksEqual(self, source, target, noeol=False): target_lines = target.splitlines(True) def nl(line): - if noeol and not line.endswith("\n"): - return line + "\n" + if noeol and not line.endswith(b"\n"): + return line + b"\n" else: return line - source_content = self._make_content([(None, nl(l)) for l in source_lines]) - target_content = self._make_content([(None, nl(l)) for l in target_lines]) + source_content = self._make_content([(b"", nl(l)) for l in source_lines]) + target_content = self._make_content([(b"", nl(l)) for l in target_lines]) line_delta = source_content.line_delta(target_content) delta_blocks = list( KnitContent.get_line_delta_blocks(line_delta, source_lines, target_lines) @@ -135,16 +135,16 @@ def nl(line): self.assertEqual(matcher_blocks, delta_blocks) def test_get_line_delta_blocks(self): - self.assertDerivedBlocksEqual("a\nb\nc\n", "q\nc\n") + self.assertDerivedBlocksEqual(b"a\nb\nc\n", b"q\nc\n") self.assertDerivedBlocksEqual(TEXT_1, TEXT_1) self.assertDerivedBlocksEqual(TEXT_1, TEXT_1A) self.assertDerivedBlocksEqual(TEXT_1, TEXT_1B) self.assertDerivedBlocksEqual(TEXT_1B, TEXT_1A) self.assertDerivedBlocksEqual(TEXT_1A, TEXT_1B) - self.assertDerivedBlocksEqual(TEXT_1A, "") - self.assertDerivedBlocksEqual("", TEXT_1A) - self.assertDerivedBlocksEqual("", "") - self.assertDerivedBlocksEqual("a\nb\nc", "a\nb\nc\nd") + self.assertDerivedBlocksEqual(TEXT_1A, b"") + self.assertDerivedBlocksEqual(b"", TEXT_1A) + self.assertDerivedBlocksEqual(b"", b"") + self.assertDerivedBlocksEqual(b"a\nb\nc", b"a\nb\nc\nd") def test_get_line_delta_blocks_noeol(self): """Handle historical knit deltas safely. @@ -155,13 +155,13 @@ def test_get_line_delta_blocks_noeol(self): New knit deltas appear to always consider the last line to differ in this case. """ - self.assertDerivedBlocksEqual("a\nb\nc", "a\nb\nc\nd\n", noeol=True) - self.assertDerivedBlocksEqual("a\nb\nc\nd\n", "a\nb\nc", noeol=True) - self.assertDerivedBlocksEqual("a\nb\nc\n", "a\nb\nc", noeol=True) - self.assertDerivedBlocksEqual("a\nb\nc", "a\nb\nc\n", noeol=True) + self.assertDerivedBlocksEqual(b"a\nb\nc", b"a\nb\nc\nd\n", noeol=True) + self.assertDerivedBlocksEqual(b"a\nb\nc\nd\n", b"a\nb\nc", noeol=True) + self.assertDerivedBlocksEqual(b"a\nb\nc\n", b"a\nb\nc", noeol=True) + self.assertDerivedBlocksEqual(b"a\nb\nc", b"a\nb\nc\n", noeol=True) -TEXT_1 = """\ +TEXT_1 = b"""\ Banana cup cakes: - bananas @@ -169,7 +169,7 @@ def test_get_line_delta_blocks_noeol(self): - broken tea cups """ -TEXT_1A = """\ +TEXT_1A = b"""\ Banana cup cake recipe (serves 6) @@ -179,7 +179,7 @@ def test_get_line_delta_blocks_noeol(self): - self-raising flour """ -TEXT_1B = """\ +TEXT_1B = b"""\ Banana cup cake recipe - bananas (do not use plantains!!!) @@ -195,7 +195,7 @@ def test_get_line_delta_blocks_noeol(self): - self-raising flour """ -TEXT_2 = """\ +TEXT_2 = b"""\ Boeuf bourguignon - beef @@ -209,25 +209,27 @@ def test_get_line_delta_blocks_noeol(self): class TestPlainKnitContent(TestCase, KnitContentTestsMixin): def _make_content(self, lines): annotated_content = AnnotatedKnitContent(lines) - return PlainKnitContent(annotated_content.text(), "bogus") + return PlainKnitContent(annotated_content.text(), b"bogus") def test_annotate(self): content = self._make_content([]) self.assertEqual(content.annotate(), []) - content = self._make_content([("origin1", "text1"), ("origin2", "text2")]) - self.assertEqual(content.annotate(), [("bogus", "text1"), ("bogus", "text2")]) + content = self._make_content([(b"origin1", b"text1"), (b"origin2", b"text2")]) + self.assertEqual( + content.annotate(), [(b"bogus", b"text1"), (b"bogus", b"text2")] + ) def test_line_delta(self): - content1 = self._make_content([("", "a"), ("", "b")]) - content2 = self._make_content([("", "a"), ("", "a"), ("", "c")]) - self.assertEqual(content1.line_delta(content2), [(1, 2, 2, ["a", "c"])]) + content1 = self._make_content([(b"", b"a"), (b"", b"b")]) + content2 = self._make_content([(b"", b"a"), (b"", b"a"), (b"", b"c")]) + self.assertEqual(content1.line_delta(content2), [(1, 2, 2, [b"a", b"c"])]) def test_line_delta_iter(self): - content1 = self._make_content([("", "a"), ("", "b")]) - content2 = self._make_content([("", "a"), ("", "a"), ("", "c")]) + content1 = self._make_content([(b"", b"a"), (b"", b"b")]) + content2 = self._make_content([(b"", b"a"), (b"", b"a"), (b"", b"c")]) it = content1.line_delta_iter(content2) - self.assertEqual(next(it), (1, 2, 2, ["a", "c"])) + self.assertEqual(next(it), (1, 2, 2, [b"a", b"c"])) self.assertRaises(StopIteration, next, it) @@ -245,17 +247,17 @@ def test_annotate(self): ) def test_line_delta(self): - content1 = self._make_content([("", "a"), ("", "b")]) - content2 = self._make_content([("", "a"), ("", "a"), ("", "c")]) + content1 = self._make_content([(b"", b"a"), (b"", b"b")]) + content2 = self._make_content([(b"", b"a"), (b"", b"a"), (b"", b"c")]) self.assertEqual( - content1.line_delta(content2), [(1, 2, 2, [("", "a"), ("", "c")])] + content1.line_delta(content2), [(1, 2, 2, [(b"", b"a"), (b"", b"c")])] ) def test_line_delta_iter(self): - content1 = self._make_content([("", "a"), ("", "b")]) - content2 = self._make_content([("", "a"), ("", "a"), ("", "c")]) + content1 = self._make_content([(b"", b"a"), (b"", b"b")]) + content2 = self._make_content([(b"", b"a"), (b"", b"a"), (b"", b"c")]) it = content1.line_delta_iter(content2) - self.assertEqual(next(it), (1, 2, 2, [("", "a"), ("", "c")])) + self.assertEqual(next(it), (1, 2, 2, [(b"", b"a"), (b"", b"c")])) self.assertRaises(StopIteration, next, it) From ee1c3f7ae9b06ef728972467c7839ce31577bacf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Wed, 13 May 2026 17:48:30 +0100 Subject: [PATCH 4/8] Add get_bytes and append_bytes to MockTransport --- breezy/bzr/tests/test_knit.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/breezy/bzr/tests/test_knit.py b/breezy/bzr/tests/test_knit.py index 0c5d563faf..60f28d48a2 100644 --- a/breezy/bzr/tests/test_knit.py +++ b/breezy/bzr/tests/test_knit.py @@ -274,6 +274,21 @@ def get(self, filename): else: return BytesIO(b"\n".join(self.file_lines)) + def get_bytes(self, filename): + if self.file_lines is None: + raise NoSuchFile(filename) + return b"\n".join(self.file_lines) + + def append_bytes(self, filename, bytes_): + self.calls.append(("append_bytes", (filename, bytes_), {})) + if self.file_lines is None: + self.file_lines = [] + offset = 0 + else: + offset = sum(len(line) + 1 for line in self.file_lines) + self.file_lines.extend(bytes_.rstrip(b"\n").split(b"\n")) + return offset + def readv(self, relpath, offsets): fp = self.get(relpath) for offset, size in offsets: From 90104d12cbeb0347d9d734e68d1eadc9342a20f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Fri, 15 May 2026 12:43:21 +0100 Subject: [PATCH 5/8] fix tests --- breezy/bzr/groupcompress_repo.py | 2 +- breezy/bzr/pack_repo.py | 16 ++++++++-------- breezy/bzr/tests/per_pack_repository.py | 22 +++++++++++----------- breezy/bzr/vf_repository.py | 2 +- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/breezy/bzr/groupcompress_repo.py b/breezy/bzr/groupcompress_repo.py index d6471b90ec..2f6842a098 100644 --- a/breezy/bzr/groupcompress_repo.py +++ b/breezy/bzr/groupcompress_repo.py @@ -819,7 +819,7 @@ def _check_new_inventories(self): # entries too. # And all this should be independent of any fallback repository. problems = [] - key_deps = self.repo.revisions._index._key_dependencies + key_deps = self.repo.revisions._index.key_dependencies new_revisions_keys = key_deps.get_new_keys() no_fallback_inv_index = self.repo.inventories._index no_fallback_chk_bytes_index = self.repo.chk_bytes._index diff --git a/breezy/bzr/pack_repo.py b/breezy/bzr/pack_repo.py index c076f67f23..ac715f1981 100644 --- a/breezy/bzr/pack_repo.py +++ b/breezy/bzr/pack_repo.py @@ -1192,13 +1192,13 @@ def _start_write_group(self): ) if self.chk_index is not None: self.chk_index.add_writable_index(self._new_pack.chk_index, self._new_pack) - self.repo.chk_bytes._index._add_callback = self.chk_index.add_callback + self.repo.chk_bytes._index.set_add_callback(self.chk_index.add_callback) self._new_pack.chk_index.set_optimize(combine_backing_indices=False) - self.repo.inventories._index._add_callback = self.inventory_index.add_callback - self.repo.revisions._index._add_callback = self.revision_index.add_callback - self.repo.signatures._index._add_callback = self.signature_index.add_callback - self.repo.texts._index._add_callback = self.text_index.add_callback + self.repo.inventories._index.set_add_callback(self.inventory_index.add_callback) + self.repo.revisions._index.set_add_callback(self.revision_index.add_callback) + self.repo.signatures._index.set_add_callback(self.signature_index.add_callback) + self.repo.texts._index.set_add_callback(self.text_index.add_callback) def _abort_write_group(self): # FIXME: just drop the transient index. @@ -1379,7 +1379,7 @@ def _all_revision_ids(self): return [key[0] for key in self.revisions.keys()] def _abort_write_group(self): - self.revisions._index._key_dependencies.clear() + self.revisions._index.clear_key_dependencies() self._pack_collection._abort_write_group() def _make_parents_provider(self): @@ -1401,7 +1401,7 @@ def _start_write_group(self): def _commit_write_group(self): hint = self._pack_collection._commit_write_group() - self.revisions._index._key_dependencies.clear() + self.revisions._index.clear_key_dependencies() # The commit may have added keys that were previously cached as # missing, so reset the cache. self._unstacked_provider.disable_cache() @@ -1416,7 +1416,7 @@ def suspend_write_group(self): """ # XXX check self._write_group is self.get_transaction()? tokens = self._pack_collection._suspend_write_group() - self.revisions._index._key_dependencies.clear() + self.revisions._index.clear_key_dependencies() self._write_group = None return tokens diff --git a/breezy/bzr/tests/per_pack_repository.py b/breezy/bzr/tests/per_pack_repository.py index 81bbf48171..dfe3d4fc88 100644 --- a/breezy/bzr/tests/per_pack_repository.py +++ b/breezy/bzr/tests/per_pack_repository.py @@ -694,29 +694,29 @@ def make_write_ready_repo(self): def test_missing_inventories_compression_parent_prevents_commit(self): repo = self.make_write_ready_repo() - key = ("junk",) - repo.inventories._index._missing_compression_parents.add(key) + key = (b"junk",) + repo.inventories._index.add_missing_compression_parent(key) self.assertRaises(BzrCheckError, repo.commit_write_group) self.assertRaises(BzrCheckError, repo.commit_write_group) def test_missing_revisions_compression_parent_prevents_commit(self): repo = self.make_write_ready_repo() - key = ("junk",) - repo.revisions._index._missing_compression_parents.add(key) + key = (b"junk",) + repo.revisions._index.add_missing_compression_parent(key) self.assertRaises(BzrCheckError, repo.commit_write_group) self.assertRaises(BzrCheckError, repo.commit_write_group) def test_missing_signatures_compression_parent_prevents_commit(self): repo = self.make_write_ready_repo() - key = ("junk",) - repo.signatures._index._missing_compression_parents.add(key) + key = (b"junk",) + repo.signatures._index.add_missing_compression_parent(key) self.assertRaises(BzrCheckError, repo.commit_write_group) self.assertRaises(BzrCheckError, repo.commit_write_group) def test_missing_text_compression_parent_prevents_commit(self): repo = self.make_write_ready_repo() - key = ("some", "junk") - repo.texts._index._missing_compression_parents.add(key) + key = (b"some", b"junk") + repo.texts._index.add_missing_compression_parent(key) self.assertRaises(BzrCheckError, repo.commit_write_group) self.assertRaises(BzrCheckError, repo.commit_write_group) @@ -1054,7 +1054,7 @@ def test_key_dependencies_cleared_on_abort(self): [(b"B-id",)], "unordered", True ) target_repo.revisions.insert_record_stream(stream) - key_refs = target_repo.revisions._index._key_dependencies + key_refs = target_repo.revisions._index.key_dependencies self.assertEqual([(b"B-id",)], sorted(key_refs.get_referrers())) finally: target_repo.abort_write_group() @@ -1068,7 +1068,7 @@ def test_key_dependencies_cleared_on_suspend(self): [(b"B-id",)], "unordered", True ) target_repo.revisions.insert_record_stream(stream) - key_refs = target_repo.revisions._index._key_dependencies + key_refs = target_repo.revisions._index.key_dependencies self.assertEqual([(b"B-id",)], sorted(key_refs.get_referrers())) finally: target_repo.suspend_write_group() @@ -1094,7 +1094,7 @@ def test_key_dependencies_cleared_on_commit(self): [(b"B-id",)], "unordered", True ) target_repo.revisions.insert_record_stream(stream) - key_refs = target_repo.revisions._index._key_dependencies + key_refs = target_repo.revisions._index.key_dependencies self.assertEqual([(b"B-id",)], sorted(key_refs.get_referrers())) finally: target_repo.commit_write_group() diff --git a/breezy/bzr/vf_repository.py b/breezy/bzr/vf_repository.py index df341a0924..9308bf2b5a 100644 --- a/breezy/bzr/vf_repository.py +++ b/breezy/bzr/vf_repository.py @@ -1244,7 +1244,7 @@ def get_missing_parent_inventories(self, check_for_missing_texts=True): # XXX: Texts referenced by all added inventories need to be present, # but at the moment we're only checking for texts referenced by # inventories at the graph's edge. - key_deps = self.revisions._index._key_dependencies + key_deps = self.revisions._index.key_dependencies key_deps.satisfy_refs_for_keys(present_inventories) referrers = frozenset(r[0] for r in key_deps.get_referrers()) file_ids = self.fileids_altered_by_revision_ids(referrers) From d0d9460c31d62228024f5d2a79e01e9165a33dd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Fri, 22 May 2026 03:09:37 +0100 Subject: [PATCH 6/8] Drop stale expectFailure in test_ordered_fulltext_complex --- .../test_get_record_stream.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/breezy/tests/per_repository_reference/test_get_record_stream.py b/breezy/tests/per_repository_reference/test_get_record_stream.py index eb402a9919..bcb071b664 100644 --- a/breezy/tests/per_repository_reference/test_get_record_stream.py +++ b/breezy/tests/per_repository_reference/test_get_record_stream.py @@ -16,8 +16,6 @@ """Tests that get_record_stream() behaves itself properly when stacked.""" -from bzrformats import knit - from breezy.tests.per_repository_reference import ( TestCaseWithExternalReferenceRepository, ) @@ -239,14 +237,9 @@ def test_ordered_fulltext_complex(self): if record.storage_kind == "absent": raise ValueError(f"absent record: {record.key}") record_keys.append(record.key) - # Note that currently --2a format repositories do this correctly, but - # KnitPack format repositories do not. - if isinstance(self.stacked_repo.texts, knit.KnitVersionedFiles): - # See https://bugs.launchpad.net/bzr/+bug/399884 - self.expectFailure( - "KVF does not weave fulltexts from fallback" - " repositories to preserve perfect order", - self.assertTrue, - record_keys in (keys, alt_1, alt_2, alt_3, alt_4), - ) + # KnitVersionedFiles used to weave the local keys ahead of the + # fallback ones, breaking topological order across the stacking + # boundary (https://bugs.launchpad.net/bzr/+bug/399884). It now + # emits keys grouped by source in topological order, so the order + # is preserved for both --2a and KnitPack repositories. self.assertIn(record_keys, (keys, alt_1, alt_2, alt_3, alt_4)) From 8121740ba65cd58283d61c4aed6d0286c332cb24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Fri, 22 May 2026 14:03:08 +0100 Subject: [PATCH 7/8] Make fetch streams reconstructable by the target --- breezy/bzr/fetch.py | 6 ++++++ breezy/bzr/groupcompress_repo.py | 7 ++++++- breezy/bzr/knitpack_repo.py | 7 ++++++- breezy/bzr/remote.py | 6 ++++++ breezy/bzr/vf_repository.py | 19 ++++++++++++++++--- breezy/tests/test_fetch.py | 4 +++- 6 files changed, 43 insertions(+), 6 deletions(-) diff --git a/breezy/bzr/fetch.py b/breezy/bzr/fetch.py index a811fb4b9c..de0a1c1cc7 100644 --- a/breezy/bzr/fetch.py +++ b/breezy/bzr/fetch.py @@ -123,6 +123,12 @@ def _fetch_everything_for_search(self, search): with ui.ui_factory.nested_progress_bar() as pb: pb.update("Get stream source") source = self.from_repository._get_source(self.to_repository._format) + # A stacked target cannot store a text delta whose basis lives + # only in a fallback (ResumedPack._check_references rejects it). + # Ask the source for self-contained texts so the stream never + # carries such deltas. + if self.to_repository._fallback_repositories: + source._stream_self_contained_texts = True stream = source.get_stream(search) from_format = self.from_repository._format pb.update("Inserting stream") diff --git a/breezy/bzr/groupcompress_repo.py b/breezy/bzr/groupcompress_repo.py index 2f6842a098..422d17cc6e 100644 --- a/breezy/bzr/groupcompress_repo.py +++ b/breezy/bzr/groupcompress_repo.py @@ -1411,8 +1411,13 @@ def _get_parent_id_basename_to_file_id_pages(): def _get_text_stream(self): # Note: We know we don't have to handle adding root keys, because both # the source and target are the identical network name. + # A stacked target cannot store a delta whose basis lives only in a + # fallback, so emit delta closures when asked for a self-contained + # stream. text_stream = self.from_repository.texts.get_record_stream( - self._text_keys, self._text_fetch_order, False + self._text_keys, + self._text_fetch_order, + self._stream_self_contained_texts, ) return ("texts", text_stream) diff --git a/breezy/bzr/knitpack_repo.py b/breezy/bzr/knitpack_repo.py index 15ed9b4974..702322d689 100644 --- a/breezy/bzr/knitpack_repo.py +++ b/breezy/bzr/knitpack_repo.py @@ -655,8 +655,13 @@ def _filtered_inv_stream(): def _get_text_stream(self): # Note: We know we don't have to handle adding root keys, because both # the source and target are the identical network name. + # A stacked target cannot store a text delta whose basis lives only + # in a fallback, so emit delta closures when asked for a + # self-contained stream. text_stream = self.from_repository.texts.get_record_stream( - self._text_keys, self._text_fetch_order, False + self._text_keys, + self._text_fetch_order, + self._stream_self_contained_texts, ) return ("texts", text_stream) diff --git a/breezy/bzr/remote.py b/breezy/bzr/remote.py index 89e650cefe..c3ca4ac222 100644 --- a/breezy/bzr/remote.py +++ b/breezy/bzr/remote.py @@ -4457,6 +4457,11 @@ def get_stream(self, search): and self.to_format._fetch_order == "topological" ): return self._real_stream(self.from_repository, search) + if self._stream_self_contained_texts: + # The smart get_stream verb cannot be told to emit delta + # closures, so generate the stream through the VFS source, + # which honours _stream_self_contained_texts. + return self._real_stream(self.from_repository, search) sources = [] seen = set() repos = [self.from_repository] @@ -4541,6 +4546,7 @@ def _real_stream(self, repo, search): if isinstance(source, RemoteStreamSource): repo._ensure_real() source = repo._real_repository._get_source(self.to_format) + source._stream_self_contained_texts = self._stream_self_contained_texts return source.get_stream(search) def _get_stream(self, repo, search): diff --git a/breezy/bzr/vf_repository.py b/breezy/bzr/vf_repository.py index 9308bf2b5a..74fc4fba4e 100644 --- a/breezy/bzr/vf_repository.py +++ b/breezy/bzr/vf_repository.py @@ -2167,6 +2167,11 @@ def __init__(self, from_repository, to_format): """Create a StreamSource streaming from from_repository.""" self.from_repository = from_repository self.to_format = to_format + # When True the text substream is emitted with delta closures, so + # every record can be reconstructed without data outside the stream. + # Set by callers fetching into a stacked repository, which cannot + # store a delta whose basis lives only in a fallback. + self._stream_self_contained_texts = False from bzrformats.recordcounter import RecordCounter self._record_counter = RecordCounter() @@ -2193,10 +2198,17 @@ def _fetch_revision_texts(self, revs): ) # If a revision has a delta, this is actually expanded inside the # insert_record_stream code now, which is an alternate fix for - # bug #261339 + # bug #261339. That expansion reads the delta basis back from the + # target, so the basis must already have been inserted: stream + # revisions topologically rather than unordered so a delta never + # arrives before its basis. from_rf = self.from_repository.revisions + if self.delta_on_metadata(): + revision_order = "topological" + else: + revision_order = self.to_format._fetch_order revisions = from_rf.get_record_stream( - keys, self.to_format._fetch_order, not self.delta_on_metadata() + keys, revision_order, not self.delta_on_metadata() ) return [("signatures", signatures), ("revisions", revisions)] @@ -2241,7 +2253,8 @@ def get_stream(self, search): from_texts.get_record_stream( text_keys, self.to_format._fetch_order, - not self.to_format._fetch_uses_deltas, + not self.to_format._fetch_uses_deltas + or self._stream_self_contained_texts, ), ) # Cause an error if a text occurs after we have done the diff --git a/breezy/tests/test_fetch.py b/breezy/tests/test_fetch.py index feee6240d2..5ddb74d8f4 100644 --- a/breezy/tests/test_fetch.py +++ b/breezy/tests/test_fetch.py @@ -306,8 +306,10 @@ def test_fetch_with_deltas_no_delta_closure(self): ("get_record_stream", [(b"rev-one",)], target._format._fetch_order, False), self.find_get_record_stream(source.inventories.calls, 2), ) + # Revisions are streamed topologically when sent as deltas, so a + # delta never arrives before the basis its expansion needs. self.assertEqual( - ("get_record_stream", [(b"rev-one",)], target._format._fetch_order, False), + ("get_record_stream", [(b"rev-one",)], "topological", False), self.find_get_record_stream(source.revisions.calls), ) # XXX: Signatures is special, and slightly broken. The From 14d68eb92fef7edc0268249ebe9712f3cf99ab94 Mon Sep 17 00:00:00 2001 From: Jelmer Vernooij Date: Fri, 22 May 2026 19:58:24 +0000 Subject: [PATCH 8/8] Fix docs --- breezy/bzr/__init__.py | 2 +- breezy/bzr/smart/protocol.py | 2 +- breezy/globbing.py | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/breezy/bzr/__init__.py b/breezy/bzr/__init__.py index 7f51a2fc3d..5ba6b33c6f 100644 --- a/breezy/bzr/__init__.py +++ b/breezy/bzr/__init__.py @@ -27,12 +27,12 @@ from typing import TYPE_CHECKING +from bzrformats._bzr_rs import hashcache, rio from catalogus import pyutils from dromedary import errors as transport_errors from dromedary.errors import NoSuchFile from .. import config, controldir, errors, registry -from .._bzr_rs import hashcache, rio from ..branch import format_registry as branch_format_registry from ..repository import format_registry as repository_format_registry from ..workingtree import format_registry as workingtree_format_registry diff --git a/breezy/bzr/smart/protocol.py b/breezy/bzr/smart/protocol.py index af633727df..5c329de638 100644 --- a/breezy/bzr/smart/protocol.py +++ b/breezy/bzr/smart/protocol.py @@ -24,13 +24,13 @@ from collections import deque from io import BytesIO +from bzrformats._bzr_rs import smart as _smart_rs from dromedary import errors as transport_errors from fastbencode import bdecode_as_tuple, bencode import breezy from ... import debug, errors, osutils -from ..._bzr_rs import smart as _smart_rs from ...trace import log_exception_quietly, mutter from . import message, request diff --git a/breezy/globbing.py b/breezy/globbing.py index 20dbd79b9c..6cb58f3ca2 100644 --- a/breezy/globbing.py +++ b/breezy/globbing.py @@ -22,8 +22,9 @@ import re +from bzrformats._bzr_rs import globbing as _globbing_rs + from . import lazy_regex -from ._bzr_rs import globbing as _globbing_rs from .trace import mutter, warning Replacer = _globbing_rs.Replacer