breezyshim/
workingtree.rs

1//! Working trees in version control systems.
2//!
3//! This module provides functionality for working with working trees, which are
4//! local directories containing the files of a branch that can be edited.
5use crate::branch::{Branch, GenericBranch, PyBranch};
6use crate::controldir::{ControlDir, GenericControlDir};
7use crate::error::Error;
8use crate::tree::{MutableTree, PyMutableTree, PyTree, RevisionTree};
9use crate::RevisionId;
10use pyo3::prelude::*;
11use std::path::{Path, PathBuf};
12
13/// Trait representing a working tree in a version control system.
14///
15/// A working tree is a local directory containing the files of a branch that can
16/// be edited. This trait provides methods for interacting with working trees
17/// across various version control systems.
18pub trait WorkingTree: MutableTree {
19    /// Get the base directory path of this working tree.
20    ///
21    /// # Returns
22    ///
23    /// The absolute path to the root directory of this working tree.
24    fn basedir(&self) -> PathBuf;
25
26    /// Get the control directory for this working tree.
27    ///
28    /// # Returns
29    ///
30    /// The control directory containing this working tree.
31    fn controldir(
32        &self,
33    ) -> Box<
34        dyn ControlDir<
35            Branch = GenericBranch,
36            Repository = crate::repository::GenericRepository,
37            WorkingTree = GenericWorkingTree,
38        >,
39    >;
40
41    /// Get the branch associated with this working tree.
42    ///
43    /// # Returns
44    ///
45    /// The branch that this working tree is tracking.
46    fn branch(&self) -> GenericBranch;
47
48    /// Get the user-visible URL for this working tree.
49    ///
50    /// # Returns
51    ///
52    /// The URL that can be used to access this working tree.
53    fn get_user_url(&self) -> url::Url;
54
55    /// Check if this working tree supports setting the last revision.
56    ///
57    /// # Returns
58    ///
59    /// `true` if the working tree supports setting the last revision, `false` otherwise.
60    fn supports_setting_file_ids(&self) -> bool;
61
62    /// Add specified files to version control and the working tree.
63    ///
64    /// # Parameters
65    ///
66    /// * `files` - The list of file paths to add.
67    ///
68    /// # Returns
69    ///
70    /// `Ok(())` on success, or an error if the files could not be added.
71    fn smart_add(&self, files: &[&Path]) -> Result<(), Error>;
72
73    /// Update the working tree to a specific revision.
74    ///
75    /// # Parameters
76    ///
77    /// * `revision_id` - The revision to update to, or None for the latest.
78    ///
79    /// # Returns
80    ///
81    /// `Ok(())` on success, or an error if the update failed.
82    fn update(&self, revision_id: Option<&RevisionId>) -> Result<(), Error>;
83
84    /// Revert changes in the working tree.
85    ///
86    /// # Parameters
87    ///
88    /// * `filenames` - Optional list of specific files to revert.
89    ///
90    /// # Returns
91    ///
92    /// `Ok(())` on success, or an error if the revert failed.
93    fn revert(&self, filenames: Option<&[&Path]>) -> Result<(), Error>;
94
95    /// Create a commit builder for this working tree.
96    ///
97    /// # Returns
98    ///
99    /// A new CommitBuilder instance for this working tree.
100    fn build_commit(&self) -> CommitBuilder;
101
102    /// Get the basis tree for this working tree.
103    ///
104    /// # Returns
105    ///
106    /// The basis tree that this working tree is based on.
107    fn basis_tree(&self) -> Result<RevisionTree, Error>;
108
109    /// Check if a path is a control filename in this working tree.
110    ///
111    /// Control filenames are filenames that are used by the version control system
112    /// for its own purposes, like .git or .bzr.
113    ///
114    /// # Parameters
115    ///
116    /// * `path` - The path to check.
117    ///
118    /// # Returns
119    ///
120    /// `true` if the path is a control filename, `false` otherwise.
121    fn is_control_filename(&self, path: &Path) -> bool;
122
123    /// Get a revision tree for a specific revision.
124    ///
125    /// # Parameters
126    ///
127    /// * `revision_id` - The ID of the revision to get the tree for.
128    ///
129    /// # Returns
130    ///
131    /// The revision tree, or an error if it could not be retrieved.
132    fn revision_tree(&self, revision_id: &RevisionId) -> Result<Box<RevisionTree>, Error>;
133
134    /// Convert a path to an absolute path relative to the working tree.
135    ///
136    /// # Parameters
137    ///
138    /// * `path` - The path to convert.
139    ///
140    /// # Returns
141    ///
142    /// The absolute path, or an error if the conversion failed.
143    fn abspath(&self, path: &Path) -> Result<PathBuf, Error>;
144
145    /// Convert an absolute path to a path relative to the working tree.
146    ///
147    /// # Parameters
148    ///
149    /// * `path` - The absolute path to convert.
150    ///
151    /// # Returns
152    ///
153    /// The relative path, or an error if the conversion failed.
154    fn relpath(&self, path: &Path) -> Result<PathBuf, Error>;
155
156    /// Pull changes from another branch into this working tree.
157    ///
158    /// # Parameters
159    ///
160    /// * `source` - The branch to pull from.
161    /// * `overwrite` - Whether to overwrite diverged changes.
162    /// * `stop_revision` - The revision to stop pulling at.
163    /// * `local` - Whether to only pull locally accessible revisions.
164    ///
165    /// # Returns
166    ///
167    /// `Ok(())` on success, or an error if the pull could not be completed.
168    fn pull(
169        &self,
170        source: &dyn Branch,
171        overwrite: Option<bool>,
172        stop_revision: Option<&RevisionId>,
173        local: Option<bool>,
174    ) -> Result<(), Error>;
175
176    /// Merge changes from another branch into this working tree.
177    ///
178    /// # Parameters
179    ///
180    /// * `source` - The branch to merge from.
181    /// * `to_revision` - The revision to merge up to.
182    ///
183    /// # Returns
184    ///
185    /// `Ok(())` on success, or an error if the merge could not be completed.
186    fn merge_from_branch(
187        &self,
188        source: &dyn Branch,
189        to_revision: Option<&RevisionId>,
190    ) -> Result<(), Error>;
191
192    /// Convert a list of files to relative paths safely.
193    ///
194    /// This function takes a list of file paths and converts them to paths relative
195    /// to the working tree, with various safety checks.
196    ///
197    /// # Parameters
198    ///
199    /// * `file_list` - The list of file paths to convert.
200    /// * `canonicalize` - Whether to canonicalize the paths first.
201    /// * `apply_view` - Whether to apply the view (if any) to the paths.
202    ///
203    /// # Returns
204    ///
205    /// A list of converted paths, or an error if the conversion failed.
206    fn safe_relpath_files(
207        &self,
208        file_list: &[&Path],
209        canonicalize: bool,
210        apply_view: bool,
211    ) -> Result<Vec<PathBuf>, Error>;
212
213    /// Add conflicts to the working tree.
214    fn add_conflicts(&self, conflicts: &[crate::tree::Conflict]) -> Result<(), Error>;
215
216    /// Add a parent tree.
217    fn add_parent_tree(
218        &self,
219        parent_id: &RevisionId,
220        parent_tree: &crate::tree::RevisionTree,
221    ) -> Result<(), Error>;
222
223    /// Add a parent tree ID.
224    fn add_parent_tree_id(&self, parent_id: &RevisionId) -> Result<(), Error>;
225
226    /// Add a pending merge.
227    fn add_pending_merge(&self, revision_id: &RevisionId) -> Result<(), Error>;
228
229    /// Auto-resolve conflicts.
230    fn auto_resolve(&self) -> Result<(), Error>;
231
232    /// Check the state of the working tree.
233    fn check_state(&self) -> Result<(), Error>;
234
235    /// Get the canonical path for a file.
236    fn get_canonical_path(&self, path: &Path) -> Result<PathBuf, Error>;
237
238    /// Get canonical paths for multiple files.
239    fn get_canonical_paths(&self, paths: &[&Path]) -> Result<Vec<PathBuf>, Error>;
240
241    /// Get the configuration stack.
242    fn get_config_stack(&self) -> Result<PyObject, Error>;
243
244    /// Get reference information.
245    fn get_reference_info(&self, path: &Path) -> Result<Option<(String, PathBuf)>, Error>;
246
247    /// Get the shelf manager.
248    fn get_shelf_manager(&self) -> Result<PyObject, Error>;
249
250    /// Get ignored files.
251    fn ignored_files(&self) -> Result<Vec<PathBuf>, Error>;
252
253    /// Check if the working tree is locked.
254    fn is_locked(&self) -> bool;
255
256    /// Get merge-modified files.
257    fn merge_modified(&self) -> Result<Vec<PathBuf>, Error>;
258
259    /// Move files within the working tree.
260    fn move_files(&self, from_paths: &[&Path], to_dir: &Path) -> Result<(), Error>;
261
262    /// Set conflicts in the working tree.
263    fn set_conflicts(&self, conflicts: &[crate::tree::Conflict]) -> Result<(), Error>;
264
265    /// Set the last revision.
266    fn set_last_revision(&self, revision_id: &RevisionId) -> Result<(), Error>;
267
268    /// Set merge-modified files.
269    fn set_merge_modified(&self, files: &[&Path]) -> Result<(), Error>;
270
271    /// Set pending merges.
272    fn set_pending_merges(&self, revision_ids: &[RevisionId]) -> Result<(), Error>;
273
274    /// Set reference information.
275    fn set_reference_info(
276        &self,
277        path: &Path,
278        location: &str,
279        file_id: Option<&str>,
280    ) -> Result<(), Error>;
281
282    /// Subsume a tree into this working tree.
283    fn subsume(&self, other: &dyn PyWorkingTree) -> Result<(), Error>;
284
285    /// Store uncommitted changes.
286    fn store_uncommitted(&self) -> Result<String, Error>;
287
288    /// Restore uncommitted changes.
289    fn restore_uncommitted(&self) -> Result<(), Error>;
290
291    /// Extract the working tree to a directory.
292    fn extract(&self, dest: &Path, format: Option<&str>) -> Result<(), Error>;
293
294    /// Clone the working tree.
295    fn clone(
296        &self,
297        dest: &Path,
298        revision_id: Option<&RevisionId>,
299    ) -> Result<GenericWorkingTree, Error>;
300
301    /// Get a control transport.
302    fn control_transport(&self) -> Result<crate::transport::Transport, Error>;
303
304    /// Get the control URL.
305    fn control_url(&self) -> url::Url;
306
307    /// Copy content into this working tree.
308    fn copy_content_into(
309        &self,
310        source: &dyn PyTree,
311        revision_id: Option<&RevisionId>,
312    ) -> Result<(), Error>;
313
314    /// Flush any pending changes.
315    fn flush(&self) -> Result<(), Error>;
316
317    /// Check if the working tree requires a rich root.
318    fn requires_rich_root(&self) -> bool;
319
320    /// Reset the state of the working tree.
321    fn reset_state(&self, revision_ids: Option<&[RevisionId]>) -> Result<(), Error>;
322
323    /// Reference a parent tree.
324    fn reference_parent(
325        &self,
326        path: &Path,
327        branch: &dyn Branch,
328        revision_id: Option<&RevisionId>,
329    ) -> Result<(), Error>;
330
331    /// Check if the working tree supports merge-modified tracking.
332    fn supports_merge_modified(&self) -> bool;
333
334    /// Break the lock on the working tree.
335    fn break_lock(&self) -> Result<(), Error>;
336
337    /// Get the physical lock status.
338    fn get_physical_lock_status(&self) -> Result<bool, Error>;
339}
340
341/// Trait for working trees that wrap Python working tree objects.
342///
343/// This trait is implemented by working tree types that wrap Python working tree objects.
344pub trait PyWorkingTree: PyMutableTree + WorkingTree {}
345
346impl dyn PyWorkingTree {
347    /// Get a reference to self as a WorkingTree trait object.
348    pub fn as_working_tree(&self) -> &dyn WorkingTree {
349        self
350    }
351}
352
353impl<T: ?Sized + PyWorkingTree> WorkingTree for T {
354    fn basedir(&self) -> PathBuf {
355        Python::with_gil(|py| {
356            let path: String = self
357                .to_object(py)
358                .getattr(py, "basedir")
359                .unwrap()
360                .extract(py)
361                .unwrap();
362            PathBuf::from(path)
363        })
364    }
365
366    fn controldir(
367        &self,
368    ) -> Box<
369        dyn ControlDir<
370            Branch = GenericBranch,
371            Repository = crate::repository::GenericRepository,
372            WorkingTree = GenericWorkingTree,
373        >,
374    > {
375        Python::with_gil(|py| {
376            let controldir = self.to_object(py).getattr(py, "controldir").unwrap();
377            Box::new(GenericControlDir::new(controldir))
378                as Box<
379                    dyn ControlDir<
380                        Branch = GenericBranch,
381                        Repository = crate::repository::GenericRepository,
382                        WorkingTree = GenericWorkingTree,
383                    >,
384                >
385        })
386    }
387
388    fn branch(&self) -> GenericBranch {
389        Python::with_gil(|py| {
390            GenericBranch::from(self.to_object(py).getattr(py, "branch").unwrap())
391        })
392    }
393
394    fn get_user_url(&self) -> url::Url {
395        Python::with_gil(|py| {
396            let url: String = self
397                .to_object(py)
398                .getattr(py, "user_url")
399                .unwrap()
400                .extract(py)
401                .unwrap();
402            url.parse().unwrap()
403        })
404    }
405
406    fn supports_setting_file_ids(&self) -> bool {
407        Python::with_gil(|py| {
408            self.to_object(py)
409                .call_method0(py, "supports_setting_file_ids")
410                .unwrap()
411                .extract(py)
412                .unwrap()
413        })
414    }
415
416    fn smart_add(&self, files: &[&Path]) -> Result<(), Error> {
417        Python::with_gil(|py| {
418            let file_paths: Vec<String> = files
419                .iter()
420                .map(|p| p.to_string_lossy().to_string())
421                .collect();
422            self.to_object(py)
423                .call_method1(py, "smart_add", (file_paths,))?;
424            Ok(())
425        })
426    }
427
428    fn update(&self, revision_id: Option<&RevisionId>) -> Result<(), Error> {
429        Python::with_gil(|py| {
430            self.to_object(py)
431                .call_method1(py, "update", (revision_id.cloned(),))?;
432            Ok(())
433        })
434    }
435
436    fn revert(&self, filenames: Option<&[&Path]>) -> Result<(), Error> {
437        Python::with_gil(|py| {
438            let file_paths = filenames.map(|files| {
439                files
440                    .iter()
441                    .map(|p| p.to_string_lossy().to_string())
442                    .collect::<Vec<String>>()
443            });
444            self.to_object(py)
445                .call_method1(py, "revert", (file_paths,))?;
446            Ok(())
447        })
448    }
449
450    fn build_commit(&self) -> CommitBuilder {
451        Python::with_gil(|py| CommitBuilder::from(GenericWorkingTree(self.to_object(py))))
452    }
453
454    fn basis_tree(&self) -> Result<RevisionTree, Error> {
455        Python::with_gil(|py| {
456            let basis_tree = self.to_object(py).call_method0(py, "basis_tree")?;
457            Ok(RevisionTree(basis_tree))
458        })
459    }
460
461    fn is_control_filename(&self, path: &Path) -> bool {
462        Python::with_gil(|py| {
463            self.to_object(py)
464                .call_method1(
465                    py,
466                    "is_control_filename",
467                    (path.to_string_lossy().as_ref(),),
468                )
469                .unwrap()
470                .extract(py)
471                .unwrap()
472        })
473    }
474
475    /// Get a revision tree for a specific revision.
476    fn revision_tree(&self, revision_id: &RevisionId) -> Result<Box<RevisionTree>, Error> {
477        Python::with_gil(|py| {
478            let tree = self.to_object(py).call_method1(
479                py,
480                "revision_tree",
481                (revision_id.clone().into_pyobject(py).unwrap(),),
482            )?;
483            Ok(Box::new(RevisionTree(tree)))
484        })
485    }
486
487    /// Convert a path to an absolute path relative to the working tree.
488    fn abspath(&self, path: &Path) -> Result<PathBuf, Error> {
489        Python::with_gil(|py| {
490            Ok(self
491                .to_object(py)
492                .call_method1(py, "abspath", (path.to_string_lossy().as_ref(),))?
493                .extract(py)?)
494        })
495    }
496
497    /// Convert an absolute path to a path relative to the working tree.
498    fn relpath(&self, path: &Path) -> Result<PathBuf, Error> {
499        Python::with_gil(|py| {
500            Ok(self
501                .to_object(py)
502                .call_method1(py, "relpath", (path.to_string_lossy().as_ref(),))?
503                .extract(py)?)
504        })
505    }
506
507    /// Pull changes from another branch into this working tree.
508    fn pull(
509        &self,
510        source: &dyn Branch,
511        overwrite: Option<bool>,
512        stop_revision: Option<&RevisionId>,
513        local: Option<bool>,
514    ) -> Result<(), Error> {
515        Python::with_gil(|py| {
516            let kwargs = {
517                let kwargs = pyo3::types::PyDict::new(py);
518                if let Some(overwrite) = overwrite {
519                    kwargs.set_item("overwrite", overwrite).unwrap();
520                }
521                if let Some(stop_revision) = stop_revision {
522                    kwargs
523                        .set_item(
524                            "stop_revision",
525                            stop_revision.clone().into_pyobject(py).unwrap(),
526                        )
527                        .unwrap();
528                }
529                if let Some(local) = local {
530                    kwargs.set_item("local", local).unwrap();
531                }
532                kwargs
533            };
534            // Try to cast to a concrete type that implements PyBranch
535            let py_obj =
536                if let Some(generic_branch) = source.as_any().downcast_ref::<GenericBranch>() {
537                    generic_branch.to_object(py)
538                } else if let Some(py_branch) = source
539                    .as_any()
540                    .downcast_ref::<crate::branch::MemoryBranch>()
541                {
542                    py_branch.to_object(py)
543                } else {
544                    return Err(Error::Other(
545                        PyErr::new::<pyo3::exceptions::PyTypeError, _>(
546                            "Branch must be a PyBranch implementation for pull operation",
547                        ),
548                    ));
549                };
550            self.to_object(py)
551                .call_method(py, "pull", (py_obj,), Some(&kwargs))?;
552            Ok(())
553        })
554    }
555
556    /// Merge changes from another branch into this working tree.
557    fn merge_from_branch(
558        &self,
559        source: &dyn Branch,
560        to_revision: Option<&RevisionId>,
561    ) -> Result<(), Error> {
562        Python::with_gil(|py| {
563            let kwargs = {
564                let kwargs = pyo3::types::PyDict::new(py);
565                if let Some(to_revision) = to_revision {
566                    kwargs
567                        .set_item(
568                            "to_revision",
569                            to_revision.clone().into_pyobject(py).unwrap(),
570                        )
571                        .unwrap();
572                }
573                kwargs
574            };
575            // Try to cast to a concrete type that implements PyBranch
576            let py_obj =
577                if let Some(generic_branch) = source.as_any().downcast_ref::<GenericBranch>() {
578                    generic_branch.to_object(py)
579                } else if let Some(py_branch) = source
580                    .as_any()
581                    .downcast_ref::<crate::branch::MemoryBranch>()
582                {
583                    py_branch.to_object(py)
584                } else {
585                    return Err(Error::Other(PyErr::new::<pyo3::exceptions::PyTypeError, _>(
586                    "Branch must be a PyBranch implementation for merge_from_branch operation"
587                )));
588                };
589            self.to_object(py)
590                .call_method(py, "merge_from_branch", (py_obj,), Some(&kwargs))?;
591            Ok(())
592        })
593    }
594
595    /// Convert a list of files to relative paths safely.
596    fn safe_relpath_files(
597        &self,
598        file_list: &[&Path],
599        canonicalize: bool,
600        apply_view: bool,
601    ) -> Result<Vec<PathBuf>, Error> {
602        Python::with_gil(|py| {
603            let result = self.to_object(py).call_method1(
604                py,
605                "safe_relpath_files",
606                (
607                    file_list
608                        .iter()
609                        .map(|x| x.to_string_lossy().to_string())
610                        .collect::<Vec<_>>(),
611                    canonicalize,
612                    apply_view,
613                ),
614            )?;
615            Ok(result.extract(py)?)
616        })
617    }
618
619    fn add_conflicts(&self, conflicts: &[crate::tree::Conflict]) -> Result<(), Error> {
620        Python::with_gil(|py| {
621            let conflicts_py: Vec<PyObject> = conflicts
622                .iter()
623                .map(|c| {
624                    let dict = pyo3::types::PyDict::new(py);
625                    dict.set_item("path", c.path.to_string_lossy().to_string())
626                        .unwrap();
627                    dict.set_item("typestring", &c.conflict_type).unwrap();
628                    if let Some(ref msg) = c.message {
629                        dict.set_item("message", msg).unwrap();
630                    }
631                    dict.into_any().unbind()
632                })
633                .collect();
634            self.to_object(py)
635                .call_method1(py, "add_conflicts", (conflicts_py,))?;
636            Ok(())
637        })
638    }
639
640    fn add_parent_tree(
641        &self,
642        parent_id: &RevisionId,
643        parent_tree: &crate::tree::RevisionTree,
644    ) -> Result<(), Error> {
645        Python::with_gil(|py| {
646            self.to_object(py).call_method1(
647                py,
648                "add_parent_tree",
649                (
650                    parent_id.clone().into_pyobject(py).unwrap(),
651                    parent_tree.to_object(py),
652                ),
653            )?;
654            Ok(())
655        })
656    }
657
658    fn add_parent_tree_id(&self, parent_id: &RevisionId) -> Result<(), Error> {
659        Python::with_gil(|py| {
660            self.to_object(py).call_method1(
661                py,
662                "add_parent_tree_id",
663                (parent_id.clone().into_pyobject(py).unwrap(),),
664            )?;
665            Ok(())
666        })
667    }
668
669    fn add_pending_merge(&self, revision_id: &RevisionId) -> Result<(), Error> {
670        Python::with_gil(|py| {
671            self.to_object(py).call_method1(
672                py,
673                "add_pending_merge",
674                (revision_id.clone().into_pyobject(py).unwrap(),),
675            )?;
676            Ok(())
677        })
678    }
679
680    fn auto_resolve(&self) -> Result<(), Error> {
681        Python::with_gil(|py| {
682            self.to_object(py).call_method0(py, "auto_resolve")?;
683            Ok(())
684        })
685    }
686
687    fn check_state(&self) -> Result<(), Error> {
688        Python::with_gil(|py| {
689            self.to_object(py).call_method0(py, "check_state")?;
690            Ok(())
691        })
692    }
693
694    fn get_canonical_path(&self, path: &Path) -> Result<PathBuf, Error> {
695        Python::with_gil(|py| {
696            Ok(self
697                .to_object(py)
698                .call_method1(py, "get_canonical_path", (path.to_string_lossy().as_ref(),))?
699                .extract(py)?)
700        })
701    }
702
703    fn get_canonical_paths(&self, paths: &[&Path]) -> Result<Vec<PathBuf>, Error> {
704        Python::with_gil(|py| {
705            let path_strings: Vec<String> = paths
706                .iter()
707                .map(|p| p.to_string_lossy().to_string())
708                .collect();
709            Ok(self
710                .to_object(py)
711                .call_method1(py, "get_canonical_paths", (path_strings,))?
712                .extract(py)?)
713        })
714    }
715
716    fn get_config_stack(&self) -> Result<PyObject, Error> {
717        Python::with_gil(|py| Ok(self.to_object(py).call_method0(py, "get_config_stack")?))
718    }
719
720    fn get_reference_info(&self, path: &Path) -> Result<Option<(String, PathBuf)>, Error> {
721        Python::with_gil(|py| {
722            let result = self.to_object(py).call_method1(
723                py,
724                "get_reference_info",
725                (path.to_string_lossy().as_ref(),),
726            )?;
727            if result.is_none(py) {
728                Ok(None)
729            } else {
730                let tuple: (String, String) = result.extract(py)?;
731                Ok(Some((tuple.0, PathBuf::from(tuple.1))))
732            }
733        })
734    }
735
736    fn get_shelf_manager(&self) -> Result<PyObject, Error> {
737        Python::with_gil(|py| Ok(self.to_object(py).call_method0(py, "get_shelf_manager")?))
738    }
739
740    fn ignored_files(&self) -> Result<Vec<PathBuf>, Error> {
741        Python::with_gil(|py| {
742            Ok(self
743                .to_object(py)
744                .call_method0(py, "ignored_files")?
745                .extract(py)?)
746        })
747    }
748
749    fn is_locked(&self) -> bool {
750        Python::with_gil(|py| {
751            self.to_object(py)
752                .call_method0(py, "is_locked")
753                .unwrap()
754                .extract(py)
755                .unwrap()
756        })
757    }
758
759    fn merge_modified(&self) -> Result<Vec<PathBuf>, Error> {
760        Python::with_gil(|py| {
761            Ok(self
762                .to_object(py)
763                .call_method0(py, "merge_modified")?
764                .extract(py)?)
765        })
766    }
767
768    fn move_files(&self, from_paths: &[&Path], to_dir: &Path) -> Result<(), Error> {
769        Python::with_gil(|py| {
770            let from_strings: Vec<String> = from_paths
771                .iter()
772                .map(|p| p.to_string_lossy().to_string())
773                .collect();
774            self.to_object(py).call_method1(
775                py,
776                "move",
777                (from_strings, to_dir.to_string_lossy().as_ref()),
778            )?;
779            Ok(())
780        })
781    }
782
783    fn set_conflicts(&self, conflicts: &[crate::tree::Conflict]) -> Result<(), Error> {
784        Python::with_gil(|py| {
785            let conflicts_py: Vec<PyObject> = conflicts
786                .iter()
787                .map(|c| {
788                    let dict = pyo3::types::PyDict::new(py);
789                    dict.set_item("path", c.path.to_string_lossy().to_string())
790                        .unwrap();
791                    dict.set_item("typestring", &c.conflict_type).unwrap();
792                    if let Some(ref msg) = c.message {
793                        dict.set_item("message", msg).unwrap();
794                    }
795                    dict.into_any().unbind()
796                })
797                .collect();
798            self.to_object(py)
799                .call_method1(py, "set_conflicts", (conflicts_py,))?;
800            Ok(())
801        })
802    }
803
804    fn set_last_revision(&self, revision_id: &RevisionId) -> Result<(), Error> {
805        Python::with_gil(|py| {
806            self.to_object(py).call_method1(
807                py,
808                "set_last_revision",
809                (revision_id.clone().into_pyobject(py).unwrap(),),
810            )?;
811            Ok(())
812        })
813    }
814
815    fn set_merge_modified(&self, files: &[&Path]) -> Result<(), Error> {
816        Python::with_gil(|py| {
817            let file_strings: Vec<String> = files
818                .iter()
819                .map(|p| p.to_string_lossy().to_string())
820                .collect();
821            self.to_object(py)
822                .call_method1(py, "set_merge_modified", (file_strings,))?;
823            Ok(())
824        })
825    }
826
827    fn set_pending_merges(&self, revision_ids: &[RevisionId]) -> Result<(), Error> {
828        Python::with_gil(|py| {
829            let revision_ids_py: Vec<PyObject> = revision_ids
830                .iter()
831                .map(|id| id.clone().into_pyobject(py).unwrap().unbind())
832                .collect();
833            self.to_object(py)
834                .call_method1(py, "set_pending_merges", (revision_ids_py,))?;
835            Ok(())
836        })
837    }
838
839    fn set_reference_info(
840        &self,
841        path: &Path,
842        location: &str,
843        file_id: Option<&str>,
844    ) -> Result<(), Error> {
845        Python::with_gil(|py| {
846            let kwargs = pyo3::types::PyDict::new(py);
847            if let Some(file_id) = file_id {
848                kwargs.set_item("file_id", file_id)?;
849            }
850            self.to_object(py).call_method(
851                py,
852                "set_reference_info",
853                (path.to_string_lossy().as_ref(), location),
854                Some(&kwargs),
855            )?;
856            Ok(())
857        })
858    }
859
860    fn subsume(&self, other: &dyn PyWorkingTree) -> Result<(), Error> {
861        Python::with_gil(|py| {
862            self.to_object(py)
863                .call_method1(py, "subsume", (other.to_object(py),))?;
864            Ok(())
865        })
866    }
867
868    fn store_uncommitted(&self) -> Result<String, Error> {
869        Python::with_gil(|py| {
870            Ok(self
871                .to_object(py)
872                .call_method0(py, "store_uncommitted")?
873                .extract(py)?)
874        })
875    }
876
877    fn restore_uncommitted(&self) -> Result<(), Error> {
878        Python::with_gil(|py| {
879            self.to_object(py).call_method0(py, "restore_uncommitted")?;
880            Ok(())
881        })
882    }
883
884    fn extract(&self, dest: &Path, format: Option<&str>) -> Result<(), Error> {
885        Python::with_gil(|py| {
886            let kwargs = pyo3::types::PyDict::new(py);
887            if let Some(format) = format {
888                kwargs.set_item("format", format)?;
889            }
890            self.to_object(py).call_method(
891                py,
892                "extract",
893                (dest.to_string_lossy().as_ref(),),
894                Some(&kwargs),
895            )?;
896            Ok(())
897        })
898    }
899
900    fn clone(
901        &self,
902        dest: &Path,
903        revision_id: Option<&RevisionId>,
904    ) -> Result<GenericWorkingTree, Error> {
905        Python::with_gil(|py| {
906            let kwargs = pyo3::types::PyDict::new(py);
907            if let Some(revision_id) = revision_id {
908                kwargs.set_item(
909                    "revision_id",
910                    revision_id.clone().into_pyobject(py).unwrap(),
911                )?;
912            }
913            let result = self.to_object(py).call_method(
914                py,
915                "clone",
916                (dest.to_string_lossy().as_ref(),),
917                Some(&kwargs),
918            )?;
919            Ok(GenericWorkingTree(result))
920        })
921    }
922
923    fn control_transport(&self) -> Result<crate::transport::Transport, Error> {
924        Python::with_gil(|py| {
925            let transport = self.to_object(py).getattr(py, "control_transport")?;
926            Ok(crate::transport::Transport::new(transport))
927        })
928    }
929
930    fn control_url(&self) -> url::Url {
931        Python::with_gil(|py| {
932            let url: String = self
933                .to_object(py)
934                .getattr(py, "control_url")
935                .unwrap()
936                .extract(py)
937                .unwrap();
938            url.parse().unwrap()
939        })
940    }
941
942    fn copy_content_into(
943        &self,
944        source: &dyn PyTree,
945        revision_id: Option<&RevisionId>,
946    ) -> Result<(), Error> {
947        Python::with_gil(|py| {
948            let kwargs = pyo3::types::PyDict::new(py);
949            if let Some(revision_id) = revision_id {
950                kwargs.set_item(
951                    "revision_id",
952                    revision_id.clone().into_pyobject(py).unwrap(),
953                )?;
954            }
955            self.to_object(py).call_method(
956                py,
957                "copy_content_into",
958                (source.to_object(py),),
959                Some(&kwargs),
960            )?;
961            Ok(())
962        })
963    }
964
965    fn flush(&self) -> Result<(), Error> {
966        Python::with_gil(|py| {
967            self.to_object(py).call_method0(py, "flush")?;
968            Ok(())
969        })
970    }
971
972    fn requires_rich_root(&self) -> bool {
973        Python::with_gil(|py| {
974            self.to_object(py)
975                .call_method0(py, "requires_rich_root")
976                .unwrap()
977                .extract(py)
978                .unwrap()
979        })
980    }
981
982    fn reset_state(&self, revision_ids: Option<&[RevisionId]>) -> Result<(), Error> {
983        Python::with_gil(|py| {
984            let kwargs = pyo3::types::PyDict::new(py);
985            if let Some(revision_ids) = revision_ids {
986                let revision_ids_py: Vec<PyObject> = revision_ids
987                    .iter()
988                    .map(|id| id.clone().into_pyobject(py).unwrap().unbind())
989                    .collect();
990                kwargs.set_item("revision_ids", revision_ids_py)?;
991            }
992            self.to_object(py)
993                .call_method(py, "reset_state", (), Some(&kwargs))?;
994            Ok(())
995        })
996    }
997
998    fn reference_parent(
999        &self,
1000        path: &Path,
1001        branch: &dyn Branch,
1002        revision_id: Option<&RevisionId>,
1003    ) -> Result<(), Error> {
1004        Python::with_gil(|py| {
1005            let kwargs = pyo3::types::PyDict::new(py);
1006            if let Some(revision_id) = revision_id {
1007                kwargs.set_item(
1008                    "revision_id",
1009                    revision_id.clone().into_pyobject(py).unwrap(),
1010                )?;
1011            }
1012            // Try to cast to a concrete type that implements PyBranch
1013            let py_obj =
1014                if let Some(generic_branch) = branch.as_any().downcast_ref::<GenericBranch>() {
1015                    generic_branch.to_object(py)
1016                } else if let Some(py_branch) = branch
1017                    .as_any()
1018                    .downcast_ref::<crate::branch::MemoryBranch>()
1019                {
1020                    py_branch.to_object(py)
1021                } else {
1022                    return Err(Error::Other(PyErr::new::<pyo3::exceptions::PyTypeError, _>(
1023                    "Branch must be a PyBranch implementation for reference_parent operation"
1024                )));
1025                };
1026            self.to_object(py).call_method(
1027                py,
1028                "reference_parent",
1029                (path.to_string_lossy().as_ref(), py_obj),
1030                Some(&kwargs),
1031            )?;
1032            Ok(())
1033        })
1034    }
1035
1036    fn supports_merge_modified(&self) -> bool {
1037        Python::with_gil(|py| {
1038            self.to_object(py)
1039                .call_method0(py, "supports_merge_modified")
1040                .unwrap()
1041                .extract(py)
1042                .unwrap()
1043        })
1044    }
1045
1046    fn break_lock(&self) -> Result<(), Error> {
1047        Python::with_gil(|py| {
1048            self.to_object(py).call_method0(py, "break_lock")?;
1049            Ok(())
1050        })
1051    }
1052
1053    fn get_physical_lock_status(&self) -> Result<bool, Error> {
1054        Python::with_gil(|py| {
1055            Ok(self
1056                .to_object(py)
1057                .call_method0(py, "get_physical_lock_status")?
1058                .extract(py)?)
1059        })
1060    }
1061}
1062
1063/// A working tree in a version control system.
1064///
1065/// A working tree is a local directory containing the files of a branch that can
1066/// be edited. This struct wraps a Python working tree object and provides access
1067/// to its functionality.
1068pub struct GenericWorkingTree(pub PyObject);
1069
1070impl crate::tree::PyTree for GenericWorkingTree {
1071    fn to_object(&self, py: Python) -> PyObject {
1072        self.0.clone_ref(py)
1073    }
1074}
1075impl crate::tree::PyMutableTree for GenericWorkingTree {}
1076
1077impl PyWorkingTree for GenericWorkingTree {}
1078
1079impl Clone for GenericWorkingTree {
1080    fn clone(&self) -> Self {
1081        Python::with_gil(|py| GenericWorkingTree(self.0.clone_ref(py)))
1082    }
1083}
1084
1085impl<'py> IntoPyObject<'py> for GenericWorkingTree {
1086    type Target = PyAny;
1087    type Output = Bound<'py, Self::Target>;
1088    type Error = std::convert::Infallible;
1089
1090    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
1091        Ok(self.0.into_bound(py))
1092    }
1093}
1094
1095/// A builder for creating commits in a working tree.
1096///
1097/// This struct provides a fluent interface for setting the parameters of a commit
1098/// and then creating it.
1099pub struct CommitBuilder(GenericWorkingTree, Py<pyo3::types::PyDict>);
1100
1101impl From<GenericWorkingTree> for CommitBuilder {
1102    /// Create a new CommitBuilder from a WorkingTree.
1103    ///
1104    /// # Parameters
1105    ///
1106    /// * `wt` - The working tree to create commits in.
1107    ///
1108    /// # Returns
1109    ///
1110    /// A new CommitBuilder instance.
1111    fn from(wt: GenericWorkingTree) -> Self {
1112        Python::with_gil(|py| {
1113            let kwargs = pyo3::types::PyDict::new(py);
1114            CommitBuilder(wt, kwargs.into())
1115        })
1116    }
1117}
1118
1119impl CommitBuilder {
1120    /// Set the committer for this commit.
1121    ///
1122    /// # Parameters
1123    ///
1124    /// * `committer` - The committer's name and email.
1125    ///
1126    /// # Returns
1127    ///
1128    /// Self for method chaining.
1129    pub fn committer(self, committer: &str) -> Self {
1130        Python::with_gil(|py| {
1131            self.1.bind(py).set_item("committer", committer).unwrap();
1132        });
1133        self
1134    }
1135
1136    /// Set the commit message.
1137    ///
1138    /// # Parameters
1139    ///
1140    /// * `message` - The commit message.
1141    ///
1142    /// # Returns
1143    ///
1144    /// Self for method chaining.
1145    pub fn message(self, message: &str) -> Self {
1146        Python::with_gil(|py| {
1147            self.1.bind(py).set_item("message", message).unwrap();
1148        });
1149        self
1150    }
1151
1152    /// Specify which files to include in this commit.
1153    ///
1154    /// # Parameters
1155    ///
1156    /// * `specific_files` - The paths of files to include in this commit.
1157    ///
1158    /// # Returns
1159    ///
1160    /// Self for method chaining.
1161    pub fn specific_files(self, specific_files: &[&Path]) -> Self {
1162        let specific_files: Vec<String> = specific_files
1163            .iter()
1164            .map(|x| x.to_string_lossy().to_string())
1165            .collect();
1166        Python::with_gil(|py| {
1167            self.1
1168                .bind(py)
1169                .set_item("specific_files", specific_files)
1170                .unwrap();
1171        });
1172        self
1173    }
1174
1175    /// Allow pointless commits.
1176    ///
1177    /// # Parameters
1178    ///
1179    /// * `allow_pointless` - Whether to allow commits that don't change any files.
1180    ///
1181    /// # Returns
1182    ///
1183    /// Self for method chaining.
1184    pub fn allow_pointless(self, allow_pointless: bool) -> Self {
1185        Python::with_gil(|py| {
1186            self.1
1187                .bind(py)
1188                .set_item("allow_pointless", allow_pointless)
1189                .unwrap();
1190        });
1191        self
1192    }
1193
1194    /// Set a reporter for this commit.
1195    ///
1196    /// # Parameters
1197    ///
1198    /// * `reporter` - The commit reporter to use.
1199    ///
1200    /// # Returns
1201    ///
1202    /// Self for method chaining.
1203    pub fn reporter(self, reporter: &dyn crate::commit::PyCommitReporter) -> Self {
1204        Python::with_gil(|py| {
1205            self.1
1206                .bind(py)
1207                .set_item("reporter", reporter.to_object(py))
1208                .unwrap();
1209        });
1210        self
1211    }
1212
1213    /// Set the timestamp for this commit.
1214    ///
1215    /// # Parameters
1216    ///
1217    /// * `timestamp` - The timestamp for the commit.
1218    ///
1219    /// # Returns
1220    ///
1221    /// Self for method chaining.
1222    pub fn timestamp(self, timestamp: f64) -> Self {
1223        Python::with_gil(|py| {
1224            self.1.bind(py).set_item("timestamp", timestamp).unwrap();
1225        });
1226        self
1227    }
1228
1229    /// Set a revision property for this commit.
1230    ///
1231    /// Revision properties are key-value pairs that can be attached to commits
1232    /// to store additional metadata beyond the standard commit fields.
1233    ///
1234    /// # Parameters
1235    ///
1236    /// * `key` - The property key (name).
1237    /// * `value` - The property value as a string.
1238    ///
1239    /// # Returns
1240    ///
1241    /// Self for method chaining, or an error if the operation failed.
1242    pub fn set_revprop(self, key: &str, value: &str) -> Result<Self, Error> {
1243        Python::with_gil(|py| {
1244            // Get or create the revprops dictionary
1245            if self.1.bind(py).get_item("revprops")?.is_none() {
1246                let new_revprops = pyo3::types::PyDict::new(py);
1247                self.1.bind(py).set_item("revprops", new_revprops)?;
1248            }
1249
1250            // Now get the revprops dictionary and set the property value
1251            let revprops = self.1.bind(py).get_item("revprops")?.ok_or_else(|| {
1252                Error::Other(pyo3::PyErr::new::<pyo3::exceptions::PyAssertionError, _>(
1253                    "revprops should exist after setting it",
1254                ))
1255            })?;
1256
1257            let revprops_dict = revprops.downcast::<pyo3::types::PyDict>().map_err(|_| {
1258                Error::Other(pyo3::PyErr::new::<pyo3::exceptions::PyTypeError, _>(
1259                    "revprops is not a dictionary",
1260                ))
1261            })?;
1262
1263            revprops_dict.set_item(key, value)?;
1264            Ok(self)
1265        })
1266    }
1267
1268    /// Create the commit.
1269    ///
1270    /// # Returns
1271    ///
1272    /// The revision ID of the new commit, or an error if the commit could not be created.
1273    pub fn commit(self) -> Result<RevisionId, Error> {
1274        Python::with_gil(|py| {
1275            Ok(self
1276                .0
1277                .to_object(py)
1278                .call_method(py, "commit", (), Some(self.1.bind(py)))?
1279                .extract(py)
1280                .unwrap())
1281        })
1282    }
1283}
1284
1285impl GenericWorkingTree {
1286    /// Open a working tree at the specified path.
1287    ///
1288    /// This method is deprecated, use the module-level `open` function instead.
1289    ///
1290    /// # Parameters
1291    ///
1292    /// * `path` - The path to the working tree.
1293    ///
1294    /// # Returns
1295    ///
1296    /// The working tree, or an error if it could not be opened.
1297    #[deprecated = "Use ::open instead"]
1298    pub fn open(path: &Path) -> Result<GenericWorkingTree, Error> {
1299        open(path)
1300    }
1301
1302    /// Open a working tree containing the specified path.
1303    ///
1304    /// This method is deprecated, use the module-level `open_containing` function instead.
1305    ///
1306    /// # Parameters
1307    ///
1308    /// * `path` - The path to look for a containing working tree.
1309    ///
1310    /// # Returns
1311    ///
1312    /// A tuple containing the working tree and the relative path, or an error
1313    /// if no containing working tree could be found.
1314    #[deprecated = "Use ::open_containing instead"]
1315    pub fn open_containing(path: &Path) -> Result<(GenericWorkingTree, PathBuf), Error> {
1316        open_containing(path)
1317    }
1318
1319    /// Create a commit with the specified parameters.
1320    ///
1321    /// This method is deprecated, use the `build_commit` method instead.
1322    ///
1323    /// # Parameters
1324    ///
1325    /// * `message` - The commit message.
1326    /// * `allow_pointless` - Whether to allow commits that don't change any files.
1327    /// * `committer` - The committer's name and email.
1328    /// * `specific_files` - The paths of files to include in this commit.
1329    ///
1330    /// # Returns
1331    ///
1332    /// The revision ID of the new commit, or an error if the commit could not be created.
1333    #[deprecated = "Use build_commit instead"]
1334    pub fn commit(
1335        &self,
1336        message: &str,
1337        committer: Option<&str>,
1338        timestamp: Option<f64>,
1339        allow_pointless: Option<bool>,
1340        specific_files: Option<&[&Path]>,
1341    ) -> Result<RevisionId, Error> {
1342        let mut builder = self.build_commit().message(message);
1343
1344        if let Some(specific_files) = specific_files {
1345            builder = builder.specific_files(specific_files);
1346        }
1347
1348        if let Some(allow_pointless) = allow_pointless {
1349            builder = builder.allow_pointless(allow_pointless);
1350        }
1351
1352        if let Some(committer) = committer {
1353            builder = builder.committer(committer);
1354        }
1355
1356        if let Some(timestamp) = timestamp {
1357            builder = builder.timestamp(timestamp);
1358        }
1359
1360        builder.commit()
1361    }
1362}
1363
1364/// Open a working tree at the specified path.
1365///
1366/// # Parameters
1367///
1368/// * `path` - The path of the working tree to open.
1369///
1370/// # Returns
1371///
1372/// The working tree, or an error if it could not be opened.
1373pub fn open(path: &Path) -> Result<GenericWorkingTree, Error> {
1374    Python::with_gil(|py| {
1375        let m = py.import("breezy.workingtree")?;
1376        let c = m.getattr("WorkingTree")?;
1377        let wt = c.call_method1("open", (path.to_string_lossy().to_string(),))?;
1378        Ok(GenericWorkingTree(wt.unbind()))
1379    })
1380}
1381
1382/// Open a working tree containing the specified path.
1383///
1384/// This function searches for a working tree containing the specified path
1385/// and returns both the working tree and the path relative to the working tree.
1386///
1387/// # Parameters
1388///
1389/// * `path` - The path to look for a containing working tree.
1390///
1391/// # Returns
1392///
1393/// A tuple containing the working tree and the relative path, or an error
1394/// if no containing working tree could be found.
1395pub fn open_containing(path: &Path) -> Result<(GenericWorkingTree, PathBuf), Error> {
1396    Python::with_gil(|py| {
1397        let m = py.import("breezy.workingtree")?;
1398        let c = m.getattr("WorkingTree")?;
1399        let (wt, p): (Bound<PyAny>, String) = c
1400            .call_method1("open_containing", (path.to_string_lossy(),))?
1401            .extract()?;
1402        Ok((GenericWorkingTree(wt.unbind()), PathBuf::from(p)))
1403    })
1404}
1405
1406/// Implementation of From<PyObject> for GenericWorkingTree.
1407impl From<PyObject> for GenericWorkingTree {
1408    /// Create a new WorkingTree from a Python object.
1409    ///
1410    /// # Parameters
1411    ///
1412    /// * `obj` - The Python object representing a working tree.
1413    ///
1414    /// # Returns
1415    ///
1416    /// A new WorkingTree instance.
1417    fn from(obj: PyObject) -> Self {
1418        GenericWorkingTree(obj)
1419    }
1420}