use crate::error::Error;
use crate::lock::Lock;
use crate::revisionid::RevisionId;
use pyo3::intern;
use pyo3::prelude::*;
pub type Path = std::path::Path;
pub type PathBuf = std::path::PathBuf;
fn convert_python_stat_to_metadata(
py_stat: &Py<PyAny>,
) -> Result<Option<std::fs::Metadata>, Error> {
Python::attach(|py| {
let stat_obj = py_stat.bind(py);
let _st_mode: u32 = stat_obj.getattr("st_mode")?.extract()?;
let _st_size: u64 = stat_obj.getattr("st_size")?.extract()?;
let _st_mtime: f64 = stat_obj.getattr("st_mtime")?.extract()?;
Ok(None)
})
}
#[derive(Debug)]
pub struct WalkdirResult {
pub relpath: PathBuf,
pub kind: Kind,
pub stat: Option<std::fs::Metadata>,
pub versioned: bool,
}
#[derive(Debug)]
pub struct PathContentSummary {
pub kind: Kind,
pub size: Option<u64>,
pub executable: Option<bool>,
pub sha1: Option<String>,
pub target: Option<String>,
}
#[derive(Debug)]
pub struct SearchRule {
pub pattern: String,
pub rule_type: SearchRuleType,
}
#[derive(Debug)]
pub enum SearchRuleType {
Include,
Exclude,
}
#[derive(Debug)]
pub struct Conflict {
pub path: PathBuf,
pub conflict_type: String,
pub message: Option<String>,
}
impl<'a, 'py> FromPyObject<'a, 'py> for Conflict {
type Error = PyErr;
fn extract(ob: Borrowed<'a, 'py, PyAny>) -> PyResult<Self> {
let path: String = ob.getattr("path")?.extract()?;
let conflict_type: String = ob.getattr("typestring")?.extract()?;
let message: Option<String> = ob.getattr("message").ok().and_then(|m| m.extract().ok());
Ok(Conflict {
path: PathBuf::from(path),
conflict_type,
message,
})
}
}
#[derive(Debug)]
pub struct TreeReference {
pub path: PathBuf,
pub kind: Kind,
pub reference_revision: Option<RevisionId>,
}
#[derive(Debug)]
pub struct InventoryDelta {
pub old_path: Option<PathBuf>,
pub new_path: Option<PathBuf>,
pub file_id: String,
pub entry: Option<TreeEntry>,
}
#[derive(Debug, PartialEq, Clone, Eq)]
pub enum Kind {
File,
Directory,
Symlink,
TreeReference,
}
impl Kind {
pub fn marker(&self) -> &'static str {
match self {
Kind::File => "",
Kind::Directory => "/",
Kind::Symlink => "@",
Kind::TreeReference => "+",
}
}
}
impl std::fmt::Display for Kind {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Kind::File => write!(f, "file"),
Kind::Directory => write!(f, "directory"),
Kind::Symlink => write!(f, "symlink"),
Kind::TreeReference => write!(f, "tree-reference"),
}
}
}
impl std::str::FromStr for Kind {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"file" => Ok(Kind::File),
"directory" => Ok(Kind::Directory),
"symlink" => Ok(Kind::Symlink),
"tree-reference" => Ok(Kind::TreeReference),
n => Err(format!("Invalid kind: {}", n)),
}
}
}
impl<'py> pyo3::IntoPyObject<'py> for Kind {
type Target = pyo3::PyAny;
type Output = pyo3::Bound<'py, Self::Target>;
type Error = std::convert::Infallible;
fn into_pyobject(self, py: pyo3::Python<'py>) -> Result<Self::Output, Self::Error> {
let s = match self {
Kind::File => "file",
Kind::Directory => "directory",
Kind::Symlink => "symlink",
Kind::TreeReference => "tree-reference",
};
Ok(pyo3::types::PyString::new(py, s).into_any())
}
}
impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for Kind {
type Error = PyErr;
fn extract(ob: Borrowed<'a, 'py, pyo3::PyAny>) -> PyResult<Self> {
let s: String = ob.extract()?;
match s.as_str() {
"file" => Ok(Kind::File),
"directory" => Ok(Kind::Directory),
"symlink" => Ok(Kind::Symlink),
"tree-reference" => Ok(Kind::TreeReference),
_ => Err(pyo3::exceptions::PyValueError::new_err(format!(
"Invalid kind: {}",
s
))),
}
}
}
#[derive(Debug)]
pub enum TreeEntry {
File {
executable: bool,
kind: Kind,
revision: Option<RevisionId>,
size: u64,
},
Directory {
revision: Option<RevisionId>,
},
Symlink {
revision: Option<RevisionId>,
symlink_target: String,
},
TreeReference {
revision: Option<RevisionId>,
reference_revision: RevisionId,
},
}
impl<'a, 'py> FromPyObject<'a, 'py> for TreeEntry {
type Error = PyErr;
fn extract(ob: Borrowed<'a, 'py, PyAny>) -> PyResult<Self> {
let kind: String = ob.getattr("kind")?.extract()?;
fn opt_revision(ob: &Borrowed<PyAny>) -> Option<RevisionId> {
ob.getattr("revision")
.ok()
.and_then(|o| o.extract::<Option<RevisionId>>().ok())
.flatten()
}
match kind.as_str() {
"file" => {
let executable: bool = ob.getattr("executable")?.extract()?;
let kind: Kind = ob.getattr("kind")?.extract()?;
let size: u64 = ob
.getattr("size")
.and_then(|o| o.extract::<u64>())
.or_else(|_| ob.getattr("text_size").and_then(|o| o.extract::<u64>()))
.unwrap_or(0);
Ok(TreeEntry::File {
executable,
kind,
size,
revision: opt_revision(&ob),
})
}
"directory" => Ok(TreeEntry::Directory {
revision: opt_revision(&ob),
}),
"symlink" => {
let symlink_target: String = ob.getattr("symlink_target")?.extract()?;
Ok(TreeEntry::Symlink {
revision: opt_revision(&ob),
symlink_target,
})
}
"tree-reference" => {
let reference_revision: RevisionId = ob.getattr("reference_revision")?.extract()?;
Ok(TreeEntry::TreeReference {
revision: opt_revision(&ob),
reference_revision,
})
}
kind => panic!("Invalid kind: {}", kind),
}
}
}
impl<'py> IntoPyObject<'py> for TreeEntry {
type Target = PyAny;
type Output = Bound<'py, Self::Target>;
type Error = std::convert::Infallible;
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
let dict = pyo3::types::PyDict::new(py);
match self {
TreeEntry::File {
executable,
kind: _,
revision,
size,
} => {
dict.set_item("kind", "file").unwrap();
dict.set_item("executable", executable).unwrap();
dict.set_item("size", size).unwrap();
dict.set_item("revision", revision).unwrap();
}
TreeEntry::Directory { revision } => {
dict.set_item("kind", "directory").unwrap();
dict.set_item("revision", revision).unwrap();
}
TreeEntry::Symlink {
revision,
symlink_target,
} => {
dict.set_item("kind", "symlink").unwrap();
dict.set_item("revision", revision).unwrap();
dict.set_item("symlink_target", symlink_target).unwrap();
}
TreeEntry::TreeReference {
revision,
reference_revision,
} => {
dict.set_item("kind", "tree-reference").unwrap();
dict.set_item("revision", revision).unwrap();
dict.set_item("reference_revision", reference_revision)
.unwrap();
}
}
Ok(dict.into_any())
}
}
pub trait Tree {
fn get_tag_dict(&self) -> Result<std::collections::HashMap<String, RevisionId>, Error>;
fn get_file(&self, path: &Path) -> Result<Box<dyn std::io::Read>, Error>;
fn get_file_text(&self, path: &Path) -> Result<Vec<u8>, Error>;
fn get_file_lines(&self, path: &Path) -> Result<Vec<Vec<u8>>, Error>;
fn lock_read(&self) -> Result<Lock, Error>;
fn has_filename(&self, path: &Path) -> bool;
fn get_symlink_target(&self, path: &Path) -> Result<PathBuf, Error>;
fn get_parent_ids(&self) -> Result<Vec<RevisionId>, Error>;
fn is_ignored(&self, path: &Path) -> Option<String>;
fn kind(&self, path: &Path) -> Result<Kind, Error>;
fn is_versioned(&self, path: &Path) -> bool;
fn iter_changes(
&self,
other: &dyn PyTree,
specific_files: Option<&[&Path]>,
want_unversioned: Option<bool>,
require_versioned: Option<bool>,
) -> Result<Box<dyn Iterator<Item = Result<TreeChange, Error>>>, Error>;
fn has_versioned_directories(&self) -> bool;
fn preview_transform(&self) -> Result<crate::transform::TreeTransform, Error>;
fn list_files(
&self,
include_root: Option<bool>,
from_dir: Option<&Path>,
recursive: Option<bool>,
recurse_nested: Option<bool>,
) -> Result<Box<dyn Iterator<Item = Result<(PathBuf, bool, Kind, TreeEntry), Error>>>, Error>;
fn iter_child_entries(
&self,
path: &std::path::Path,
) -> Result<Box<dyn Iterator<Item = Result<(PathBuf, Kind, TreeEntry), Error>>>, Error>;
fn get_file_size(&self, path: &Path) -> Result<u64, Error>;
fn get_file_sha1(
&self,
path: &Path,
stat_value: Option<&std::fs::Metadata>,
) -> Result<String, Error>;
fn get_file_mtime(&self, path: &Path) -> Result<u64, Error>;
fn get_file_revision(&self, path: &Path) -> Result<RevisionId, Error>;
fn is_executable(&self, path: &Path) -> Result<bool, Error>;
fn stored_kind(&self, path: &Path) -> Result<Kind, Error>;
fn supports_content_filtering(&self) -> bool;
fn supports_file_ids(&self) -> bool;
fn supports_rename_tracking(&self) -> bool;
fn supports_symlinks(&self) -> bool;
fn supports_tree_reference(&self) -> bool;
fn unknowns(&self) -> Result<Vec<PathBuf>, Error>;
fn all_versioned_paths(
&self,
) -> Result<Box<dyn Iterator<Item = Result<PathBuf, Error>>>, Error>;
fn conflicts(&self) -> Result<Vec<Conflict>, Error>;
fn extras(&self) -> Result<Vec<PathBuf>, Error>;
fn filter_unversioned_files(&self, paths: &[&Path]) -> Result<Vec<PathBuf>, Error>;
fn walkdirs(
&self,
prefix: Option<&Path>,
) -> Result<Box<dyn Iterator<Item = Result<WalkdirResult, Error>>>, Error>;
fn versionable_kind(&self, kind: &Kind) -> bool;
fn path_content_summary(&self, path: &Path) -> Result<PathContentSummary, Error>;
fn iter_files_bytes(
&self,
paths: &[&Path],
) -> Result<Box<dyn Iterator<Item = Result<(PathBuf, Vec<u8>), Error>>>, Error>;
fn iter_entries_by_dir(
&self,
specific_files: Option<&[&Path]>,
) -> Result<Box<dyn Iterator<Item = Result<(PathBuf, TreeEntry), Error>>>, Error>;
fn get_file_verifier(
&self,
path: &Path,
stat_value: Option<&std::fs::Metadata>,
) -> Result<(String, Vec<u8>), Error>;
fn get_reference_revision(&self, path: &Path) -> Result<RevisionId, Error>;
fn archive(
&self,
format: &str,
name: &str,
root: Option<&str>,
subdir: Option<&Path>,
force_mtime: Option<f64>,
recurse_nested: bool,
) -> Result<Box<dyn Iterator<Item = Result<Vec<u8>, Error>>>, Error>;
fn annotate_iter(
&self,
path: &Path,
default_revision: Option<&RevisionId>,
) -> Result<Box<dyn Iterator<Item = Result<(RevisionId, Vec<u8>), Error>>>, Error>;
fn is_special_path(&self, path: &Path) -> bool;
fn iter_search_rules(
&self,
paths: &[&Path],
) -> Result<Box<dyn Iterator<Item = Result<SearchRule, Error>>>, Error>;
}
pub trait PyTree: Tree + std::any::Any {
fn to_object(&self, py: Python) -> Py<PyAny>;
}
impl dyn PyTree {
pub fn as_tree(&self) -> &dyn Tree {
self
}
}
impl<T: PyTree + ?Sized> Tree for T {
fn get_tag_dict(&self) -> Result<std::collections::HashMap<String, RevisionId>, Error> {
Python::attach(|py| {
let branch = self.to_object(py).getattr(py, "branch")?;
let tags = branch.getattr(py, "tags")?;
let tag_dict = tags.call_method0(py, intern!(py, "get_tag_dict"))?;
tag_dict.extract(py)
})
.map_err(|e: PyErr| -> Error { e.into() })
}
fn get_file(&self, path: &Path) -> Result<Box<dyn std::io::Read>, Error> {
Python::attach(|py| {
let path_str = path.to_string_lossy().to_string();
let f = self
.to_object(py)
.call_method1(py, "get_file", (path_str,))?;
let f = pyo3_filelike::PyBinaryFile::from(f);
Ok(Box::new(f) as Box<dyn std::io::Read>)
})
}
fn get_file_text(&self, path: &Path) -> Result<Vec<u8>, Error> {
Python::attach(|py| {
let path_str = path.to_string_lossy().to_string();
let text = self
.to_object(py)
.call_method1(py, "get_file_text", (path_str,))?;
text.extract(py).map_err(Into::into)
})
}
fn get_file_lines(&self, path: &Path) -> Result<Vec<Vec<u8>>, Error> {
Python::attach(|py| {
let path_str = path.to_string_lossy().to_string();
let lines = self
.to_object(py)
.call_method1(py, "get_file_lines", (path_str,))?;
lines.extract(py).map_err(Into::into)
})
}
fn lock_read(&self) -> Result<Lock, Error> {
Python::attach(|py| {
let lock = self
.to_object(py)
.call_method0(py, intern!(py, "lock_read"))?;
Ok(Lock::from(lock))
})
}
fn has_filename(&self, path: &Path) -> bool {
Python::attach(|py| {
let path_str = path.to_string_lossy().to_string();
self.to_object(py)
.call_method1(py, intern!(py, "has_filename"), (path_str,))
.and_then(|result| result.extract(py))
.unwrap_or(false)
})
}
fn get_symlink_target(&self, path: &Path) -> Result<PathBuf, Error> {
Python::attach(|py| {
let path_str = path.to_string_lossy().to_string();
let target = self
.to_object(py)
.call_method1(py, "get_symlink_target", (path_str,))?;
target.extract(py).map_err(Into::into)
})
}
fn get_parent_ids(&self) -> Result<Vec<RevisionId>, Error> {
Python::attach(|py| {
Ok(self
.to_object(py)
.call_method0(py, intern!(py, "get_parent_ids"))
.unwrap()
.extract(py)?)
})
}
fn is_ignored(&self, path: &Path) -> Option<String> {
Python::attach(|py| {
let path_str = path.to_string_lossy().to_string();
self.to_object(py)
.call_method1(py, "is_ignored", (path_str,))
.unwrap()
.extract(py)
.unwrap()
})
}
fn kind(&self, path: &Path) -> Result<Kind, Error> {
Python::attach(|py| {
let path_str = path.to_string_lossy().to_string();
self.to_object(py)
.call_method1(py, "kind", (path_str,))
.unwrap()
.extract(py)
.map_err(Into::into)
})
}
fn is_versioned(&self, path: &Path) -> bool {
Python::attach(|py| {
let path_str = path.to_string_lossy().to_string();
self.to_object(py)
.call_method1(py, "is_versioned", (path_str,))
.unwrap()
.extract(py)
.unwrap()
})
}
fn iter_changes(
&self,
other: &dyn PyTree,
specific_files: Option<&[&Path]>,
want_unversioned: Option<bool>,
require_versioned: Option<bool>,
) -> Result<Box<dyn Iterator<Item = Result<TreeChange, Error>>>, Error> {
Python::attach(|py| {
let kwargs = pyo3::types::PyDict::new(py);
if let Some(specific_files) = specific_files {
kwargs.set_item(
"specific_files",
specific_files
.iter()
.map(|p| p.to_string_lossy().to_string())
.collect::<Vec<_>>(),
)?;
}
if let Some(want_unversioned) = want_unversioned {
kwargs.set_item("want_unversioned", want_unversioned)?;
}
if let Some(require_versioned) = require_versioned {
kwargs.set_item("require_versioned", require_versioned)?;
}
struct TreeChangeIter(pyo3::Py<PyAny>);
impl Iterator for TreeChangeIter {
type Item = Result<TreeChange, Error>;
fn next(&mut self) -> Option<Self::Item> {
Python::attach(|py| {
let next = match self.0.call_method0(py, intern!(py, "__next__")) {
Ok(v) => v,
Err(e) => {
if e.is_instance_of::<pyo3::exceptions::PyStopIteration>(py) {
return None;
}
return Some(Err(e.into()));
}
};
if next.is_none(py) {
None
} else {
Some(next.extract(py).map_err(Into::into))
}
})
}
}
Ok(Box::new(TreeChangeIter(self.to_object(py).call_method(
py,
"iter_changes",
(other.to_object(py),),
Some(&kwargs),
)?))
as Box<dyn Iterator<Item = Result<TreeChange, Error>>>)
})
}
fn has_versioned_directories(&self) -> bool {
Python::attach(|py| {
self.to_object(py)
.call_method0(py, "has_versioned_directories")
.unwrap()
.extract(py)
.unwrap()
})
}
fn preview_transform(&self) -> Result<crate::transform::TreeTransform, Error> {
Python::attach(|py| {
let transform = self.to_object(py).call_method0(py, "preview_transform")?;
Ok(crate::transform::TreeTransform::from(transform))
})
}
fn list_files(
&self,
include_root: Option<bool>,
from_dir: Option<&Path>,
recursive: Option<bool>,
recurse_nested: Option<bool>,
) -> Result<Box<dyn Iterator<Item = Result<(PathBuf, bool, Kind, TreeEntry), Error>>>, Error>
{
Python::attach(|py| {
let kwargs = pyo3::types::PyDict::new(py);
if let Some(include_root) = include_root {
kwargs.set_item("include_root", include_root)?;
}
if let Some(from_dir) = from_dir {
kwargs.set_item("from_dir", from_dir.to_string_lossy().to_string())?;
}
if let Some(recursive) = recursive {
kwargs.set_item("recursive", recursive)?;
}
if let Some(recurse_nested) = recurse_nested {
kwargs.set_item("recurse_nested", recurse_nested)?;
}
struct ListFilesIter(pyo3::Py<PyAny>);
impl Iterator for ListFilesIter {
type Item = Result<(PathBuf, bool, Kind, TreeEntry), Error>;
fn next(&mut self) -> Option<Self::Item> {
Python::attach(|py| {
let next = match self.0.call_method0(py, intern!(py, "__next__")) {
Ok(v) => v,
Err(e) => {
if e.is_instance_of::<pyo3::exceptions::PyStopIteration>(py) {
return None;
}
return Some(Err(e.into()));
}
};
if next.is_none(py) {
None
} else {
let bound = next.bind(py);
let tuple = match bound.cast::<pyo3::types::PyTuple>() {
Ok(t) => t,
Err(e) => return Some(Err(PyErr::from(e).into())),
};
if tuple.len() != 4 {
return Some(Err(pyo3::exceptions::PyValueError::new_err(
"list_files: expected 4-tuple",
)
.into()));
}
let path: PathBuf =
match tuple.get_item(0).and_then(|o| o.extract::<String>()) {
Ok(s) => PathBuf::from(s),
Err(e) => return Some(Err(e.into())),
};
let flag = match tuple.get_item(1) {
Ok(o) => o,
Err(e) => return Some(Err(e.into())),
};
let versioned: bool = if let Ok(b) = flag.extract::<bool>() {
b
} else if let Ok(s) = flag.extract::<String>() {
s == "V"
} else {
return Some(Err(pyo3::exceptions::PyTypeError::new_err(
"list_files: unexpected type for versioned flag",
)
.into()));
};
let kind: Kind =
match tuple.get_item(2).and_then(|o| o.extract::<Kind>()) {
Ok(k) => k,
Err(e) => return Some(Err(e.into())),
};
let entry: TreeEntry = match tuple
.get_item(3)
.and_then(|o| TreeEntry::extract(o.as_borrowed()))
{
Ok(e) => e,
Err(e) => return Some(Err(e.into())),
};
Some(Ok((path, versioned, kind, entry)))
}
})
}
}
Ok(Box::new(ListFilesIter(self.to_object(py).call_method(
py,
"list_files",
(),
Some(&kwargs),
)?))
as Box<
dyn Iterator<Item = Result<(PathBuf, bool, Kind, TreeEntry), Error>>,
>)
})
.map_err(|e: PyErr| -> Error { e.into() })
}
fn iter_child_entries(
&self,
path: &std::path::Path,
) -> Result<Box<dyn Iterator<Item = Result<(PathBuf, Kind, TreeEntry), Error>>>, Error> {
Python::attach(|py| {
struct IterChildEntriesIter(pyo3::Py<PyAny>);
impl Iterator for IterChildEntriesIter {
type Item = Result<(PathBuf, Kind, TreeEntry), Error>;
fn next(&mut self) -> Option<Self::Item> {
Python::attach(|py| {
let next = match self.0.call_method0(py, intern!(py, "__next__")) {
Ok(v) => v,
Err(e) => {
if e.is_instance_of::<pyo3::exceptions::PyStopIteration>(py) {
return None;
}
return Some(Err(e.into()));
}
};
if next.is_none(py) {
None
} else {
Some(next.extract(py).map_err(Into::into))
}
})
}
}
let path_str = path.to_string_lossy().to_string();
Ok(
Box::new(IterChildEntriesIter(self.to_object(py).call_method1(
py,
"iter_child_entries",
(path_str,),
)?))
as Box<dyn Iterator<Item = Result<(PathBuf, Kind, TreeEntry), Error>>>,
)
})
}
fn get_file_size(&self, path: &Path) -> Result<u64, Error> {
Python::attach(|py| {
let path_str = path.to_string_lossy().to_string();
let size = self
.to_object(py)
.call_method1(py, "get_file_size", (path_str,))?;
size.extract(py).map_err(Into::into)
})
}
fn get_file_sha1(
&self,
path: &Path,
_stat_value: Option<&std::fs::Metadata>,
) -> Result<String, Error> {
Python::attach(|py| {
let path_str = path.to_string_lossy().to_string();
let sha1 = self
.to_object(py)
.call_method1(py, "get_file_sha1", (path_str,))?;
sha1.extract(py).map_err(Into::into)
})
}
fn get_file_mtime(&self, path: &Path) -> Result<u64, Error> {
Python::attach(|py| {
let path_str = path.to_string_lossy().to_string();
let mtime = self
.to_object(py)
.call_method1(py, "get_file_mtime", (path_str,))?;
mtime.extract(py).map_err(Into::into)
})
}
fn get_file_revision(&self, path: &Path) -> Result<RevisionId, Error> {
Python::attach(|py| {
let path_str = path.to_string_lossy().to_string();
let rev = self
.to_object(py)
.call_method1(py, "get_file_revision", (path_str,))?;
rev.extract(py).map_err(Into::into)
})
}
fn is_executable(&self, path: &Path) -> Result<bool, Error> {
Python::attach(|py| {
let path_str = path.to_string_lossy().to_string();
let result = self
.to_object(py)
.call_method1(py, "is_executable", (path_str,))?;
result.extract(py).map_err(Into::into)
})
}
fn stored_kind(&self, path: &Path) -> Result<Kind, Error> {
Python::attach(|py| {
let path_str = path.to_string_lossy().to_string();
self.to_object(py)
.call_method1(py, "stored_kind", (path_str,))?
.extract(py)
.map_err(Into::into)
})
}
fn supports_content_filtering(&self) -> bool {
Python::attach(|py| {
self.to_object(py)
.call_method0(py, "supports_content_filtering")
.unwrap()
.extract(py)
.unwrap()
})
}
fn supports_file_ids(&self) -> bool {
Python::attach(|py| {
self.to_object(py)
.call_method0(py, "supports_file_ids")
.unwrap()
.extract(py)
.unwrap()
})
}
fn supports_rename_tracking(&self) -> bool {
Python::attach(|py| {
self.to_object(py)
.call_method0(py, "supports_rename_tracking")
.unwrap()
.extract(py)
.unwrap()
})
}
fn supports_symlinks(&self) -> bool {
Python::attach(|py| {
self.to_object(py)
.call_method0(py, "supports_symlinks")
.unwrap()
.extract(py)
.unwrap()
})
}
fn supports_tree_reference(&self) -> bool {
Python::attach(|py| {
self.to_object(py)
.call_method0(py, "supports_tree_reference")
.unwrap()
.extract(py)
.unwrap()
})
}
fn unknowns(&self) -> Result<Vec<PathBuf>, Error> {
Python::attach(|py| {
let unknowns = self.to_object(py).call_method0(py, "unknowns")?;
unknowns.extract(py).map_err(Into::into)
})
}
fn all_versioned_paths(
&self,
) -> Result<Box<dyn Iterator<Item = Result<PathBuf, Error>>>, Error> {
Python::attach(|py| {
struct AllVersionedPathsIter(pyo3::Py<PyAny>);
impl Iterator for AllVersionedPathsIter {
type Item = Result<PathBuf, Error>;
fn next(&mut self) -> Option<Self::Item> {
Python::attach(|py| {
let next = match self.0.call_method0(py, intern!(py, "__next__")) {
Ok(v) => v,
Err(e) => {
if e.is_instance_of::<pyo3::exceptions::PyStopIteration>(py) {
return None;
}
return Some(Err(e.into()));
}
};
if next.is_none(py) {
None
} else {
Some(next.extract(py).map_err(Into::into))
}
})
}
}
Ok(Box::new(AllVersionedPathsIter(
self.to_object(py).call_method0(py, "all_versioned_paths")?,
))
as Box<dyn Iterator<Item = Result<PathBuf, Error>>>)
})
}
fn conflicts(&self) -> Result<Vec<Conflict>, Error> {
Python::attach(|py| {
let conflicts = self.to_object(py).call_method0(py, "conflicts")?;
conflicts.extract(py).map_err(Into::into)
})
}
fn extras(&self) -> Result<Vec<PathBuf>, Error> {
Python::attach(|py| {
let extras = self.to_object(py).call_method0(py, "extras")?;
extras.extract(py).map_err(Into::into)
})
}
fn filter_unversioned_files(&self, paths: &[&Path]) -> Result<Vec<PathBuf>, Error> {
Python::attach(|py| {
let path_strings: Vec<String> = paths
.iter()
.map(|p| p.to_string_lossy().to_string())
.collect();
let result =
self.to_object(py)
.call_method1(py, "filter_unversioned_files", (path_strings,))?;
result.extract(py).map_err(Into::into)
})
}
fn walkdirs(
&self,
prefix: Option<&Path>,
) -> Result<Box<dyn Iterator<Item = Result<WalkdirResult, Error>>>, Error> {
Python::attach(|py| {
type EntryTuple = (String, String, String, Option<Py<PyAny>>, Option<String>);
fn process_entry(entry: &EntryTuple) -> Result<WalkdirResult, Error> {
let kind = entry.2.parse().map_err(|_| {
Error::Other(Python::attach(|_py| {
PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
"Invalid kind: {}",
entry.2
))
}))
})?;
let versioned = entry.4.is_some();
let stat = if let Some(ref py_stat) = entry.3 {
convert_python_stat_to_metadata(py_stat)?
} else {
None
};
Ok(WalkdirResult {
relpath: PathBuf::from(&entry.0),
kind,
stat,
versioned,
})
}
struct WalkdirsIter {
py_iter: pyo3::Py<PyAny>,
current_entries: Vec<EntryTuple>,
current_index: usize,
}
impl Iterator for WalkdirsIter {
type Item = Result<WalkdirResult, Error>;
fn next(&mut self) -> Option<Self::Item> {
Python::attach(|py| {
if self.current_index < self.current_entries.len() {
let entry = &self.current_entries[self.current_index];
self.current_index += 1;
return Some(process_entry(entry));
}
loop {
let next = match self.py_iter.call_method0(py, intern!(py, "__next__"))
{
Ok(v) => v,
Err(e) => {
if e.is_instance_of::<pyo3::exceptions::PyStopIteration>(py) {
return None;
}
return Some(Err(e.into()));
}
};
if next.is_none(py) {
return None;
}
let tuple: (String, Vec<EntryTuple>) = match next.extract(py) {
Ok(t) => t,
Err(e) => return Some(Err(e.into())),
};
self.current_entries = tuple.1;
self.current_index = 0;
if !self.current_entries.is_empty() {
let entry = &self.current_entries[0];
self.current_index = 1;
return Some(process_entry(entry));
}
}
})
}
}
let prefix_str = match prefix {
Some(p) => p.to_string_lossy().to_string(),
None => "".to_string(),
};
let py_iter = self
.to_object(py)
.call_method1(py, "walkdirs", (prefix_str,))?;
Ok(Box::new(WalkdirsIter {
py_iter,
current_entries: Vec::new(),
current_index: 0,
})
as Box<dyn Iterator<Item = Result<WalkdirResult, Error>>>)
})
}
fn versionable_kind(&self, kind: &Kind) -> bool {
Python::attach(|py| {
self.to_object(py)
.call_method1(py, "versionable_kind", (kind.clone(),))
.unwrap()
.extract(py)
.unwrap()
})
}
fn path_content_summary(&self, path: &Path) -> Result<PathContentSummary, Error> {
Python::attach(|py| {
let path_str = path.to_string_lossy().to_string();
let summary =
self.to_object(py)
.call_method1(py, "path_content_summary", (path_str,))?;
let summary_bound = summary.bind(py);
let kind: String = summary_bound.get_item("kind")?.extract()?;
let size: Option<u64> = summary_bound
.get_item("size")
.ok()
.map(|v| v.extract().expect("size should be u64"));
let executable: Option<bool> = summary_bound
.get_item("executable")
.ok()
.map(|v| v.extract().expect("executable should be bool"));
let sha1: Option<String> = summary_bound
.get_item("sha1")
.ok()
.map(|v| v.extract().expect("sha1 should be string"));
let target: Option<String> = summary_bound
.get_item("target")
.ok()
.map(|v| v.extract().expect("target should be string"));
Ok(PathContentSummary {
kind: kind.parse().unwrap(),
size,
executable,
sha1,
target,
})
})
}
fn iter_files_bytes(
&self,
paths: &[&Path],
) -> Result<Box<dyn Iterator<Item = Result<(PathBuf, Vec<u8>), Error>>>, Error> {
Python::attach(|py| {
struct IterFilesBytesIter(pyo3::Py<PyAny>);
impl Iterator for IterFilesBytesIter {
type Item = Result<(PathBuf, Vec<u8>), Error>;
fn next(&mut self) -> Option<Self::Item> {
Python::attach(|py| {
let next = match self.0.call_method0(py, intern!(py, "__next__")) {
Ok(v) => v,
Err(e) => {
if e.is_instance_of::<pyo3::exceptions::PyStopIteration>(py) {
return None;
}
return Some(Err(e.into()));
}
};
if next.is_none(py) {
None
} else {
Some(next.extract(py).map_err(Into::into))
}
})
}
}
let path_strings: Vec<String> = paths
.iter()
.map(|p| p.to_string_lossy().to_string())
.collect();
Ok(Box::new(IterFilesBytesIter(self.to_object(py).call_method1(
py,
"iter_files_bytes",
(path_strings,),
)?))
as Box<
dyn Iterator<Item = Result<(PathBuf, Vec<u8>), Error>>,
>)
})
}
fn iter_entries_by_dir(
&self,
specific_files: Option<&[&Path]>,
) -> Result<Box<dyn Iterator<Item = Result<(PathBuf, TreeEntry), Error>>>, Error> {
Python::attach(|py| {
struct IterEntriesByDirIter(pyo3::Py<PyAny>);
impl Iterator for IterEntriesByDirIter {
type Item = Result<(PathBuf, TreeEntry), Error>;
fn next(&mut self) -> Option<Self::Item> {
Python::attach(|py| {
let next = match self.0.call_method0(py, intern!(py, "__next__")) {
Ok(v) => v,
Err(e) => {
if e.is_instance_of::<pyo3::exceptions::PyStopIteration>(py) {
return None;
}
return Some(Err(e.into()));
}
};
if next.is_none(py) {
None
} else {
Some(next.extract(py).map_err(Into::into))
}
})
}
}
let kwargs = pyo3::types::PyDict::new(py);
if let Some(specific_files) = specific_files {
let path_strings: Vec<String> = specific_files
.iter()
.map(|p| p.to_string_lossy().to_string())
.collect();
kwargs.set_item("specific_files", path_strings)?;
}
Ok(
Box::new(IterEntriesByDirIter(self.to_object(py).call_method(
py,
"iter_entries_by_dir",
(),
Some(&kwargs),
)?))
as Box<dyn Iterator<Item = Result<(PathBuf, TreeEntry), Error>>>,
)
})
}
fn get_file_verifier(
&self,
path: &Path,
_stat_value: Option<&std::fs::Metadata>,
) -> Result<(String, Vec<u8>), Error> {
Python::attach(|py| {
let path_str = path.to_string_lossy().to_string();
let result = self
.to_object(py)
.call_method1(py, "get_file_verifier", (path_str,))?;
result.extract(py).map_err(Into::into)
})
}
fn get_reference_revision(&self, path: &Path) -> Result<RevisionId, Error> {
Python::attach(|py| {
let path_str = path.to_string_lossy().to_string();
let rev = self
.to_object(py)
.call_method1(py, "get_reference_revision", (path_str,))?;
rev.extract(py).map_err(Into::into)
})
}
fn archive(
&self,
format: &str,
name: &str,
root: Option<&str>,
subdir: Option<&Path>,
force_mtime: Option<f64>,
recurse_nested: bool,
) -> Result<Box<dyn Iterator<Item = Result<Vec<u8>, Error>>>, Error> {
Python::attach(|py| {
struct ArchiveIter(pyo3::Py<PyAny>);
impl Iterator for ArchiveIter {
type Item = Result<Vec<u8>, Error>;
fn next(&mut self) -> Option<Self::Item> {
Python::attach(|py| {
let next = match self.0.call_method0(py, intern!(py, "__next__")) {
Ok(v) => v,
Err(e) => {
if e.is_instance_of::<pyo3::exceptions::PyStopIteration>(py) {
return None;
}
return Some(Err(e.into()));
}
};
if next.is_none(py) {
None
} else {
Some(next.extract(py).map_err(Into::into))
}
})
}
}
let kwargs = pyo3::types::PyDict::new(py);
kwargs.set_item("format", format)?;
kwargs.set_item("name", name)?;
if let Some(root) = root {
kwargs.set_item("root", root)?;
}
if let Some(subdir) = subdir {
kwargs.set_item("subdir", subdir.to_string_lossy().to_string())?;
}
if let Some(force_mtime) = force_mtime {
kwargs.set_item("force_mtime", force_mtime)?;
}
kwargs.set_item("recurse_nested", recurse_nested)?;
Ok(Box::new(ArchiveIter(self.to_object(py).call_method(
py,
"archive",
(),
Some(&kwargs),
)?))
as Box<dyn Iterator<Item = Result<Vec<u8>, Error>>>)
})
}
fn annotate_iter(
&self,
path: &Path,
default_revision: Option<&RevisionId>,
) -> Result<Box<dyn Iterator<Item = Result<(RevisionId, Vec<u8>), Error>>>, Error> {
Python::attach(|py| {
struct AnnotateIter(pyo3::Py<PyAny>);
impl Iterator for AnnotateIter {
type Item = Result<(RevisionId, Vec<u8>), Error>;
fn next(&mut self) -> Option<Self::Item> {
Python::attach(|py| {
let next = match self.0.call_method0(py, intern!(py, "__next__")) {
Ok(v) => v,
Err(e) => {
if e.is_instance_of::<pyo3::exceptions::PyStopIteration>(py) {
return None;
}
return Some(Err(e.into()));
}
};
if next.is_none(py) {
None
} else {
Some(next.extract(py).map_err(Into::into))
}
})
}
}
let path_str = path.to_string_lossy().to_string();
let kwargs = pyo3::types::PyDict::new(py);
if let Some(default_revision) = default_revision {
kwargs.set_item(
"default_revision",
default_revision.clone().into_pyobject(py).unwrap(),
)?;
}
let result =
self.to_object(py)
.call_method(py, "annotate_iter", (path_str,), Some(&kwargs))?;
let iterator = py
.import("builtins")?
.getattr("iter")?
.call1((result,))?
.unbind();
Ok(Box::new(AnnotateIter(iterator))
as Box<
dyn Iterator<Item = Result<(RevisionId, Vec<u8>), Error>>,
>)
})
}
fn is_special_path(&self, path: &Path) -> bool {
Python::attach(|py| {
let path_str = path.to_string_lossy().to_string();
self.to_object(py)
.call_method1(py, "is_special_path", (path_str,))
.unwrap()
.extract(py)
.unwrap()
})
}
fn iter_search_rules(
&self,
paths: &[&Path],
) -> Result<Box<dyn Iterator<Item = Result<SearchRule, Error>>>, Error> {
Python::attach(|py| {
struct IterSearchRulesIter(pyo3::Py<PyAny>);
impl Iterator for IterSearchRulesIter {
type Item = Result<SearchRule, Error>;
fn next(&mut self) -> Option<Self::Item> {
Python::attach(|py| {
let next = match self.0.call_method0(py, intern!(py, "__next__")) {
Ok(v) => v,
Err(e) => {
if e.is_instance_of::<pyo3::exceptions::PyStopIteration>(py) {
return None;
}
return Some(Err(e.into()));
}
};
if next.is_none(py) {
None
} else {
let tuple = match next.extract::<(String, String)>(py) {
Ok(t) => t,
Err(e) => return Some(Err(e.into())),
};
let rule_type = match tuple.1.as_str() {
"include" => SearchRuleType::Include,
"exclude" => SearchRuleType::Exclude,
_ => {
return Some(Err(Error::Other(PyErr::new::<
pyo3::exceptions::PyValueError,
_,
>(
"Unknown search rule type"
))))
}
};
Some(Ok(SearchRule {
pattern: tuple.0,
rule_type,
}))
}
})
}
}
let path_strings: Vec<String> = paths
.iter()
.map(|p| p.to_string_lossy().to_string())
.collect();
Ok(
Box::new(IterSearchRulesIter(self.to_object(py).call_method1(
py,
"iter_search_rules",
(path_strings,),
)?)) as Box<dyn Iterator<Item = Result<SearchRule, Error>>>,
)
})
}
}
pub struct GenericTree(Py<PyAny>);
impl<'py> IntoPyObject<'py> for GenericTree {
type Target = PyAny;
type Output = Bound<'py, Self::Target>;
type Error = std::convert::Infallible;
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
Ok(self.0.into_bound(py))
}
}
impl From<Py<PyAny>> for GenericTree {
fn from(obj: Py<PyAny>) -> Self {
GenericTree(obj)
}
}
impl PyTree for GenericTree {
fn to_object(&self, py: Python) -> Py<PyAny> {
self.0.clone_ref(py)
}
}
pub trait MutableTree: Tree {
fn add(&self, files: &[&Path]) -> Result<(), Error>;
fn lock_write(&self) -> Result<Lock, Error>;
fn put_file_bytes_non_atomic(&self, path: &Path, data: &[u8]) -> Result<(), Error>;
fn has_changes(&self) -> std::result::Result<bool, Error>;
fn mkdir(&self, path: &Path) -> Result<(), Error>;
fn remove(&self, files: &[&std::path::Path]) -> Result<(), Error>;
fn add_reference(&self, reference: &TreeReference) -> Result<(), Error>;
fn copy_one(&self, from_path: &Path, to_path: &Path) -> Result<(), Error>;
fn last_revision(&self) -> Result<RevisionId, Error>;
fn lock_tree_write(&self) -> Result<Lock, Error>;
fn set_parent_ids(&self, parent_ids: &[RevisionId]) -> Result<(), Error>;
fn set_parent_trees(&self, parent_trees: &[(RevisionId, RevisionTree)]) -> Result<(), Error>;
fn apply_inventory_delta(&self, delta: Vec<InventoryDelta>) -> Result<(), Error>;
fn commit(
&self,
message: &str,
committer: Option<&str>,
timestamp: Option<f64>,
allow_pointless: Option<bool>,
specific_files: Option<&[&Path]>,
) -> Result<RevisionId, Error>;
}
pub trait PyMutableTree: PyTree + MutableTree {}
impl dyn PyMutableTree {
pub fn as_mutable_tree(&self) -> &dyn MutableTree {
self
}
}
impl<T: PyMutableTree + ?Sized> MutableTree for T {
fn add(&self, files: &[&Path]) -> Result<(), Error> {
for f in files {
assert!(f.is_relative());
}
Python::attach(|py| -> Result<(), PyErr> {
let path_strings: Vec<String> = files
.iter()
.map(|p| p.to_string_lossy().to_string())
.collect();
self.to_object(py)
.call_method1(py, "add", (path_strings,))?;
Ok(())
})
.map_err(Into::into)
}
fn lock_write(&self) -> Result<Lock, Error> {
Python::attach(|py| {
let lock = self
.to_object(py)
.call_method0(py, intern!(py, "lock_write"))?;
Ok(Lock::from(lock))
})
}
fn put_file_bytes_non_atomic(&self, path: &Path, data: &[u8]) -> Result<(), Error> {
assert!(path.is_relative());
Python::attach(|py| {
let path_str = path.to_string_lossy().to_string();
self.to_object(py)
.call_method1(py, "put_file_bytes_non_atomic", (path_str, data))?;
Ok(())
})
}
fn has_changes(&self) -> std::result::Result<bool, Error> {
Python::attach(|py| {
self.to_object(py)
.call_method0(py, "has_changes")?
.extract::<bool>(py)
.map_err(Into::into)
})
}
fn mkdir(&self, path: &Path) -> Result<(), Error> {
assert!(path.is_relative());
Python::attach(|py| -> Result<(), PyErr> {
let path_str = path.to_string_lossy().to_string();
self.to_object(py).call_method1(py, "mkdir", (path_str,))?;
Ok(())
})
.map_err(Into::into)
}
fn remove(&self, files: &[&std::path::Path]) -> Result<(), Error> {
for f in files {
assert!(f.is_relative());
}
Python::attach(|py| -> Result<(), PyErr> {
let path_strings: Vec<String> = files
.iter()
.map(|p| p.to_string_lossy().to_string())
.collect();
self.to_object(py)
.call_method1(py, "remove", (path_strings,))?;
Ok(())
})
.map_err(Into::into)
}
fn add_reference(&self, reference: &TreeReference) -> Result<(), Error> {
Python::attach(|py| {
let kwargs = pyo3::types::PyDict::new(py);
kwargs.set_item("path", reference.path.to_string_lossy().to_string())?;
kwargs.set_item("kind", reference.kind.clone())?;
if let Some(ref rev) = reference.reference_revision {
kwargs.set_item("reference_revision", rev.clone().into_pyobject(py).unwrap())?;
}
self.to_object(py)
.call_method(py, "add_reference", (), Some(&kwargs))?;
Ok(())
})
}
fn copy_one(&self, from_path: &Path, to_path: &Path) -> Result<(), Error> {
assert!(from_path.is_relative());
assert!(to_path.is_relative());
Python::attach(|py| {
let from_str = from_path.to_string_lossy().to_string();
let to_str = to_path.to_string_lossy().to_string();
self.to_object(py)
.call_method1(py, "copy_one", (from_str, to_str))?;
Ok(())
})
}
fn last_revision(&self) -> Result<RevisionId, Error> {
Python::attach(|py| {
let last_revision = self
.to_object(py)
.call_method0(py, intern!(py, "last_revision"))?;
Ok(RevisionId::from(last_revision.extract::<Vec<u8>>(py)?))
})
}
fn lock_tree_write(&self) -> Result<Lock, Error> {
Python::attach(|py| {
let lock = self.to_object(py).call_method0(py, "lock_tree_write")?;
Ok(Lock::from(lock))
})
}
fn set_parent_ids(&self, parent_ids: &[RevisionId]) -> Result<(), Error> {
Python::attach(|py| {
let parent_ids_py: Vec<Py<PyAny>> = parent_ids
.iter()
.map(|id| id.clone().into_pyobject(py).unwrap().unbind())
.collect();
self.to_object(py)
.call_method1(py, "set_parent_ids", (parent_ids_py,))?;
Ok(())
})
}
fn set_parent_trees(&self, parent_trees: &[(RevisionId, RevisionTree)]) -> Result<(), Error> {
Python::attach(|py| {
let parent_trees_py: Vec<(Py<PyAny>, Py<PyAny>)> = parent_trees
.iter()
.map(|(id, tree)| {
(
id.clone().into_pyobject(py).unwrap().unbind(),
tree.to_object(py),
)
})
.collect();
self.to_object(py)
.call_method1(py, "set_parent_trees", (parent_trees_py,))?;
Ok(())
})
}
fn apply_inventory_delta(&self, delta: Vec<InventoryDelta>) -> Result<(), Error> {
Python::attach(|py| {
let delta_py: Vec<Py<PyAny>> = delta
.into_iter()
.map(|d| {
let tuple = pyo3::types::PyTuple::new(
py,
vec![
d.old_path
.map(|p| p.to_string_lossy().to_string())
.into_pyobject(py)
.unwrap()
.into_any(),
d.new_path
.map(|p| p.to_string_lossy().to_string())
.into_pyobject(py)
.unwrap()
.into_any(),
d.file_id.into_pyobject(py).unwrap().into_any(),
d.entry.into_pyobject(py).unwrap().into_any(),
],
)
.unwrap();
tuple.into_any().unbind()
})
.collect();
self.to_object(py)
.call_method1(py, "apply_inventory_delta", (delta_py,))?;
Ok(())
})
}
fn commit(
&self,
message: &str,
committer: Option<&str>,
timestamp: Option<f64>,
allow_pointless: Option<bool>,
specific_files: Option<&[&Path]>,
) -> Result<RevisionId, Error> {
Python::attach(|py| {
let kwargs = pyo3::types::PyDict::new(py);
if let Some(committer) = committer {
kwargs.set_item("committer", committer)?;
}
if let Some(timestamp) = timestamp {
kwargs.set_item("timestamp", timestamp)?;
}
if let Some(allow_pointless) = allow_pointless {
kwargs.set_item("allow_pointless", allow_pointless)?;
}
if let Some(specific_files) = specific_files {
let file_paths: Vec<String> = specific_files
.iter()
.map(|p| p.to_string_lossy().to_string())
.collect();
kwargs.set_item("specific_files", file_paths)?;
}
let result = self
.to_object(py)
.call_method(py, "commit", (message,), Some(&kwargs))?;
result.extract(py).map_err(Into::into)
})
}
}
pub struct RevisionTree(pub Py<PyAny>);
impl<'py> IntoPyObject<'py> for RevisionTree {
type Target = PyAny;
type Output = Bound<'py, Self::Target>;
type Error = std::convert::Infallible;
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
Ok(self.0.into_bound(py))
}
}
impl PyTree for RevisionTree {
fn to_object(&self, py: Python) -> Py<PyAny> {
self.0.clone_ref(py)
}
}
impl Clone for RevisionTree {
fn clone(&self) -> Self {
Python::attach(|py| RevisionTree(self.0.clone_ref(py)))
}
}
impl RevisionTree {
pub fn repository(&self) -> crate::repository::GenericRepository {
Python::attach(|py| {
let repository = self.to_object(py).getattr(py, "_repository").unwrap();
crate::repository::GenericRepository::new(repository)
})
}
pub fn get_revision_id(&self) -> RevisionId {
Python::attach(|py| {
self.to_object(py)
.call_method0(py, "get_revision_id")
.unwrap()
.extract(py)
.unwrap()
})
}
pub fn get_parent_ids(&self) -> Vec<RevisionId> {
Python::attach(|py| {
self.to_object(py)
.call_method0(py, intern!(py, "get_parent_ids"))
.unwrap()
.extract(py)
.unwrap()
})
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct TreeChange {
pub path: (Option<PathBuf>, Option<PathBuf>),
pub changed_content: bool,
pub versioned: (Option<bool>, Option<bool>),
pub name: (Option<std::ffi::OsString>, Option<std::ffi::OsString>),
pub kind: (Option<Kind>, Option<Kind>),
pub executable: (Option<bool>, Option<bool>),
pub copied: bool,
}
impl<'py> IntoPyObject<'py> for TreeChange {
type Target = PyAny;
type Output = Bound<'py, Self::Target>;
type Error = std::convert::Infallible;
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
let dict = pyo3::types::PyDict::new(py);
dict.set_item(
"path",
(
self.path
.0
.as_ref()
.map(|p| p.to_string_lossy().to_string()),
self.path
.1
.as_ref()
.map(|p| p.to_string_lossy().to_string()),
),
)
.unwrap();
dict.set_item("changed_content", self.changed_content)
.unwrap();
dict.set_item("versioned", self.versioned).unwrap();
dict.set_item("name", &self.name).unwrap();
dict.set_item("kind", self.kind.clone()).unwrap();
dict.set_item("executable", self.executable).unwrap();
dict.set_item("copied", self.copied).unwrap();
Ok(dict.into_any())
}
}
impl<'a, 'py> FromPyObject<'a, 'py> for TreeChange {
type Error = PyErr;
fn extract(obj: Borrowed<'a, 'py, PyAny>) -> PyResult<Self> {
fn from_bool(o: &Bound<PyAny>) -> PyResult<bool> {
if let Ok(b) = o.extract::<isize>() {
Ok(b != 0)
} else {
o.extract::<bool>()
}
}
fn from_opt_bool_tuple(o: &Bound<PyAny>) -> PyResult<(Option<bool>, Option<bool>)> {
let tuple = o.extract::<(Option<Bound<PyAny>>, Option<Bound<PyAny>>)>()?;
Ok((
tuple.0.map(|o| from_bool(&o.as_borrowed())).transpose()?,
tuple.1.map(|o| from_bool(&o.as_borrowed())).transpose()?,
))
}
let path = obj.getattr("path")?;
let changed_content = from_bool(&obj.getattr("changed_content")?)?;
let versioned = from_opt_bool_tuple(&obj.getattr("versioned")?)?;
let name = obj.getattr("name")?;
let kind = obj.getattr("kind")?;
let executable = from_opt_bool_tuple(&obj.getattr("executable")?)?;
let copied = obj.getattr("copied")?;
Ok(TreeChange {
path: path.extract()?,
changed_content,
versioned,
name: name.extract()?,
kind: kind.extract()?,
executable,
copied: copied.extract()?,
})
}
}
pub struct MemoryTree(pub Py<PyAny>);
impl<'py> IntoPyObject<'py> for MemoryTree {
type Target = PyAny;
type Output = Bound<'py, Self::Target>;
type Error = std::convert::Infallible;
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
Ok(self.0.into_bound(py))
}
}
impl<B: crate::branch::PyBranch> From<&B> for MemoryTree {
fn from(branch: &B) -> Self {
Python::attach(|py| {
MemoryTree(
branch
.to_object(py)
.call_method0(py, "create_memorytree")
.unwrap()
.extract(py)
.unwrap(),
)
})
}
}
impl PyTree for MemoryTree {
fn to_object(&self, py: Python) -> Py<PyAny> {
self.0.clone_ref(py)
}
}
impl PyMutableTree for MemoryTree {}
pub use crate::workingtree::WorkingTree;
#[cfg(test)]
mod tests {
use super::*;
use crate::controldir::{create_standalone_workingtree, ControlDirFormat};
use serial_test::serial;
#[test]
#[serial]
fn test_remove() {
let env = crate::testing::TestEnv::new();
let wt =
create_standalone_workingtree(std::path::Path::new("."), &ControlDirFormat::default())
.unwrap();
let path = std::path::Path::new("foo");
std::fs::write(&path, b"").unwrap();
wt.add(&[(std::path::Path::new("foo"))]).unwrap();
wt.build_commit()
.message("Initial commit")
.reporter(&crate::commit::NullCommitReporter::new())
.commit()
.unwrap();
assert!(wt.has_filename(&path));
wt.remove(&[Path::new("foo")]).unwrap();
assert!(!wt.is_versioned(&path));
std::mem::drop(env);
}
#[test]
#[serial]
fn test_walkdirs() {
let env = crate::testing::TestEnv::new();
let wt =
create_standalone_workingtree(std::path::Path::new("."), &ControlDirFormat::default())
.unwrap();
std::fs::create_dir("subdir").unwrap();
std::fs::write("file1.txt", b"content1").unwrap();
std::fs::write("subdir/file2.txt", b"content2").unwrap();
std::fs::write(".gitattributes", b"* text=auto\n").unwrap();
wt.add(&[
Path::new("file1.txt"),
Path::new("subdir"),
Path::new("subdir/file2.txt"),
Path::new(".gitattributes"),
])
.unwrap();
wt.build_commit()
.message("Add files")
.reporter(&crate::commit::NullCommitReporter::new())
.commit()
.unwrap();
let lock = wt.lock_read().unwrap();
let entries: Vec<_> = wt.walkdirs(None).unwrap().collect();
assert!(entries.len() > 0, "Should have at least some entries");
let found_gitattributes = entries.iter().any(|entry| {
entry
.as_ref()
.map(|e| e.relpath.file_name() == Some(std::ffi::OsStr::new(".gitattributes")))
.unwrap_or(false)
});
assert!(
found_gitattributes,
"Should find .gitattributes file. Found entries: {:?}",
entries
.iter()
.filter_map(|e| e.as_ref().ok())
.map(|e| &e.relpath)
.collect::<Vec<_>>()
);
let found_subdir_file = entries.iter().any(|entry| {
entry
.as_ref()
.map(|e| e.relpath.to_str() == Some("subdir/file2.txt"))
.unwrap_or(false)
});
assert!(found_subdir_file, "Should find file in subdirectory");
std::mem::drop(lock);
std::mem::drop(env);
}
}