Skip to main content

breezyshim/
tree.rs

1//! Trees
2use crate::error::Error;
3use crate::lock::Lock;
4use crate::revisionid::RevisionId;
5use pyo3::intern;
6use pyo3::prelude::*;
7
8/// Type alias for std::path::Path.
9pub type Path = std::path::Path;
10/// Type alias for std::path::PathBuf.
11pub type PathBuf = std::path::PathBuf;
12
13/// Convert a Python stat_result object to Rust Metadata.
14///
15/// Note: std::fs::Metadata is an opaque platform-specific type that cannot be
16/// directly constructed. This function extracts and validates the stat data from
17/// Python, but currently returns None since we cannot create a Metadata instance.
18///
19/// In the future, this could be extended to return a custom metadata type or
20/// to use platform-specific unsafe code to construct Metadata.
21///
22/// # Arguments
23/// * `py_stat` - Python stat_result object (from os.stat or similar)
24///
25/// # Returns
26/// * `Ok(None)` - Successfully validated stat object (but cannot convert to Metadata)
27/// * `Err(_)` - Invalid or missing stat fields
28fn convert_python_stat_to_metadata(
29    py_stat: &Py<PyAny>,
30) -> Result<Option<std::fs::Metadata>, Error> {
31    Python::attach(|py| {
32        let stat_obj = py_stat.bind(py);
33
34        // Validate that required fields exist and are correct types
35        let _st_mode: u32 = stat_obj.getattr("st_mode")?.extract()?;
36        let _st_size: u64 = stat_obj.getattr("st_size")?.extract()?;
37        let _st_mtime: f64 = stat_obj.getattr("st_mtime")?.extract()?;
38
39        // Cannot construct std::fs::Metadata directly, so return None
40        // The stat data is validated but not converted
41        Ok(None)
42    })
43}
44
45/// Result of walking directories in a tree.
46#[derive(Debug)]
47pub struct WalkdirResult {
48    /// The path relative to the tree root.
49    pub relpath: PathBuf,
50    /// The kind of the entry.
51    pub kind: Kind,
52    /// The stat information for the entry.
53    pub stat: Option<std::fs::Metadata>,
54    /// Whether the entry is versioned.
55    pub versioned: bool,
56}
57
58/// Summary of path content.
59#[derive(Debug)]
60pub struct PathContentSummary {
61    /// The kind of the content.
62    pub kind: Kind,
63    /// The size in bytes (for files).
64    pub size: Option<u64>,
65    /// Whether the file is executable.
66    pub executable: Option<bool>,
67    /// The SHA1 hash (for files).
68    pub sha1: Option<String>,
69    /// The target (for symlinks).
70    pub target: Option<String>,
71}
72
73/// Search rule for path matching.
74#[derive(Debug)]
75pub struct SearchRule {
76    /// The pattern to match.
77    pub pattern: String,
78    /// The type of rule.
79    pub rule_type: SearchRuleType,
80}
81
82/// Type of search rule.
83#[derive(Debug)]
84pub enum SearchRuleType {
85    /// Include the matched paths.
86    Include,
87    /// Exclude the matched paths.
88    Exclude,
89}
90
91/// Represents a conflict in a tree.
92#[derive(Debug)]
93pub struct Conflict {
94    /// The path involved in the conflict.
95    pub path: PathBuf,
96    /// The type of conflict.
97    pub conflict_type: String,
98    /// Additional information about the conflict.
99    pub message: Option<String>,
100}
101
102impl<'a, 'py> FromPyObject<'a, 'py> for Conflict {
103    type Error = PyErr;
104
105    fn extract(ob: Borrowed<'a, 'py, PyAny>) -> PyResult<Self> {
106        let path: String = ob.getattr("path")?.extract()?;
107        let conflict_type: String = ob.getattr("typestring")?.extract()?;
108        let message: Option<String> = ob.getattr("message").ok().and_then(|m| m.extract().ok());
109
110        Ok(Conflict {
111            path: PathBuf::from(path),
112            conflict_type,
113            message,
114        })
115    }
116}
117
118/// Represents a tree reference.
119#[derive(Debug)]
120pub struct TreeReference {
121    /// The path where the reference should be added.
122    pub path: PathBuf,
123    /// The kind of reference.
124    pub kind: Kind,
125    /// The reference revision.
126    pub reference_revision: Option<RevisionId>,
127}
128
129/// Represents a change in the inventory.
130#[derive(Debug)]
131pub struct InventoryDelta {
132    /// The old path (None if new).
133    pub old_path: Option<PathBuf>,
134    /// The new path (None if deleted).
135    pub new_path: Option<PathBuf>,
136    /// The file ID.
137    pub file_id: String,
138    /// The entry details.
139    pub entry: Option<TreeEntry>,
140}
141
142#[derive(Debug, PartialEq, Clone, Eq)]
143/// Kind of object in a tree.
144pub enum Kind {
145    /// Regular file.
146    File,
147    /// Directory.
148    Directory,
149    /// Symbolic link.
150    Symlink,
151    /// Reference to another tree.
152    TreeReference,
153}
154
155impl Kind {
156    /// Get a marker string for this kind of tree object.
157    pub fn marker(&self) -> &'static str {
158        match self {
159            Kind::File => "",
160            Kind::Directory => "/",
161            Kind::Symlink => "@",
162            Kind::TreeReference => "+",
163        }
164    }
165}
166
167impl std::fmt::Display for Kind {
168    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
169        match self {
170            Kind::File => write!(f, "file"),
171            Kind::Directory => write!(f, "directory"),
172            Kind::Symlink => write!(f, "symlink"),
173            Kind::TreeReference => write!(f, "tree-reference"),
174        }
175    }
176}
177
178impl std::str::FromStr for Kind {
179    type Err = String;
180
181    fn from_str(s: &str) -> Result<Self, Self::Err> {
182        match s {
183            "file" => Ok(Kind::File),
184            "directory" => Ok(Kind::Directory),
185            "symlink" => Ok(Kind::Symlink),
186            "tree-reference" => Ok(Kind::TreeReference),
187            n => Err(format!("Invalid kind: {}", n)),
188        }
189    }
190}
191
192impl<'py> pyo3::IntoPyObject<'py> for Kind {
193    type Target = pyo3::PyAny;
194    type Output = pyo3::Bound<'py, Self::Target>;
195    type Error = std::convert::Infallible;
196
197    fn into_pyobject(self, py: pyo3::Python<'py>) -> Result<Self::Output, Self::Error> {
198        let s = match self {
199            Kind::File => "file",
200            Kind::Directory => "directory",
201            Kind::Symlink => "symlink",
202            Kind::TreeReference => "tree-reference",
203        };
204        Ok(pyo3::types::PyString::new(py, s).into_any())
205    }
206}
207
208impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for Kind {
209    type Error = PyErr;
210
211    fn extract(ob: Borrowed<'a, 'py, pyo3::PyAny>) -> PyResult<Self> {
212        let s: String = ob.extract()?;
213        match s.as_str() {
214            "file" => Ok(Kind::File),
215            "directory" => Ok(Kind::Directory),
216            "symlink" => Ok(Kind::Symlink),
217            "tree-reference" => Ok(Kind::TreeReference),
218            _ => Err(pyo3::exceptions::PyValueError::new_err(format!(
219                "Invalid kind: {}",
220                s
221            ))),
222        }
223    }
224}
225
226/// A tree entry, representing different types of objects in a tree.
227#[derive(Debug)]
228pub enum TreeEntry {
229    /// A regular file entry.
230    File {
231        /// Whether the file is executable.
232        executable: bool,
233        /// The kind of file.
234        kind: Kind,
235        /// The revision ID that introduced this file, if known.
236        revision: Option<RevisionId>,
237        /// The size of the file in bytes.
238        size: u64,
239    },
240    /// A directory entry.
241    Directory {
242        /// The revision ID that introduced this directory, if known.
243        revision: Option<RevisionId>,
244    },
245    /// A symbolic link entry.
246    Symlink {
247        /// The revision ID that introduced this symlink, if known.
248        revision: Option<RevisionId>,
249        /// The target path of the symbolic link.
250        symlink_target: String,
251    },
252    /// A reference to another tree.
253    TreeReference {
254        /// The revision ID that introduced this reference, if known.
255        revision: Option<RevisionId>,
256        /// The revision ID this reference points to.
257        reference_revision: RevisionId,
258    },
259}
260
261impl<'a, 'py> FromPyObject<'a, 'py> for TreeEntry {
262    type Error = PyErr;
263
264    fn extract(ob: Borrowed<'a, 'py, PyAny>) -> PyResult<Self> {
265        let kind: String = ob.getattr("kind")?.extract()?;
266        match kind.as_str() {
267            "file" => {
268                let executable: bool = ob.getattr("executable")?.extract()?;
269                let kind: Kind = ob.getattr("kind")?.extract()?;
270                let size: u64 = ob.getattr("size")?.extract()?;
271                let revision: Option<RevisionId> = ob.getattr("revision")?.extract()?;
272                Ok(TreeEntry::File {
273                    executable,
274                    kind,
275                    size,
276                    revision,
277                })
278            }
279            "directory" => {
280                let revision: Option<RevisionId> = ob.getattr("revision")?.extract()?;
281                Ok(TreeEntry::Directory { revision })
282            }
283            "symlink" => {
284                let revision: Option<RevisionId> = ob.getattr("revision")?.extract()?;
285                let symlink_target: String = ob.getattr("symlink_target")?.extract()?;
286                Ok(TreeEntry::Symlink {
287                    revision,
288                    symlink_target,
289                })
290            }
291            "tree-reference" => {
292                let revision: Option<RevisionId> = ob.getattr("revision")?.extract()?;
293                let reference_revision: RevisionId = ob.getattr("reference_revision")?.extract()?;
294                Ok(TreeEntry::TreeReference {
295                    revision,
296                    reference_revision,
297                })
298            }
299            kind => panic!("Invalid kind: {}", kind),
300        }
301    }
302}
303
304impl<'py> IntoPyObject<'py> for TreeEntry {
305    type Target = PyAny;
306    type Output = Bound<'py, Self::Target>;
307    type Error = std::convert::Infallible;
308
309    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
310        let dict = pyo3::types::PyDict::new(py);
311        match self {
312            TreeEntry::File {
313                executable,
314                kind: _,
315                revision,
316                size,
317            } => {
318                dict.set_item("kind", "file").unwrap();
319                dict.set_item("executable", executable).unwrap();
320                dict.set_item("size", size).unwrap();
321                dict.set_item("revision", revision).unwrap();
322            }
323            TreeEntry::Directory { revision } => {
324                dict.set_item("kind", "directory").unwrap();
325                dict.set_item("revision", revision).unwrap();
326            }
327            TreeEntry::Symlink {
328                revision,
329                symlink_target,
330            } => {
331                dict.set_item("kind", "symlink").unwrap();
332                dict.set_item("revision", revision).unwrap();
333                dict.set_item("symlink_target", symlink_target).unwrap();
334            }
335            TreeEntry::TreeReference {
336                revision,
337                reference_revision,
338            } => {
339                dict.set_item("kind", "tree-reference").unwrap();
340                dict.set_item("revision", revision).unwrap();
341                dict.set_item("reference_revision", reference_revision)
342                    .unwrap();
343            }
344        }
345        Ok(dict.into_any())
346    }
347}
348
349/// The core tree interface that provides access to content and metadata.
350///
351/// A tree represents a structured collection of files that can be
352/// read, modified, and compared, depending on the implementation.
353pub trait Tree {
354    /// Get a dictionary of tags and their revision IDs.
355    fn get_tag_dict(&self) -> Result<std::collections::HashMap<String, RevisionId>, Error>;
356    /// Get a file from the tree as a readable stream.
357    fn get_file(&self, path: &Path) -> Result<Box<dyn std::io::Read>, Error>;
358    /// Get the contents of a file from the tree as a byte vector.
359    fn get_file_text(&self, path: &Path) -> Result<Vec<u8>, Error>;
360    /// Get the contents of a file as a vector of lines (byte vectors).
361    fn get_file_lines(&self, path: &Path) -> Result<Vec<Vec<u8>>, Error>;
362    /// Lock the tree for read operations.
363    fn lock_read(&self) -> Result<Lock, Error>;
364
365    /// Check if a file exists in the tree at the specified path.
366    fn has_filename(&self, path: &Path) -> bool;
367
368    /// Get the target of a symbolic link.
369    fn get_symlink_target(&self, path: &Path) -> Result<PathBuf, Error>;
370
371    /// Get the IDs of the parent revisions of this tree.
372    fn get_parent_ids(&self) -> Result<Vec<RevisionId>, Error>;
373    /// Check if a path is ignored by version control.
374    fn is_ignored(&self, path: &Path) -> Option<String>;
375    /// Get the kind of object at the specified path (file, directory, symlink, etc.).
376    fn kind(&self, path: &Path) -> Result<Kind, Error>;
377    /// Check if a path is under version control.
378    fn is_versioned(&self, path: &Path) -> bool;
379
380    /// Iterate through the changes between this tree and another tree.
381    ///
382    /// # Arguments
383    /// * `other` - The other tree to compare against
384    /// * `specific_files` - Optional list of specific files to check
385    /// * `want_unversioned` - Whether to include unversioned files
386    /// * `require_versioned` - Whether to require files to be versioned
387    fn iter_changes(
388        &self,
389        other: &dyn PyTree,
390        specific_files: Option<&[&Path]>,
391        want_unversioned: Option<bool>,
392        require_versioned: Option<bool>,
393    ) -> Result<Box<dyn Iterator<Item = Result<TreeChange, Error>>>, Error>;
394
395    /// Check if this tree supports versioned directories.
396    fn has_versioned_directories(&self) -> bool;
397
398    /// Get a preview of transformations that would be applied to this tree.
399    fn preview_transform(&self) -> Result<crate::transform::TreeTransform, Error>;
400
401    /// List files in the tree, optionally recursively.
402    ///
403    /// # Arguments
404    /// * `include_root` - Whether to include the root directory
405    /// * `from_dir` - Starting directory (if not the root)
406    /// * `recursive` - Whether to recurse into subdirectories
407    /// * `recurse_nested` - Whether to recurse into nested trees
408    fn list_files(
409        &self,
410        include_root: Option<bool>,
411        from_dir: Option<&Path>,
412        recursive: Option<bool>,
413        recurse_nested: Option<bool>,
414    ) -> Result<Box<dyn Iterator<Item = Result<(PathBuf, bool, Kind, TreeEntry), Error>>>, Error>;
415
416    /// Iterate through entries in a directory.
417    ///
418    /// # Arguments
419    /// * `path` - Path to the directory to list
420    fn iter_child_entries(
421        &self,
422        path: &std::path::Path,
423    ) -> Result<Box<dyn Iterator<Item = Result<(PathBuf, Kind, TreeEntry), Error>>>, Error>;
424
425    /// Get the size of a file in bytes.
426    fn get_file_size(&self, path: &Path) -> Result<u64, Error>;
427
428    /// Get the SHA1 hash of a file's contents.
429    fn get_file_sha1(
430        &self,
431        path: &Path,
432        stat_value: Option<&std::fs::Metadata>,
433    ) -> Result<String, Error>;
434
435    /// Get the modification time of a file.
436    fn get_file_mtime(&self, path: &Path) -> Result<u64, Error>;
437
438    /// Check if a file is executable.
439    fn is_executable(&self, path: &Path) -> Result<bool, Error>;
440
441    /// Get the stored kind of a file (as opposed to the actual kind on disk).
442    fn stored_kind(&self, path: &Path) -> Result<Kind, Error>;
443
444    /// Check if the tree supports content filtering.
445    fn supports_content_filtering(&self) -> bool;
446
447    /// Check if the tree supports file IDs.
448    fn supports_file_ids(&self) -> bool;
449
450    /// Check if the tree supports rename tracking.
451    fn supports_rename_tracking(&self) -> bool;
452
453    /// Check if the tree supports symbolic links.
454    fn supports_symlinks(&self) -> bool;
455
456    /// Check if the tree supports tree references.
457    fn supports_tree_reference(&self) -> bool;
458
459    /// Get unversioned files in the tree.
460    fn unknowns(&self) -> Result<Vec<PathBuf>, Error>;
461
462    /// Get all versioned paths in the tree.
463    fn all_versioned_paths(
464        &self,
465    ) -> Result<Box<dyn Iterator<Item = Result<PathBuf, Error>>>, Error>;
466
467    /// Get conflicts in the tree.
468    fn conflicts(&self) -> Result<Vec<Conflict>, Error>;
469
470    /// Get extra (unversioned) files in the tree.
471    fn extras(&self) -> Result<Vec<PathBuf>, Error>;
472
473    /// Filter out versioned files from a list of paths.
474    fn filter_unversioned_files(&self, paths: &[&Path]) -> Result<Vec<PathBuf>, Error>;
475
476    /// Walk directories in the tree.
477    fn walkdirs(
478        &self,
479        prefix: Option<&Path>,
480    ) -> Result<Box<dyn Iterator<Item = Result<WalkdirResult, Error>>>, Error>;
481
482    /// Check if a file kind is versionable.
483    fn versionable_kind(&self, kind: &Kind) -> bool;
484
485    /// Get file content summary for a path.
486    fn path_content_summary(&self, path: &Path) -> Result<PathContentSummary, Error>;
487
488    /// Iterate through file bytes.
489    fn iter_files_bytes(
490        &self,
491        paths: &[&Path],
492    ) -> Result<Box<dyn Iterator<Item = Result<(PathBuf, Vec<u8>), Error>>>, Error>;
493
494    /// Iterate through entries by directory.
495    fn iter_entries_by_dir(
496        &self,
497        specific_files: Option<&[&Path]>,
498    ) -> Result<Box<dyn Iterator<Item = Result<(PathBuf, TreeEntry), Error>>>, Error>;
499
500    /// Get file verifier information.
501    fn get_file_verifier(
502        &self,
503        path: &Path,
504        stat_value: Option<&std::fs::Metadata>,
505    ) -> Result<(String, Vec<u8>), Error>;
506
507    /// Get the reference revision for a tree reference.
508    fn get_reference_revision(&self, path: &Path) -> Result<RevisionId, Error>;
509
510    /// Create an archive of the tree.
511    fn archive(
512        &self,
513        format: &str,
514        name: &str,
515        root: Option<&str>,
516        subdir: Option<&Path>,
517        force_mtime: Option<f64>,
518        recurse_nested: bool,
519    ) -> Result<Box<dyn Iterator<Item = Result<Vec<u8>, Error>>>, Error>;
520
521    /// Annotate a file with revision information.
522    fn annotate_iter(
523        &self,
524        path: &Path,
525        default_revision: Option<&RevisionId>,
526    ) -> Result<Box<dyn Iterator<Item = Result<(RevisionId, Vec<u8>), Error>>>, Error>;
527
528    /// Check if a path is a special path (e.g., control directory).
529    fn is_special_path(&self, path: &Path) -> bool;
530
531    /// Iterate through search rules.
532    fn iter_search_rules(
533        &self,
534        paths: &[&Path],
535    ) -> Result<Box<dyn Iterator<Item = Result<SearchRule, Error>>>, Error>;
536}
537
538/// Trait for Python tree objects that can be converted to and from Python objects.
539///
540/// This trait is implemented by all tree types that wrap Python objects.
541pub trait PyTree: Tree + std::any::Any {
542    /// Get the underlying Python object for this tree.
543    fn to_object(&self, py: Python) -> Py<PyAny>;
544}
545
546impl dyn PyTree {
547    /// Get a reference to self as a Tree trait object.
548    pub fn as_tree(&self) -> &dyn Tree {
549        self
550    }
551}
552
553impl<T: PyTree + ?Sized> Tree for T {
554    fn get_tag_dict(&self) -> Result<std::collections::HashMap<String, RevisionId>, Error> {
555        Python::attach(|py| {
556            let branch = self.to_object(py).getattr(py, "branch")?;
557            let tags = branch.getattr(py, "tags")?;
558            let tag_dict = tags.call_method0(py, intern!(py, "get_tag_dict"))?;
559            tag_dict.extract(py)
560        })
561        .map_err(|e: PyErr| -> Error { e.into() })
562    }
563
564    fn get_file(&self, path: &Path) -> Result<Box<dyn std::io::Read>, Error> {
565        Python::attach(|py| {
566            let path_str = path.to_string_lossy().to_string();
567            let f = self
568                .to_object(py)
569                .call_method1(py, "get_file", (path_str,))?;
570
571            let f = pyo3_filelike::PyBinaryFile::from(f);
572
573            Ok(Box::new(f) as Box<dyn std::io::Read>)
574        })
575    }
576
577    fn get_file_text(&self, path: &Path) -> Result<Vec<u8>, Error> {
578        Python::attach(|py| {
579            let path_str = path.to_string_lossy().to_string();
580            let text = self
581                .to_object(py)
582                .call_method1(py, "get_file_text", (path_str,))?;
583            text.extract(py).map_err(Into::into)
584        })
585    }
586
587    fn get_file_lines(&self, path: &Path) -> Result<Vec<Vec<u8>>, Error> {
588        Python::attach(|py| {
589            let path_str = path.to_string_lossy().to_string();
590            let lines = self
591                .to_object(py)
592                .call_method1(py, "get_file_lines", (path_str,))?;
593            lines.extract(py).map_err(Into::into)
594        })
595    }
596
597    fn lock_read(&self) -> Result<Lock, Error> {
598        Python::attach(|py| {
599            let lock = self
600                .to_object(py)
601                .call_method0(py, intern!(py, "lock_read"))?;
602            Ok(Lock::from(lock))
603        })
604    }
605
606    fn has_filename(&self, path: &Path) -> bool {
607        Python::attach(|py| {
608            let path_str = path.to_string_lossy().to_string();
609            self.to_object(py)
610                .call_method1(py, intern!(py, "has_filename"), (path_str,))
611                .and_then(|result| result.extract(py))
612                .unwrap_or(false)
613        })
614    }
615
616    fn get_symlink_target(&self, path: &Path) -> Result<PathBuf, Error> {
617        Python::attach(|py| {
618            let path_str = path.to_string_lossy().to_string();
619            let target = self
620                .to_object(py)
621                .call_method1(py, "get_symlink_target", (path_str,))?;
622            target.extract(py).map_err(Into::into)
623        })
624    }
625
626    fn get_parent_ids(&self) -> Result<Vec<RevisionId>, Error> {
627        Python::attach(|py| {
628            Ok(self
629                .to_object(py)
630                .call_method0(py, intern!(py, "get_parent_ids"))
631                .unwrap()
632                .extract(py)?)
633        })
634    }
635
636    fn is_ignored(&self, path: &Path) -> Option<String> {
637        Python::attach(|py| {
638            let path_str = path.to_string_lossy().to_string();
639            self.to_object(py)
640                .call_method1(py, "is_ignored", (path_str,))
641                .unwrap()
642                .extract(py)
643                .unwrap()
644        })
645    }
646
647    fn kind(&self, path: &Path) -> Result<Kind, Error> {
648        Python::attach(|py| {
649            let path_str = path.to_string_lossy().to_string();
650            self.to_object(py)
651                .call_method1(py, "kind", (path_str,))
652                .unwrap()
653                .extract(py)
654                .map_err(Into::into)
655        })
656    }
657
658    fn is_versioned(&self, path: &Path) -> bool {
659        Python::attach(|py| {
660            let path_str = path.to_string_lossy().to_string();
661            self.to_object(py)
662                .call_method1(py, "is_versioned", (path_str,))
663                .unwrap()
664                .extract(py)
665                .unwrap()
666        })
667    }
668
669    fn iter_changes(
670        &self,
671        other: &dyn PyTree,
672        specific_files: Option<&[&Path]>,
673        want_unversioned: Option<bool>,
674        require_versioned: Option<bool>,
675    ) -> Result<Box<dyn Iterator<Item = Result<TreeChange, Error>>>, Error> {
676        Python::attach(|py| {
677            let kwargs = pyo3::types::PyDict::new(py);
678            if let Some(specific_files) = specific_files {
679                kwargs.set_item(
680                    "specific_files",
681                    specific_files
682                        .iter()
683                        .map(|p| p.to_string_lossy().to_string())
684                        .collect::<Vec<_>>(),
685                )?;
686            }
687            if let Some(want_unversioned) = want_unversioned {
688                kwargs.set_item("want_unversioned", want_unversioned)?;
689            }
690            if let Some(require_versioned) = require_versioned {
691                kwargs.set_item("require_versioned", require_versioned)?;
692            }
693            struct TreeChangeIter(pyo3::Py<PyAny>);
694
695            impl Iterator for TreeChangeIter {
696                type Item = Result<TreeChange, Error>;
697
698                fn next(&mut self) -> Option<Self::Item> {
699                    Python::attach(|py| {
700                        let next = match self.0.call_method0(py, intern!(py, "__next__")) {
701                            Ok(v) => v,
702                            Err(e) => {
703                                if e.is_instance_of::<pyo3::exceptions::PyStopIteration>(py) {
704                                    return None;
705                                }
706                                return Some(Err(e.into()));
707                            }
708                        };
709
710                        if next.is_none(py) {
711                            None
712                        } else {
713                            Some(next.extract(py).map_err(Into::into))
714                        }
715                    })
716                }
717            }
718
719            Ok(Box::new(TreeChangeIter(self.to_object(py).call_method(
720                py,
721                "iter_changes",
722                (other.to_object(py),),
723                Some(&kwargs),
724            )?))
725                as Box<dyn Iterator<Item = Result<TreeChange, Error>>>)
726        })
727    }
728
729    fn has_versioned_directories(&self) -> bool {
730        Python::attach(|py| {
731            self.to_object(py)
732                .call_method0(py, "has_versioned_directories")
733                .unwrap()
734                .extract(py)
735                .unwrap()
736        })
737    }
738
739    fn preview_transform(&self) -> Result<crate::transform::TreeTransform, Error> {
740        Python::attach(|py| {
741            let transform = self.to_object(py).call_method0(py, "preview_transform")?;
742            Ok(crate::transform::TreeTransform::from(transform))
743        })
744    }
745
746    fn list_files(
747        &self,
748        include_root: Option<bool>,
749        from_dir: Option<&Path>,
750        recursive: Option<bool>,
751        recurse_nested: Option<bool>,
752    ) -> Result<Box<dyn Iterator<Item = Result<(PathBuf, bool, Kind, TreeEntry), Error>>>, Error>
753    {
754        Python::attach(|py| {
755            let kwargs = pyo3::types::PyDict::new(py);
756            if let Some(include_root) = include_root {
757                kwargs.set_item("include_root", include_root)?;
758            }
759            if let Some(from_dir) = from_dir {
760                kwargs.set_item("from_dir", from_dir.to_string_lossy().to_string())?;
761            }
762            if let Some(recursive) = recursive {
763                kwargs.set_item("recursive", recursive)?;
764            }
765            if let Some(recurse_nested) = recurse_nested {
766                kwargs.set_item("recurse_nested", recurse_nested)?;
767            }
768            struct ListFilesIter(pyo3::Py<PyAny>);
769
770            impl Iterator for ListFilesIter {
771                type Item = Result<(PathBuf, bool, Kind, TreeEntry), Error>;
772
773                fn next(&mut self) -> Option<Self::Item> {
774                    Python::attach(|py| {
775                        let next = match self.0.call_method0(py, intern!(py, "__next__")) {
776                            Ok(v) => v,
777                            Err(e) => {
778                                if e.is_instance_of::<pyo3::exceptions::PyStopIteration>(py) {
779                                    return None;
780                                }
781                                return Some(Err(e.into()));
782                            }
783                        };
784
785                        if next.is_none(py) {
786                            None
787                        } else {
788                            Some(next.extract(py).map_err(Into::into))
789                        }
790                    })
791                }
792            }
793
794            Ok(Box::new(ListFilesIter(self.to_object(py).call_method(
795                py,
796                "list_files",
797                (),
798                Some(&kwargs),
799            )?))
800                as Box<
801                    dyn Iterator<Item = Result<(PathBuf, bool, Kind, TreeEntry), Error>>,
802                >)
803        })
804        .map_err(|e: PyErr| -> Error { e.into() })
805    }
806
807    fn iter_child_entries(
808        &self,
809        path: &std::path::Path,
810    ) -> Result<Box<dyn Iterator<Item = Result<(PathBuf, Kind, TreeEntry), Error>>>, Error> {
811        Python::attach(|py| {
812            struct IterChildEntriesIter(pyo3::Py<PyAny>);
813
814            impl Iterator for IterChildEntriesIter {
815                type Item = Result<(PathBuf, Kind, TreeEntry), Error>;
816
817                fn next(&mut self) -> Option<Self::Item> {
818                    Python::attach(|py| {
819                        let next = match self.0.call_method0(py, intern!(py, "__next__")) {
820                            Ok(v) => v,
821                            Err(e) => {
822                                if e.is_instance_of::<pyo3::exceptions::PyStopIteration>(py) {
823                                    return None;
824                                }
825                                return Some(Err(e.into()));
826                            }
827                        };
828
829                        if next.is_none(py) {
830                            None
831                        } else {
832                            Some(next.extract(py).map_err(Into::into))
833                        }
834                    })
835                }
836            }
837
838            let path_str = path.to_string_lossy().to_string();
839            Ok(
840                Box::new(IterChildEntriesIter(self.to_object(py).call_method1(
841                    py,
842                    "iter_child_entries",
843                    (path_str,),
844                )?))
845                    as Box<dyn Iterator<Item = Result<(PathBuf, Kind, TreeEntry), Error>>>,
846            )
847        })
848    }
849
850    fn get_file_size(&self, path: &Path) -> Result<u64, Error> {
851        Python::attach(|py| {
852            let path_str = path.to_string_lossy().to_string();
853            let size = self
854                .to_object(py)
855                .call_method1(py, "get_file_size", (path_str,))?;
856            size.extract(py).map_err(Into::into)
857        })
858    }
859
860    fn get_file_sha1(
861        &self,
862        path: &Path,
863        _stat_value: Option<&std::fs::Metadata>,
864    ) -> Result<String, Error> {
865        Python::attach(|py| {
866            let path_str = path.to_string_lossy().to_string();
867            let sha1 = self
868                .to_object(py)
869                .call_method1(py, "get_file_sha1", (path_str,))?;
870            sha1.extract(py).map_err(Into::into)
871        })
872    }
873
874    fn get_file_mtime(&self, path: &Path) -> Result<u64, Error> {
875        Python::attach(|py| {
876            let path_str = path.to_string_lossy().to_string();
877            let mtime = self
878                .to_object(py)
879                .call_method1(py, "get_file_mtime", (path_str,))?;
880            mtime.extract(py).map_err(Into::into)
881        })
882    }
883
884    fn is_executable(&self, path: &Path) -> Result<bool, Error> {
885        Python::attach(|py| {
886            let path_str = path.to_string_lossy().to_string();
887            let result = self
888                .to_object(py)
889                .call_method1(py, "is_executable", (path_str,))?;
890            result.extract(py).map_err(Into::into)
891        })
892    }
893
894    fn stored_kind(&self, path: &Path) -> Result<Kind, Error> {
895        Python::attach(|py| {
896            let path_str = path.to_string_lossy().to_string();
897            self.to_object(py)
898                .call_method1(py, "stored_kind", (path_str,))?
899                .extract(py)
900                .map_err(Into::into)
901        })
902    }
903
904    fn supports_content_filtering(&self) -> bool {
905        Python::attach(|py| {
906            self.to_object(py)
907                .call_method0(py, "supports_content_filtering")
908                .unwrap()
909                .extract(py)
910                .unwrap()
911        })
912    }
913
914    fn supports_file_ids(&self) -> bool {
915        Python::attach(|py| {
916            self.to_object(py)
917                .call_method0(py, "supports_file_ids")
918                .unwrap()
919                .extract(py)
920                .unwrap()
921        })
922    }
923
924    fn supports_rename_tracking(&self) -> bool {
925        Python::attach(|py| {
926            self.to_object(py)
927                .call_method0(py, "supports_rename_tracking")
928                .unwrap()
929                .extract(py)
930                .unwrap()
931        })
932    }
933
934    fn supports_symlinks(&self) -> bool {
935        Python::attach(|py| {
936            self.to_object(py)
937                .call_method0(py, "supports_symlinks")
938                .unwrap()
939                .extract(py)
940                .unwrap()
941        })
942    }
943
944    fn supports_tree_reference(&self) -> bool {
945        Python::attach(|py| {
946            self.to_object(py)
947                .call_method0(py, "supports_tree_reference")
948                .unwrap()
949                .extract(py)
950                .unwrap()
951        })
952    }
953
954    fn unknowns(&self) -> Result<Vec<PathBuf>, Error> {
955        Python::attach(|py| {
956            let unknowns = self.to_object(py).call_method0(py, "unknowns")?;
957            unknowns.extract(py).map_err(Into::into)
958        })
959    }
960
961    fn all_versioned_paths(
962        &self,
963    ) -> Result<Box<dyn Iterator<Item = Result<PathBuf, Error>>>, Error> {
964        Python::attach(|py| {
965            struct AllVersionedPathsIter(pyo3::Py<PyAny>);
966
967            impl Iterator for AllVersionedPathsIter {
968                type Item = Result<PathBuf, Error>;
969
970                fn next(&mut self) -> Option<Self::Item> {
971                    Python::attach(|py| {
972                        let next = match self.0.call_method0(py, intern!(py, "__next__")) {
973                            Ok(v) => v,
974                            Err(e) => {
975                                if e.is_instance_of::<pyo3::exceptions::PyStopIteration>(py) {
976                                    return None;
977                                }
978                                return Some(Err(e.into()));
979                            }
980                        };
981
982                        if next.is_none(py) {
983                            None
984                        } else {
985                            Some(next.extract(py).map_err(Into::into))
986                        }
987                    })
988                }
989            }
990
991            Ok(Box::new(AllVersionedPathsIter(
992                self.to_object(py).call_method0(py, "all_versioned_paths")?,
993            ))
994                as Box<dyn Iterator<Item = Result<PathBuf, Error>>>)
995        })
996    }
997
998    fn conflicts(&self) -> Result<Vec<Conflict>, Error> {
999        Python::attach(|py| {
1000            let conflicts = self.to_object(py).call_method0(py, "conflicts")?;
1001            conflicts.extract(py).map_err(Into::into)
1002        })
1003    }
1004
1005    fn extras(&self) -> Result<Vec<PathBuf>, Error> {
1006        Python::attach(|py| {
1007            let extras = self.to_object(py).call_method0(py, "extras")?;
1008            extras.extract(py).map_err(Into::into)
1009        })
1010    }
1011
1012    fn filter_unversioned_files(&self, paths: &[&Path]) -> Result<Vec<PathBuf>, Error> {
1013        Python::attach(|py| {
1014            let path_strings: Vec<String> = paths
1015                .iter()
1016                .map(|p| p.to_string_lossy().to_string())
1017                .collect();
1018            let result =
1019                self.to_object(py)
1020                    .call_method1(py, "filter_unversioned_files", (path_strings,))?;
1021            result.extract(py).map_err(Into::into)
1022        })
1023    }
1024
1025    fn walkdirs(
1026        &self,
1027        prefix: Option<&Path>,
1028    ) -> Result<Box<dyn Iterator<Item = Result<WalkdirResult, Error>>>, Error> {
1029        Python::attach(|py| {
1030            // Entry type: (relpath, basename, kind, lstat, versioned_kind)
1031            // versioned_kind is Some(kind_string) if versioned, None if unversioned
1032            type EntryTuple = (String, String, String, Option<Py<PyAny>>, Option<String>);
1033
1034            // Helper function to process an entry tuple into a WalkdirResult
1035            fn process_entry(entry: &EntryTuple) -> Result<WalkdirResult, Error> {
1036                let kind = entry.2.parse().map_err(|_| {
1037                    Error::Other(Python::attach(|_py| {
1038                        PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
1039                            "Invalid kind: {}",
1040                            entry.2
1041                        ))
1042                    }))
1043                })?;
1044
1045                // File is versioned if versioned_kind is Some
1046                let versioned = entry.4.is_some();
1047
1048                // Convert stat if present
1049                let stat = if let Some(ref py_stat) = entry.3 {
1050                    convert_python_stat_to_metadata(py_stat)?
1051                } else {
1052                    None
1053                };
1054
1055                Ok(WalkdirResult {
1056                    relpath: PathBuf::from(&entry.0),
1057                    kind,
1058                    stat,
1059                    versioned,
1060                })
1061            }
1062
1063            struct WalkdirsIter {
1064                py_iter: pyo3::Py<PyAny>,
1065                current_entries: Vec<EntryTuple>,
1066                current_index: usize,
1067            }
1068
1069            impl Iterator for WalkdirsIter {
1070                type Item = Result<WalkdirResult, Error>;
1071
1072                fn next(&mut self) -> Option<Self::Item> {
1073                    Python::attach(|py| {
1074                        // If we have entries from the current directory, return them
1075                        if self.current_index < self.current_entries.len() {
1076                            let entry = &self.current_entries[self.current_index];
1077                            self.current_index += 1;
1078                            return Some(process_entry(entry));
1079                        }
1080
1081                        // Get the next directory from Python
1082                        loop {
1083                            let next = match self.py_iter.call_method0(py, intern!(py, "__next__"))
1084                            {
1085                                Ok(v) => v,
1086                                Err(e) => {
1087                                    if e.is_instance_of::<pyo3::exceptions::PyStopIteration>(py) {
1088                                        return None;
1089                                    }
1090                                    return Some(Err(e.into()));
1091                                }
1092                            };
1093
1094                            if next.is_none(py) {
1095                                return None;
1096                            }
1097
1098                            // Python walkdirs returns: (directory_relpath, [(relpath, basename, kind, lstat, versioned_kind), ...])
1099                            let tuple: (String, Vec<EntryTuple>) = match next.extract(py) {
1100                                Ok(t) => t,
1101                                Err(e) => return Some(Err(e.into())),
1102                            };
1103
1104                            self.current_entries = tuple.1;
1105                            self.current_index = 0;
1106
1107                            // If this directory has entries, return the first one
1108                            if !self.current_entries.is_empty() {
1109                                let entry = &self.current_entries[0];
1110                                self.current_index = 1;
1111                                return Some(process_entry(entry));
1112                            }
1113                            // If directory is empty, continue to next directory
1114                        }
1115                    })
1116                }
1117            }
1118
1119            // Convert prefix to string, using "" if None to avoid Python TypeError
1120            let prefix_str = match prefix {
1121                Some(p) => p.to_string_lossy().to_string(),
1122                None => "".to_string(),
1123            };
1124            let py_iter = self
1125                .to_object(py)
1126                .call_method1(py, "walkdirs", (prefix_str,))?;
1127
1128            Ok(Box::new(WalkdirsIter {
1129                py_iter,
1130                current_entries: Vec::new(),
1131                current_index: 0,
1132            })
1133                as Box<dyn Iterator<Item = Result<WalkdirResult, Error>>>)
1134        })
1135    }
1136
1137    fn versionable_kind(&self, kind: &Kind) -> bool {
1138        Python::attach(|py| {
1139            self.to_object(py)
1140                .call_method1(py, "versionable_kind", (kind.clone(),))
1141                .unwrap()
1142                .extract(py)
1143                .unwrap()
1144        })
1145    }
1146
1147    fn path_content_summary(&self, path: &Path) -> Result<PathContentSummary, Error> {
1148        Python::attach(|py| {
1149            let path_str = path.to_string_lossy().to_string();
1150            let summary =
1151                self.to_object(py)
1152                    .call_method1(py, "path_content_summary", (path_str,))?;
1153
1154            let summary_bound = summary.bind(py);
1155            let kind: String = summary_bound.get_item("kind")?.extract()?;
1156            let size: Option<u64> = summary_bound
1157                .get_item("size")
1158                .ok()
1159                .map(|v| v.extract().expect("size should be u64"));
1160            let executable: Option<bool> = summary_bound
1161                .get_item("executable")
1162                .ok()
1163                .map(|v| v.extract().expect("executable should be bool"));
1164            let sha1: Option<String> = summary_bound
1165                .get_item("sha1")
1166                .ok()
1167                .map(|v| v.extract().expect("sha1 should be string"));
1168            let target: Option<String> = summary_bound
1169                .get_item("target")
1170                .ok()
1171                .map(|v| v.extract().expect("target should be string"));
1172
1173            Ok(PathContentSummary {
1174                kind: kind.parse().unwrap(),
1175                size,
1176                executable,
1177                sha1,
1178                target,
1179            })
1180        })
1181    }
1182
1183    fn iter_files_bytes(
1184        &self,
1185        paths: &[&Path],
1186    ) -> Result<Box<dyn Iterator<Item = Result<(PathBuf, Vec<u8>), Error>>>, Error> {
1187        Python::attach(|py| {
1188            struct IterFilesBytesIter(pyo3::Py<PyAny>);
1189
1190            impl Iterator for IterFilesBytesIter {
1191                type Item = Result<(PathBuf, Vec<u8>), Error>;
1192
1193                fn next(&mut self) -> Option<Self::Item> {
1194                    Python::attach(|py| {
1195                        let next = match self.0.call_method0(py, intern!(py, "__next__")) {
1196                            Ok(v) => v,
1197                            Err(e) => {
1198                                if e.is_instance_of::<pyo3::exceptions::PyStopIteration>(py) {
1199                                    return None;
1200                                }
1201                                return Some(Err(e.into()));
1202                            }
1203                        };
1204
1205                        if next.is_none(py) {
1206                            None
1207                        } else {
1208                            Some(next.extract(py).map_err(Into::into))
1209                        }
1210                    })
1211                }
1212            }
1213
1214            let path_strings: Vec<String> = paths
1215                .iter()
1216                .map(|p| p.to_string_lossy().to_string())
1217                .collect();
1218            Ok(Box::new(IterFilesBytesIter(self.to_object(py).call_method1(
1219                py,
1220                "iter_files_bytes",
1221                (path_strings,),
1222            )?))
1223                as Box<
1224                    dyn Iterator<Item = Result<(PathBuf, Vec<u8>), Error>>,
1225                >)
1226        })
1227    }
1228
1229    fn iter_entries_by_dir(
1230        &self,
1231        specific_files: Option<&[&Path]>,
1232    ) -> Result<Box<dyn Iterator<Item = Result<(PathBuf, TreeEntry), Error>>>, Error> {
1233        Python::attach(|py| {
1234            struct IterEntriesByDirIter(pyo3::Py<PyAny>);
1235
1236            impl Iterator for IterEntriesByDirIter {
1237                type Item = Result<(PathBuf, TreeEntry), Error>;
1238
1239                fn next(&mut self) -> Option<Self::Item> {
1240                    Python::attach(|py| {
1241                        let next = match self.0.call_method0(py, intern!(py, "__next__")) {
1242                            Ok(v) => v,
1243                            Err(e) => {
1244                                if e.is_instance_of::<pyo3::exceptions::PyStopIteration>(py) {
1245                                    return None;
1246                                }
1247                                return Some(Err(e.into()));
1248                            }
1249                        };
1250
1251                        if next.is_none(py) {
1252                            None
1253                        } else {
1254                            Some(next.extract(py).map_err(Into::into))
1255                        }
1256                    })
1257                }
1258            }
1259
1260            let kwargs = pyo3::types::PyDict::new(py);
1261            if let Some(specific_files) = specific_files {
1262                let path_strings: Vec<String> = specific_files
1263                    .iter()
1264                    .map(|p| p.to_string_lossy().to_string())
1265                    .collect();
1266                kwargs.set_item("specific_files", path_strings)?;
1267            }
1268
1269            Ok(
1270                Box::new(IterEntriesByDirIter(self.to_object(py).call_method(
1271                    py,
1272                    "iter_entries_by_dir",
1273                    (),
1274                    Some(&kwargs),
1275                )?))
1276                    as Box<dyn Iterator<Item = Result<(PathBuf, TreeEntry), Error>>>,
1277            )
1278        })
1279    }
1280
1281    fn get_file_verifier(
1282        &self,
1283        path: &Path,
1284        _stat_value: Option<&std::fs::Metadata>,
1285    ) -> Result<(String, Vec<u8>), Error> {
1286        Python::attach(|py| {
1287            let path_str = path.to_string_lossy().to_string();
1288            let result = self
1289                .to_object(py)
1290                .call_method1(py, "get_file_verifier", (path_str,))?;
1291            result.extract(py).map_err(Into::into)
1292        })
1293    }
1294
1295    fn get_reference_revision(&self, path: &Path) -> Result<RevisionId, Error> {
1296        Python::attach(|py| {
1297            let path_str = path.to_string_lossy().to_string();
1298            let rev = self
1299                .to_object(py)
1300                .call_method1(py, "get_reference_revision", (path_str,))?;
1301            rev.extract(py).map_err(Into::into)
1302        })
1303    }
1304
1305    fn archive(
1306        &self,
1307        format: &str,
1308        name: &str,
1309        root: Option<&str>,
1310        subdir: Option<&Path>,
1311        force_mtime: Option<f64>,
1312        recurse_nested: bool,
1313    ) -> Result<Box<dyn Iterator<Item = Result<Vec<u8>, Error>>>, Error> {
1314        Python::attach(|py| {
1315            struct ArchiveIter(pyo3::Py<PyAny>);
1316
1317            impl Iterator for ArchiveIter {
1318                type Item = Result<Vec<u8>, Error>;
1319
1320                fn next(&mut self) -> Option<Self::Item> {
1321                    Python::attach(|py| {
1322                        let next = match self.0.call_method0(py, intern!(py, "__next__")) {
1323                            Ok(v) => v,
1324                            Err(e) => {
1325                                if e.is_instance_of::<pyo3::exceptions::PyStopIteration>(py) {
1326                                    return None;
1327                                }
1328                                return Some(Err(e.into()));
1329                            }
1330                        };
1331
1332                        if next.is_none(py) {
1333                            None
1334                        } else {
1335                            Some(next.extract(py).map_err(Into::into))
1336                        }
1337                    })
1338                }
1339            }
1340
1341            let kwargs = pyo3::types::PyDict::new(py);
1342            kwargs.set_item("format", format)?;
1343            kwargs.set_item("name", name)?;
1344            if let Some(root) = root {
1345                kwargs.set_item("root", root)?;
1346            }
1347            if let Some(subdir) = subdir {
1348                kwargs.set_item("subdir", subdir.to_string_lossy().to_string())?;
1349            }
1350            if let Some(force_mtime) = force_mtime {
1351                kwargs.set_item("force_mtime", force_mtime)?;
1352            }
1353            kwargs.set_item("recurse_nested", recurse_nested)?;
1354
1355            Ok(Box::new(ArchiveIter(self.to_object(py).call_method(
1356                py,
1357                "archive",
1358                (),
1359                Some(&kwargs),
1360            )?))
1361                as Box<dyn Iterator<Item = Result<Vec<u8>, Error>>>)
1362        })
1363    }
1364
1365    fn annotate_iter(
1366        &self,
1367        path: &Path,
1368        default_revision: Option<&RevisionId>,
1369    ) -> Result<Box<dyn Iterator<Item = Result<(RevisionId, Vec<u8>), Error>>>, Error> {
1370        Python::attach(|py| {
1371            struct AnnotateIter(pyo3::Py<PyAny>);
1372
1373            impl Iterator for AnnotateIter {
1374                type Item = Result<(RevisionId, Vec<u8>), Error>;
1375
1376                fn next(&mut self) -> Option<Self::Item> {
1377                    Python::attach(|py| {
1378                        let next = match self.0.call_method0(py, intern!(py, "__next__")) {
1379                            Ok(v) => v,
1380                            Err(e) => {
1381                                if e.is_instance_of::<pyo3::exceptions::PyStopIteration>(py) {
1382                                    return None;
1383                                }
1384                                return Some(Err(e.into()));
1385                            }
1386                        };
1387
1388                        if next.is_none(py) {
1389                            None
1390                        } else {
1391                            Some(next.extract(py).map_err(Into::into))
1392                        }
1393                    })
1394                }
1395            }
1396
1397            let path_str = path.to_string_lossy().to_string();
1398            let kwargs = pyo3::types::PyDict::new(py);
1399            if let Some(default_revision) = default_revision {
1400                kwargs.set_item(
1401                    "default_revision",
1402                    default_revision.clone().into_pyobject(py).unwrap(),
1403                )?;
1404            }
1405
1406            Ok(Box::new(AnnotateIter(self.to_object(py).call_method(
1407                py,
1408                "annotate_iter",
1409                (path_str,),
1410                Some(&kwargs),
1411            )?))
1412                as Box<
1413                    dyn Iterator<Item = Result<(RevisionId, Vec<u8>), Error>>,
1414                >)
1415        })
1416    }
1417
1418    fn is_special_path(&self, path: &Path) -> bool {
1419        Python::attach(|py| {
1420            let path_str = path.to_string_lossy().to_string();
1421            self.to_object(py)
1422                .call_method1(py, "is_special_path", (path_str,))
1423                .unwrap()
1424                .extract(py)
1425                .unwrap()
1426        })
1427    }
1428
1429    fn iter_search_rules(
1430        &self,
1431        paths: &[&Path],
1432    ) -> Result<Box<dyn Iterator<Item = Result<SearchRule, Error>>>, Error> {
1433        Python::attach(|py| {
1434            struct IterSearchRulesIter(pyo3::Py<PyAny>);
1435
1436            impl Iterator for IterSearchRulesIter {
1437                type Item = Result<SearchRule, Error>;
1438
1439                fn next(&mut self) -> Option<Self::Item> {
1440                    Python::attach(|py| {
1441                        let next = match self.0.call_method0(py, intern!(py, "__next__")) {
1442                            Ok(v) => v,
1443                            Err(e) => {
1444                                if e.is_instance_of::<pyo3::exceptions::PyStopIteration>(py) {
1445                                    return None;
1446                                }
1447                                return Some(Err(e.into()));
1448                            }
1449                        };
1450
1451                        if next.is_none(py) {
1452                            None
1453                        } else {
1454                            let tuple = match next.extract::<(String, String)>(py) {
1455                                Ok(t) => t,
1456                                Err(e) => return Some(Err(e.into())),
1457                            };
1458
1459                            let rule_type = match tuple.1.as_str() {
1460                                "include" => SearchRuleType::Include,
1461                                "exclude" => SearchRuleType::Exclude,
1462                                _ => {
1463                                    return Some(Err(Error::Other(PyErr::new::<
1464                                        pyo3::exceptions::PyValueError,
1465                                        _,
1466                                    >(
1467                                        "Unknown search rule type"
1468                                    ))))
1469                                }
1470                            };
1471
1472                            Some(Ok(SearchRule {
1473                                pattern: tuple.0,
1474                                rule_type,
1475                            }))
1476                        }
1477                    })
1478                }
1479            }
1480
1481            let path_strings: Vec<String> = paths
1482                .iter()
1483                .map(|p| p.to_string_lossy().to_string())
1484                .collect();
1485            Ok(
1486                Box::new(IterSearchRulesIter(self.to_object(py).call_method1(
1487                    py,
1488                    "iter_search_rules",
1489                    (path_strings,),
1490                )?)) as Box<dyn Iterator<Item = Result<SearchRule, Error>>>,
1491            )
1492        })
1493    }
1494}
1495
1496/// A generic tree implementation that wraps any Python tree object.
1497pub struct GenericTree(Py<PyAny>);
1498
1499impl<'py> IntoPyObject<'py> for GenericTree {
1500    type Target = PyAny;
1501    type Output = Bound<'py, Self::Target>;
1502    type Error = std::convert::Infallible;
1503
1504    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
1505        Ok(self.0.into_bound(py))
1506    }
1507}
1508
1509impl From<Py<PyAny>> for GenericTree {
1510    fn from(obj: Py<PyAny>) -> Self {
1511        GenericTree(obj)
1512    }
1513}
1514
1515impl PyTree for GenericTree {
1516    fn to_object(&self, py: Python) -> Py<PyAny> {
1517        self.0.clone_ref(py)
1518    }
1519}
1520
1521/// Trait for trees that support modification operations.
1522pub trait MutableTree: Tree {
1523    /// Add specified files to version control.
1524    fn add(&self, files: &[&Path]) -> Result<(), Error>;
1525    /// Lock the tree for write operations.
1526    fn lock_write(&self) -> Result<Lock, Error>;
1527    /// Write bytes to a file in the tree without atomic guarantees.
1528    fn put_file_bytes_non_atomic(&self, path: &Path, data: &[u8]) -> Result<(), Error>;
1529    /// Check if the tree has any uncommitted changes.
1530    fn has_changes(&self) -> std::result::Result<bool, Error>;
1531    /// Create a directory in the tree.
1532    fn mkdir(&self, path: &Path) -> Result<(), Error>;
1533    /// Remove specified files from version control and from the filesystem.
1534    fn remove(&self, files: &[&std::path::Path]) -> Result<(), Error>;
1535
1536    /// Add a tree reference.
1537    fn add_reference(&self, reference: &TreeReference) -> Result<(), Error>;
1538
1539    /// Copy a file or directory to a new location.
1540    fn copy_one(&self, from_path: &Path, to_path: &Path) -> Result<(), Error>;
1541
1542    /// Get the last revision ID.
1543    fn last_revision(&self) -> Result<RevisionId, Error>;
1544
1545    /// Lock the tree for write operations.
1546    fn lock_tree_write(&self) -> Result<Lock, Error>;
1547
1548    /// Set the parent IDs for this tree.
1549    fn set_parent_ids(&self, parent_ids: &[RevisionId]) -> Result<(), Error>;
1550
1551    /// Set the parent trees for this tree.
1552    fn set_parent_trees(&self, parent_trees: &[(RevisionId, RevisionTree)]) -> Result<(), Error>;
1553
1554    /// Apply a delta to the tree.
1555    fn apply_inventory_delta(&self, delta: Vec<InventoryDelta>) -> Result<(), Error>;
1556
1557    /// Commit changes in the tree.
1558    fn commit(
1559        &self,
1560        message: &str,
1561        committer: Option<&str>,
1562        timestamp: Option<f64>,
1563        allow_pointless: Option<bool>,
1564        specific_files: Option<&[&Path]>,
1565    ) -> Result<RevisionId, Error>;
1566}
1567
1568/// A tree that can be modified.
1569pub trait PyMutableTree: PyTree + MutableTree {}
1570
1571impl dyn PyMutableTree {
1572    /// Get a reference to self as a MutableTree trait object.
1573    pub fn as_mutable_tree(&self) -> &dyn MutableTree {
1574        self
1575    }
1576}
1577
1578impl<T: PyMutableTree + ?Sized> MutableTree for T {
1579    fn add(&self, files: &[&Path]) -> Result<(), Error> {
1580        for f in files {
1581            assert!(f.is_relative());
1582        }
1583        Python::attach(|py| -> Result<(), PyErr> {
1584            let path_strings: Vec<String> = files
1585                .iter()
1586                .map(|p| p.to_string_lossy().to_string())
1587                .collect();
1588            self.to_object(py)
1589                .call_method1(py, "add", (path_strings,))?;
1590            Ok(())
1591        })
1592        .map_err(Into::into)
1593    }
1594
1595    fn lock_write(&self) -> Result<Lock, Error> {
1596        Python::attach(|py| {
1597            let lock = self
1598                .to_object(py)
1599                .call_method0(py, intern!(py, "lock_write"))?;
1600            Ok(Lock::from(lock))
1601        })
1602    }
1603
1604    fn put_file_bytes_non_atomic(&self, path: &Path, data: &[u8]) -> Result<(), Error> {
1605        assert!(path.is_relative());
1606        Python::attach(|py| {
1607            let path_str = path.to_string_lossy().to_string();
1608            self.to_object(py)
1609                .call_method1(py, "put_file_bytes_non_atomic", (path_str, data))?;
1610            Ok(())
1611        })
1612    }
1613
1614    fn has_changes(&self) -> std::result::Result<bool, Error> {
1615        Python::attach(|py| {
1616            self.to_object(py)
1617                .call_method0(py, "has_changes")?
1618                .extract::<bool>(py)
1619                .map_err(Into::into)
1620        })
1621    }
1622
1623    fn mkdir(&self, path: &Path) -> Result<(), Error> {
1624        assert!(path.is_relative());
1625        Python::attach(|py| -> Result<(), PyErr> {
1626            let path_str = path.to_string_lossy().to_string();
1627            self.to_object(py).call_method1(py, "mkdir", (path_str,))?;
1628            Ok(())
1629        })
1630        .map_err(Into::into)
1631    }
1632
1633    fn remove(&self, files: &[&std::path::Path]) -> Result<(), Error> {
1634        for f in files {
1635            assert!(f.is_relative());
1636        }
1637        Python::attach(|py| -> Result<(), PyErr> {
1638            let path_strings: Vec<String> = files
1639                .iter()
1640                .map(|p| p.to_string_lossy().to_string())
1641                .collect();
1642            self.to_object(py)
1643                .call_method1(py, "remove", (path_strings,))?;
1644            Ok(())
1645        })
1646        .map_err(Into::into)
1647    }
1648
1649    fn add_reference(&self, reference: &TreeReference) -> Result<(), Error> {
1650        Python::attach(|py| {
1651            let kwargs = pyo3::types::PyDict::new(py);
1652            kwargs.set_item("path", reference.path.to_string_lossy().to_string())?;
1653            kwargs.set_item("kind", reference.kind.clone())?;
1654            if let Some(ref rev) = reference.reference_revision {
1655                kwargs.set_item("reference_revision", rev.clone().into_pyobject(py).unwrap())?;
1656            }
1657            self.to_object(py)
1658                .call_method(py, "add_reference", (), Some(&kwargs))?;
1659            Ok(())
1660        })
1661    }
1662
1663    fn copy_one(&self, from_path: &Path, to_path: &Path) -> Result<(), Error> {
1664        assert!(from_path.is_relative());
1665        assert!(to_path.is_relative());
1666        Python::attach(|py| {
1667            let from_str = from_path.to_string_lossy().to_string();
1668            let to_str = to_path.to_string_lossy().to_string();
1669            self.to_object(py)
1670                .call_method1(py, "copy_one", (from_str, to_str))?;
1671            Ok(())
1672        })
1673    }
1674
1675    fn last_revision(&self) -> Result<RevisionId, Error> {
1676        Python::attach(|py| {
1677            let last_revision = self
1678                .to_object(py)
1679                .call_method0(py, intern!(py, "last_revision"))?;
1680            Ok(RevisionId::from(last_revision.extract::<Vec<u8>>(py)?))
1681        })
1682    }
1683
1684    fn lock_tree_write(&self) -> Result<Lock, Error> {
1685        Python::attach(|py| {
1686            let lock = self.to_object(py).call_method0(py, "lock_tree_write")?;
1687            Ok(Lock::from(lock))
1688        })
1689    }
1690
1691    fn set_parent_ids(&self, parent_ids: &[RevisionId]) -> Result<(), Error> {
1692        Python::attach(|py| {
1693            let parent_ids_py: Vec<Py<PyAny>> = parent_ids
1694                .iter()
1695                .map(|id| id.clone().into_pyobject(py).unwrap().unbind())
1696                .collect();
1697            self.to_object(py)
1698                .call_method1(py, "set_parent_ids", (parent_ids_py,))?;
1699            Ok(())
1700        })
1701    }
1702
1703    fn set_parent_trees(&self, parent_trees: &[(RevisionId, RevisionTree)]) -> Result<(), Error> {
1704        Python::attach(|py| {
1705            let parent_trees_py: Vec<(Py<PyAny>, Py<PyAny>)> = parent_trees
1706                .iter()
1707                .map(|(id, tree)| {
1708                    (
1709                        id.clone().into_pyobject(py).unwrap().unbind(),
1710                        tree.to_object(py),
1711                    )
1712                })
1713                .collect();
1714            self.to_object(py)
1715                .call_method1(py, "set_parent_trees", (parent_trees_py,))?;
1716            Ok(())
1717        })
1718    }
1719
1720    fn apply_inventory_delta(&self, delta: Vec<InventoryDelta>) -> Result<(), Error> {
1721        Python::attach(|py| {
1722            let delta_py: Vec<Py<PyAny>> = delta
1723                .into_iter()
1724                .map(|d| {
1725                    let tuple = pyo3::types::PyTuple::new(
1726                        py,
1727                        vec![
1728                            d.old_path
1729                                .map(|p| p.to_string_lossy().to_string())
1730                                .into_pyobject(py)
1731                                .unwrap()
1732                                .into_any(),
1733                            d.new_path
1734                                .map(|p| p.to_string_lossy().to_string())
1735                                .into_pyobject(py)
1736                                .unwrap()
1737                                .into_any(),
1738                            d.file_id.into_pyobject(py).unwrap().into_any(),
1739                            d.entry.into_pyobject(py).unwrap().into_any(),
1740                        ],
1741                    )
1742                    .unwrap();
1743                    tuple.into_any().unbind()
1744                })
1745                .collect();
1746            self.to_object(py)
1747                .call_method1(py, "apply_inventory_delta", (delta_py,))?;
1748            Ok(())
1749        })
1750    }
1751
1752    fn commit(
1753        &self,
1754        message: &str,
1755        committer: Option<&str>,
1756        timestamp: Option<f64>,
1757        allow_pointless: Option<bool>,
1758        specific_files: Option<&[&Path]>,
1759    ) -> Result<RevisionId, Error> {
1760        Python::attach(|py| {
1761            let kwargs = pyo3::types::PyDict::new(py);
1762            if let Some(committer) = committer {
1763                kwargs.set_item("committer", committer)?;
1764            }
1765            if let Some(timestamp) = timestamp {
1766                kwargs.set_item("timestamp", timestamp)?;
1767            }
1768            if let Some(allow_pointless) = allow_pointless {
1769                kwargs.set_item("allow_pointless", allow_pointless)?;
1770            }
1771            if let Some(specific_files) = specific_files {
1772                let file_paths: Vec<String> = specific_files
1773                    .iter()
1774                    .map(|p| p.to_string_lossy().to_string())
1775                    .collect();
1776                kwargs.set_item("specific_files", file_paths)?;
1777            }
1778            let result = self
1779                .to_object(py)
1780                .call_method(py, "commit", (message,), Some(&kwargs))?;
1781            result.extract(py).map_err(Into::into)
1782        })
1783    }
1784}
1785
1786/// A read-only tree at a specific revision.
1787pub struct RevisionTree(pub Py<PyAny>);
1788
1789impl<'py> IntoPyObject<'py> for RevisionTree {
1790    type Target = PyAny;
1791    type Output = Bound<'py, Self::Target>;
1792    type Error = std::convert::Infallible;
1793
1794    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
1795        Ok(self.0.into_bound(py))
1796    }
1797}
1798
1799impl PyTree for RevisionTree {
1800    fn to_object(&self, py: Python) -> Py<PyAny> {
1801        self.0.clone_ref(py)
1802    }
1803}
1804
1805impl Clone for RevisionTree {
1806    fn clone(&self) -> Self {
1807        Python::attach(|py| RevisionTree(self.0.clone_ref(py)))
1808    }
1809}
1810
1811impl RevisionTree {
1812    /// Get the repository this revision tree belongs to.
1813    pub fn repository(&self) -> crate::repository::GenericRepository {
1814        Python::attach(|py| {
1815            let repository = self.to_object(py).getattr(py, "_repository").unwrap();
1816            crate::repository::GenericRepository::new(repository)
1817        })
1818    }
1819
1820    /// Get the revision ID of this tree.
1821    pub fn get_revision_id(&self) -> RevisionId {
1822        Python::attach(|py| {
1823            self.to_object(py)
1824                .call_method0(py, "get_revision_id")
1825                .unwrap()
1826                .extract(py)
1827                .unwrap()
1828        })
1829    }
1830
1831    /// Get the parent revision IDs of this tree.
1832    pub fn get_parent_ids(&self) -> Vec<RevisionId> {
1833        Python::attach(|py| {
1834            self.to_object(py)
1835                .call_method0(py, intern!(py, "get_parent_ids"))
1836                .unwrap()
1837                .extract(py)
1838                .unwrap()
1839        })
1840    }
1841}
1842
1843#[derive(Debug, PartialEq, Eq, Clone)]
1844/// Represents a change to a file in a tree.
1845pub struct TreeChange {
1846    /// The path of the file, as (old_path, new_path).
1847    pub path: (Option<PathBuf>, Option<PathBuf>),
1848    /// Whether the content of the file changed.
1849    pub changed_content: bool,
1850    /// Whether the file is versioned, as (old_versioned, new_versioned).
1851    pub versioned: (Option<bool>, Option<bool>),
1852    /// The name of the file, as (old_name, new_name).
1853    pub name: (Option<std::ffi::OsString>, Option<std::ffi::OsString>),
1854    /// The kind of the file, as (old_kind, new_kind).
1855    pub kind: (Option<Kind>, Option<Kind>),
1856    /// Whether the file is executable, as (old_executable, new_executable).
1857    pub executable: (Option<bool>, Option<bool>),
1858    /// Whether the file was copied rather than just changed/renamed.
1859    pub copied: bool,
1860}
1861
1862impl<'py> IntoPyObject<'py> for TreeChange {
1863    type Target = PyAny;
1864    type Output = Bound<'py, Self::Target>;
1865    type Error = std::convert::Infallible;
1866
1867    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
1868        let dict = pyo3::types::PyDict::new(py);
1869        dict.set_item(
1870            "path",
1871            (
1872                self.path
1873                    .0
1874                    .as_ref()
1875                    .map(|p| p.to_string_lossy().to_string()),
1876                self.path
1877                    .1
1878                    .as_ref()
1879                    .map(|p| p.to_string_lossy().to_string()),
1880            ),
1881        )
1882        .unwrap();
1883        dict.set_item("changed_content", self.changed_content)
1884            .unwrap();
1885        dict.set_item("versioned", self.versioned).unwrap();
1886        dict.set_item("name", &self.name).unwrap();
1887        dict.set_item("kind", self.kind.clone()).unwrap();
1888        dict.set_item("executable", self.executable).unwrap();
1889        dict.set_item("copied", self.copied).unwrap();
1890        Ok(dict.into_any())
1891    }
1892}
1893
1894impl<'a, 'py> FromPyObject<'a, 'py> for TreeChange {
1895    type Error = PyErr;
1896
1897    fn extract(obj: Borrowed<'a, 'py, PyAny>) -> PyResult<Self> {
1898        fn from_bool(o: &Bound<PyAny>) -> PyResult<bool> {
1899            if let Ok(b) = o.extract::<isize>() {
1900                Ok(b != 0)
1901            } else {
1902                o.extract::<bool>()
1903            }
1904        }
1905
1906        fn from_opt_bool_tuple(o: &Bound<PyAny>) -> PyResult<(Option<bool>, Option<bool>)> {
1907            let tuple = o.extract::<(Option<Bound<PyAny>>, Option<Bound<PyAny>>)>()?;
1908            Ok((
1909                tuple.0.map(|o| from_bool(&o.as_borrowed())).transpose()?,
1910                tuple.1.map(|o| from_bool(&o.as_borrowed())).transpose()?,
1911            ))
1912        }
1913
1914        let path = obj.getattr("path")?;
1915        let changed_content = from_bool(&obj.getattr("changed_content")?)?;
1916
1917        let versioned = from_opt_bool_tuple(&obj.getattr("versioned")?)?;
1918        let name = obj.getattr("name")?;
1919        let kind = obj.getattr("kind")?;
1920        let executable = from_opt_bool_tuple(&obj.getattr("executable")?)?;
1921        let copied = obj.getattr("copied")?;
1922
1923        Ok(TreeChange {
1924            path: path.extract()?,
1925            changed_content,
1926            versioned,
1927            name: name.extract()?,
1928            kind: kind.extract()?,
1929            executable,
1930            copied: copied.extract()?,
1931        })
1932    }
1933}
1934
1935/// An in-memory tree implementation.
1936pub struct MemoryTree(pub Py<PyAny>);
1937
1938impl<'py> IntoPyObject<'py> for MemoryTree {
1939    type Target = PyAny;
1940    type Output = Bound<'py, Self::Target>;
1941    type Error = std::convert::Infallible;
1942
1943    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
1944        Ok(self.0.into_bound(py))
1945    }
1946}
1947
1948impl<B: crate::branch::PyBranch> From<&B> for MemoryTree {
1949    fn from(branch: &B) -> Self {
1950        Python::attach(|py| {
1951            MemoryTree(
1952                branch
1953                    .to_object(py)
1954                    .call_method0(py, "create_memorytree")
1955                    .unwrap()
1956                    .extract(py)
1957                    .unwrap(),
1958            )
1959        })
1960    }
1961}
1962
1963impl PyTree for MemoryTree {
1964    fn to_object(&self, py: Python) -> Py<PyAny> {
1965        self.0.clone_ref(py)
1966    }
1967}
1968
1969impl PyMutableTree for MemoryTree {}
1970
1971pub use crate::workingtree::WorkingTree;
1972
1973#[cfg(test)]
1974mod tests {
1975    use super::*;
1976    use crate::controldir::{create_standalone_workingtree, ControlDirFormat};
1977    use serial_test::serial;
1978
1979    #[test]
1980    #[serial]
1981    fn test_remove() {
1982        let env = crate::testing::TestEnv::new();
1983        let wt =
1984            create_standalone_workingtree(std::path::Path::new("."), &ControlDirFormat::default())
1985                .unwrap();
1986        let path = std::path::Path::new("foo");
1987        std::fs::write(&path, b"").unwrap();
1988        wt.add(&[(std::path::Path::new("foo"))]).unwrap();
1989        wt.build_commit()
1990            .message("Initial commit")
1991            .reporter(&crate::commit::NullCommitReporter::new())
1992            .commit()
1993            .unwrap();
1994        assert!(wt.has_filename(&path));
1995        wt.remove(&[Path::new("foo")]).unwrap();
1996        assert!(!wt.is_versioned(&path));
1997        std::mem::drop(env);
1998    }
1999
2000    #[test]
2001    #[serial]
2002    fn test_walkdirs() {
2003        let env = crate::testing::TestEnv::new();
2004        let wt =
2005            create_standalone_workingtree(std::path::Path::new("."), &ControlDirFormat::default())
2006                .unwrap();
2007
2008        // Create a directory structure
2009        std::fs::create_dir("subdir").unwrap();
2010        std::fs::write("file1.txt", b"content1").unwrap();
2011        std::fs::write("subdir/file2.txt", b"content2").unwrap();
2012        std::fs::write(".gitattributes", b"* text=auto\n").unwrap();
2013
2014        wt.add(&[
2015            Path::new("file1.txt"),
2016            Path::new("subdir"),
2017            Path::new("subdir/file2.txt"),
2018            Path::new(".gitattributes"),
2019        ])
2020        .unwrap();
2021
2022        wt.build_commit()
2023            .message("Add files")
2024            .reporter(&crate::commit::NullCommitReporter::new())
2025            .commit()
2026            .unwrap();
2027
2028        let lock = wt.lock_read().unwrap();
2029
2030        // Test walkdirs with no prefix
2031        let entries: Vec<_> = wt.walkdirs(None).unwrap().collect();
2032        assert!(entries.len() > 0, "Should have at least some entries");
2033
2034        // Check that we can find .gitattributes
2035        let found_gitattributes = entries.iter().any(|entry| {
2036            entry
2037                .as_ref()
2038                .map(|e| e.relpath.file_name() == Some(std::ffi::OsStr::new(".gitattributes")))
2039                .unwrap_or(false)
2040        });
2041        assert!(
2042            found_gitattributes,
2043            "Should find .gitattributes file. Found entries: {:?}",
2044            entries
2045                .iter()
2046                .filter_map(|e| e.as_ref().ok())
2047                .map(|e| &e.relpath)
2048                .collect::<Vec<_>>()
2049        );
2050
2051        // Check that we can find file in subdirectory
2052        let found_subdir_file = entries.iter().any(|entry| {
2053            entry
2054                .as_ref()
2055                .map(|e| e.relpath.to_str() == Some("subdir/file2.txt"))
2056                .unwrap_or(false)
2057        });
2058        assert!(found_subdir_file, "Should find file in subdirectory");
2059
2060        std::mem::drop(lock);
2061        std::mem::drop(env);
2062    }
2063}