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<Py<PyAny>, 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<Py<PyAny>, 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::attach(|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::attach(|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::attach(|py| GenericBranch::from(self.to_object(py).getattr(py, "branch").unwrap()))
390    }
391
392    fn get_user_url(&self) -> url::Url {
393        Python::attach(|py| {
394            let url: String = self
395                .to_object(py)
396                .getattr(py, "user_url")
397                .unwrap()
398                .extract(py)
399                .unwrap();
400            url.parse().unwrap()
401        })
402    }
403
404    fn supports_setting_file_ids(&self) -> bool {
405        Python::attach(|py| {
406            self.to_object(py)
407                .call_method0(py, "supports_setting_file_ids")
408                .unwrap()
409                .extract(py)
410                .unwrap()
411        })
412    }
413
414    fn smart_add(&self, files: &[&Path]) -> Result<(), Error> {
415        Python::attach(|py| {
416            let file_paths: Vec<String> = files
417                .iter()
418                .map(|p| p.to_string_lossy().to_string())
419                .collect();
420            self.to_object(py)
421                .call_method1(py, "smart_add", (file_paths,))?;
422            Ok(())
423        })
424    }
425
426    fn update(&self, revision_id: Option<&RevisionId>) -> Result<(), Error> {
427        Python::attach(|py| {
428            self.to_object(py)
429                .call_method1(py, "update", (revision_id.cloned(),))?;
430            Ok(())
431        })
432    }
433
434    fn revert(&self, filenames: Option<&[&Path]>) -> Result<(), Error> {
435        Python::attach(|py| {
436            let file_paths = filenames.map(|files| {
437                files
438                    .iter()
439                    .map(|p| p.to_string_lossy().to_string())
440                    .collect::<Vec<String>>()
441            });
442            self.to_object(py)
443                .call_method1(py, "revert", (file_paths,))?;
444            Ok(())
445        })
446    }
447
448    fn build_commit(&self) -> CommitBuilder {
449        Python::attach(|py| CommitBuilder::from(GenericWorkingTree(self.to_object(py))))
450    }
451
452    fn basis_tree(&self) -> Result<RevisionTree, Error> {
453        Python::attach(|py| {
454            let basis_tree = self.to_object(py).call_method0(py, "basis_tree")?;
455            Ok(RevisionTree(basis_tree))
456        })
457    }
458
459    fn is_control_filename(&self, path: &Path) -> bool {
460        Python::attach(|py| {
461            self.to_object(py)
462                .call_method1(
463                    py,
464                    "is_control_filename",
465                    (path.to_string_lossy().as_ref(),),
466                )
467                .unwrap()
468                .extract(py)
469                .unwrap()
470        })
471    }
472
473    /// Get a revision tree for a specific revision.
474    fn revision_tree(&self, revision_id: &RevisionId) -> Result<Box<RevisionTree>, Error> {
475        Python::attach(|py| {
476            let tree = self.to_object(py).call_method1(
477                py,
478                "revision_tree",
479                (revision_id.clone().into_pyobject(py).unwrap(),),
480            )?;
481            Ok(Box::new(RevisionTree(tree)))
482        })
483    }
484
485    /// Convert a path to an absolute path relative to the working tree.
486    fn abspath(&self, path: &Path) -> Result<PathBuf, Error> {
487        Python::attach(|py| {
488            Ok(self
489                .to_object(py)
490                .call_method1(py, "abspath", (path.to_string_lossy().as_ref(),))?
491                .extract(py)?)
492        })
493    }
494
495    /// Convert an absolute path to a path relative to the working tree.
496    fn relpath(&self, path: &Path) -> Result<PathBuf, Error> {
497        Python::attach(|py| {
498            Ok(self
499                .to_object(py)
500                .call_method1(py, "relpath", (path.to_string_lossy().as_ref(),))?
501                .extract(py)?)
502        })
503    }
504
505    /// Pull changes from another branch into this working tree.
506    fn pull(
507        &self,
508        source: &dyn Branch,
509        overwrite: Option<bool>,
510        stop_revision: Option<&RevisionId>,
511        local: Option<bool>,
512    ) -> Result<(), Error> {
513        Python::attach(|py| {
514            let kwargs = {
515                let kwargs = pyo3::types::PyDict::new(py);
516                if let Some(overwrite) = overwrite {
517                    kwargs.set_item("overwrite", overwrite).unwrap();
518                }
519                if let Some(stop_revision) = stop_revision {
520                    kwargs
521                        .set_item(
522                            "stop_revision",
523                            stop_revision.clone().into_pyobject(py).unwrap(),
524                        )
525                        .unwrap();
526                }
527                if let Some(local) = local {
528                    kwargs.set_item("local", local).unwrap();
529                }
530                kwargs
531            };
532            // Try to cast to a concrete type that implements PyBranch
533            let py_obj =
534                if let Some(generic_branch) = source.as_any().downcast_ref::<GenericBranch>() {
535                    generic_branch.to_object(py)
536                } else if let Some(py_branch) = source
537                    .as_any()
538                    .downcast_ref::<crate::branch::MemoryBranch>()
539                {
540                    py_branch.to_object(py)
541                } else {
542                    return Err(Error::Other(
543                        PyErr::new::<pyo3::exceptions::PyTypeError, _>(
544                            "Branch must be a PyBranch implementation for pull operation",
545                        ),
546                    ));
547                };
548            self.to_object(py)
549                .call_method(py, "pull", (py_obj,), Some(&kwargs))?;
550            Ok(())
551        })
552    }
553
554    /// Merge changes from another branch into this working tree.
555    fn merge_from_branch(
556        &self,
557        source: &dyn Branch,
558        to_revision: Option<&RevisionId>,
559    ) -> Result<(), Error> {
560        Python::attach(|py| {
561            let kwargs = {
562                let kwargs = pyo3::types::PyDict::new(py);
563                if let Some(to_revision) = to_revision {
564                    kwargs
565                        .set_item(
566                            "to_revision",
567                            to_revision.clone().into_pyobject(py).unwrap(),
568                        )
569                        .unwrap();
570                }
571                kwargs
572            };
573            // Try to cast to a concrete type that implements PyBranch
574            let py_obj =
575                if let Some(generic_branch) = source.as_any().downcast_ref::<GenericBranch>() {
576                    generic_branch.to_object(py)
577                } else if let Some(py_branch) = source
578                    .as_any()
579                    .downcast_ref::<crate::branch::MemoryBranch>()
580                {
581                    py_branch.to_object(py)
582                } else {
583                    return Err(Error::Other(PyErr::new::<pyo3::exceptions::PyTypeError, _>(
584                    "Branch must be a PyBranch implementation for merge_from_branch operation"
585                )));
586                };
587            self.to_object(py)
588                .call_method(py, "merge_from_branch", (py_obj,), Some(&kwargs))?;
589            Ok(())
590        })
591    }
592
593    /// Convert a list of files to relative paths safely.
594    fn safe_relpath_files(
595        &self,
596        file_list: &[&Path],
597        canonicalize: bool,
598        apply_view: bool,
599    ) -> Result<Vec<PathBuf>, Error> {
600        Python::attach(|py| {
601            let result = self.to_object(py).call_method1(
602                py,
603                "safe_relpath_files",
604                (
605                    file_list
606                        .iter()
607                        .map(|x| x.to_string_lossy().to_string())
608                        .collect::<Vec<_>>(),
609                    canonicalize,
610                    apply_view,
611                ),
612            )?;
613            Ok(result.extract(py)?)
614        })
615    }
616
617    fn add_conflicts(&self, conflicts: &[crate::tree::Conflict]) -> Result<(), Error> {
618        Python::attach(|py| {
619            let conflicts_py: Vec<Py<PyAny>> = conflicts
620                .iter()
621                .map(|c| {
622                    let dict = pyo3::types::PyDict::new(py);
623                    dict.set_item("path", c.path.to_string_lossy().to_string())
624                        .unwrap();
625                    dict.set_item("typestring", &c.conflict_type).unwrap();
626                    if let Some(ref msg) = c.message {
627                        dict.set_item("message", msg).unwrap();
628                    }
629                    dict.into_any().unbind()
630                })
631                .collect();
632            self.to_object(py)
633                .call_method1(py, "add_conflicts", (conflicts_py,))?;
634            Ok(())
635        })
636    }
637
638    fn add_parent_tree(
639        &self,
640        parent_id: &RevisionId,
641        parent_tree: &crate::tree::RevisionTree,
642    ) -> Result<(), Error> {
643        Python::attach(|py| {
644            self.to_object(py).call_method1(
645                py,
646                "add_parent_tree",
647                (
648                    parent_id.clone().into_pyobject(py).unwrap(),
649                    parent_tree.to_object(py),
650                ),
651            )?;
652            Ok(())
653        })
654    }
655
656    fn add_parent_tree_id(&self, parent_id: &RevisionId) -> Result<(), Error> {
657        Python::attach(|py| {
658            self.to_object(py).call_method1(
659                py,
660                "add_parent_tree_id",
661                (parent_id.clone().into_pyobject(py).unwrap(),),
662            )?;
663            Ok(())
664        })
665    }
666
667    fn add_pending_merge(&self, revision_id: &RevisionId) -> Result<(), Error> {
668        Python::attach(|py| {
669            self.to_object(py).call_method1(
670                py,
671                "add_pending_merge",
672                (revision_id.clone().into_pyobject(py).unwrap(),),
673            )?;
674            Ok(())
675        })
676    }
677
678    fn auto_resolve(&self) -> Result<(), Error> {
679        Python::attach(|py| {
680            self.to_object(py).call_method0(py, "auto_resolve")?;
681            Ok(())
682        })
683    }
684
685    fn check_state(&self) -> Result<(), Error> {
686        Python::attach(|py| {
687            self.to_object(py).call_method0(py, "check_state")?;
688            Ok(())
689        })
690    }
691
692    fn get_canonical_path(&self, path: &Path) -> Result<PathBuf, Error> {
693        Python::attach(|py| {
694            Ok(self
695                .to_object(py)
696                .call_method1(py, "get_canonical_path", (path.to_string_lossy().as_ref(),))?
697                .extract(py)?)
698        })
699    }
700
701    fn get_canonical_paths(&self, paths: &[&Path]) -> Result<Vec<PathBuf>, Error> {
702        Python::attach(|py| {
703            let path_strings: Vec<String> = paths
704                .iter()
705                .map(|p| p.to_string_lossy().to_string())
706                .collect();
707            Ok(self
708                .to_object(py)
709                .call_method1(py, "get_canonical_paths", (path_strings,))?
710                .extract(py)?)
711        })
712    }
713
714    fn get_config_stack(&self) -> Result<Py<PyAny>, Error> {
715        Python::attach(|py| Ok(self.to_object(py).call_method0(py, "get_config_stack")?))
716    }
717
718    fn get_reference_info(&self, path: &Path) -> Result<Option<(String, PathBuf)>, Error> {
719        Python::attach(|py| {
720            let result = self.to_object(py).call_method1(
721                py,
722                "get_reference_info",
723                (path.to_string_lossy().as_ref(),),
724            )?;
725            if result.is_none(py) {
726                Ok(None)
727            } else {
728                let tuple: (String, String) = result.extract(py)?;
729                Ok(Some((tuple.0, PathBuf::from(tuple.1))))
730            }
731        })
732    }
733
734    fn get_shelf_manager(&self) -> Result<Py<PyAny>, Error> {
735        Python::attach(|py| Ok(self.to_object(py).call_method0(py, "get_shelf_manager")?))
736    }
737
738    fn ignored_files(&self) -> Result<Vec<PathBuf>, Error> {
739        Python::attach(|py| {
740            Ok(self
741                .to_object(py)
742                .call_method0(py, "ignored_files")?
743                .extract(py)?)
744        })
745    }
746
747    fn is_locked(&self) -> bool {
748        Python::attach(|py| {
749            self.to_object(py)
750                .call_method0(py, "is_locked")
751                .unwrap()
752                .extract(py)
753                .unwrap()
754        })
755    }
756
757    fn merge_modified(&self) -> Result<Vec<PathBuf>, Error> {
758        Python::attach(|py| {
759            Ok(self
760                .to_object(py)
761                .call_method0(py, "merge_modified")?
762                .extract(py)?)
763        })
764    }
765
766    fn move_files(&self, from_paths: &[&Path], to_dir: &Path) -> Result<(), Error> {
767        Python::attach(|py| {
768            let from_strings: Vec<String> = from_paths
769                .iter()
770                .map(|p| p.to_string_lossy().to_string())
771                .collect();
772            self.to_object(py).call_method1(
773                py,
774                "move",
775                (from_strings, to_dir.to_string_lossy().as_ref()),
776            )?;
777            Ok(())
778        })
779    }
780
781    fn set_conflicts(&self, conflicts: &[crate::tree::Conflict]) -> Result<(), Error> {
782        Python::attach(|py| {
783            let conflicts_py: Vec<Py<PyAny>> = conflicts
784                .iter()
785                .map(|c| {
786                    let dict = pyo3::types::PyDict::new(py);
787                    dict.set_item("path", c.path.to_string_lossy().to_string())
788                        .unwrap();
789                    dict.set_item("typestring", &c.conflict_type).unwrap();
790                    if let Some(ref msg) = c.message {
791                        dict.set_item("message", msg).unwrap();
792                    }
793                    dict.into_any().unbind()
794                })
795                .collect();
796            self.to_object(py)
797                .call_method1(py, "set_conflicts", (conflicts_py,))?;
798            Ok(())
799        })
800    }
801
802    fn set_last_revision(&self, revision_id: &RevisionId) -> Result<(), Error> {
803        Python::attach(|py| {
804            self.to_object(py).call_method1(
805                py,
806                "set_last_revision",
807                (revision_id.clone().into_pyobject(py).unwrap(),),
808            )?;
809            Ok(())
810        })
811    }
812
813    fn set_merge_modified(&self, files: &[&Path]) -> Result<(), Error> {
814        Python::attach(|py| {
815            let file_strings: Vec<String> = files
816                .iter()
817                .map(|p| p.to_string_lossy().to_string())
818                .collect();
819            self.to_object(py)
820                .call_method1(py, "set_merge_modified", (file_strings,))?;
821            Ok(())
822        })
823    }
824
825    fn set_pending_merges(&self, revision_ids: &[RevisionId]) -> Result<(), Error> {
826        Python::attach(|py| {
827            let revision_ids_py: Vec<Py<PyAny>> = revision_ids
828                .iter()
829                .map(|id| id.clone().into_pyobject(py).unwrap().unbind())
830                .collect();
831            self.to_object(py)
832                .call_method1(py, "set_pending_merges", (revision_ids_py,))?;
833            Ok(())
834        })
835    }
836
837    fn set_reference_info(
838        &self,
839        path: &Path,
840        location: &str,
841        file_id: Option<&str>,
842    ) -> Result<(), Error> {
843        Python::attach(|py| {
844            let kwargs = pyo3::types::PyDict::new(py);
845            if let Some(file_id) = file_id {
846                kwargs.set_item("file_id", file_id)?;
847            }
848            self.to_object(py).call_method(
849                py,
850                "set_reference_info",
851                (path.to_string_lossy().as_ref(), location),
852                Some(&kwargs),
853            )?;
854            Ok(())
855        })
856    }
857
858    fn subsume(&self, other: &dyn PyWorkingTree) -> Result<(), Error> {
859        Python::attach(|py| {
860            self.to_object(py)
861                .call_method1(py, "subsume", (other.to_object(py),))?;
862            Ok(())
863        })
864    }
865
866    fn store_uncommitted(&self) -> Result<String, Error> {
867        Python::attach(|py| {
868            Ok(self
869                .to_object(py)
870                .call_method0(py, "store_uncommitted")?
871                .extract(py)?)
872        })
873    }
874
875    fn restore_uncommitted(&self) -> Result<(), Error> {
876        Python::attach(|py| {
877            self.to_object(py).call_method0(py, "restore_uncommitted")?;
878            Ok(())
879        })
880    }
881
882    fn extract(&self, dest: &Path, format: Option<&str>) -> Result<(), Error> {
883        Python::attach(|py| {
884            let kwargs = pyo3::types::PyDict::new(py);
885            if let Some(format) = format {
886                kwargs.set_item("format", format)?;
887            }
888            self.to_object(py).call_method(
889                py,
890                "extract",
891                (dest.to_string_lossy().as_ref(),),
892                Some(&kwargs),
893            )?;
894            Ok(())
895        })
896    }
897
898    fn clone(
899        &self,
900        dest: &Path,
901        revision_id: Option<&RevisionId>,
902    ) -> Result<GenericWorkingTree, Error> {
903        Python::attach(|py| {
904            let kwargs = pyo3::types::PyDict::new(py);
905            if let Some(revision_id) = revision_id {
906                kwargs.set_item(
907                    "revision_id",
908                    revision_id.clone().into_pyobject(py).unwrap(),
909                )?;
910            }
911            let result = self.to_object(py).call_method(
912                py,
913                "clone",
914                (dest.to_string_lossy().as_ref(),),
915                Some(&kwargs),
916            )?;
917            Ok(GenericWorkingTree(result))
918        })
919    }
920
921    fn control_transport(&self) -> Result<crate::transport::Transport, Error> {
922        Python::attach(|py| {
923            let transport = self.to_object(py).getattr(py, "control_transport")?;
924            Ok(crate::transport::Transport::new(transport))
925        })
926    }
927
928    fn control_url(&self) -> url::Url {
929        Python::attach(|py| {
930            let url: String = self
931                .to_object(py)
932                .getattr(py, "control_url")
933                .unwrap()
934                .extract(py)
935                .unwrap();
936            url.parse().unwrap()
937        })
938    }
939
940    fn copy_content_into(
941        &self,
942        source: &dyn PyTree,
943        revision_id: Option<&RevisionId>,
944    ) -> Result<(), Error> {
945        Python::attach(|py| {
946            let kwargs = pyo3::types::PyDict::new(py);
947            if let Some(revision_id) = revision_id {
948                kwargs.set_item(
949                    "revision_id",
950                    revision_id.clone().into_pyobject(py).unwrap(),
951                )?;
952            }
953            self.to_object(py).call_method(
954                py,
955                "copy_content_into",
956                (source.to_object(py),),
957                Some(&kwargs),
958            )?;
959            Ok(())
960        })
961    }
962
963    fn flush(&self) -> Result<(), Error> {
964        Python::attach(|py| {
965            self.to_object(py).call_method0(py, "flush")?;
966            Ok(())
967        })
968    }
969
970    fn requires_rich_root(&self) -> bool {
971        Python::attach(|py| {
972            self.to_object(py)
973                .call_method0(py, "requires_rich_root")
974                .unwrap()
975                .extract(py)
976                .unwrap()
977        })
978    }
979
980    fn reset_state(&self, revision_ids: Option<&[RevisionId]>) -> Result<(), Error> {
981        Python::attach(|py| {
982            let kwargs = pyo3::types::PyDict::new(py);
983            if let Some(revision_ids) = revision_ids {
984                let revision_ids_py: Vec<Py<PyAny>> = revision_ids
985                    .iter()
986                    .map(|id| id.clone().into_pyobject(py).unwrap().unbind())
987                    .collect();
988                kwargs.set_item("revision_ids", revision_ids_py)?;
989            }
990            self.to_object(py)
991                .call_method(py, "reset_state", (), Some(&kwargs))?;
992            Ok(())
993        })
994    }
995
996    fn reference_parent(
997        &self,
998        path: &Path,
999        branch: &dyn Branch,
1000        revision_id: Option<&RevisionId>,
1001    ) -> Result<(), Error> {
1002        Python::attach(|py| {
1003            let kwargs = pyo3::types::PyDict::new(py);
1004            if let Some(revision_id) = revision_id {
1005                kwargs.set_item(
1006                    "revision_id",
1007                    revision_id.clone().into_pyobject(py).unwrap(),
1008                )?;
1009            }
1010            // Try to cast to a concrete type that implements PyBranch
1011            let py_obj =
1012                if let Some(generic_branch) = branch.as_any().downcast_ref::<GenericBranch>() {
1013                    generic_branch.to_object(py)
1014                } else if let Some(py_branch) = branch
1015                    .as_any()
1016                    .downcast_ref::<crate::branch::MemoryBranch>()
1017                {
1018                    py_branch.to_object(py)
1019                } else {
1020                    return Err(Error::Other(PyErr::new::<pyo3::exceptions::PyTypeError, _>(
1021                    "Branch must be a PyBranch implementation for reference_parent operation"
1022                )));
1023                };
1024            self.to_object(py).call_method(
1025                py,
1026                "reference_parent",
1027                (path.to_string_lossy().as_ref(), py_obj),
1028                Some(&kwargs),
1029            )?;
1030            Ok(())
1031        })
1032    }
1033
1034    fn supports_merge_modified(&self) -> bool {
1035        Python::attach(|py| {
1036            self.to_object(py)
1037                .call_method0(py, "supports_merge_modified")
1038                .unwrap()
1039                .extract(py)
1040                .unwrap()
1041        })
1042    }
1043
1044    fn break_lock(&self) -> Result<(), Error> {
1045        Python::attach(|py| {
1046            self.to_object(py).call_method0(py, "break_lock")?;
1047            Ok(())
1048        })
1049    }
1050
1051    fn get_physical_lock_status(&self) -> Result<bool, Error> {
1052        Python::attach(|py| {
1053            Ok(self
1054                .to_object(py)
1055                .call_method0(py, "get_physical_lock_status")?
1056                .extract(py)?)
1057        })
1058    }
1059}
1060
1061/// A working tree in a version control system.
1062///
1063/// A working tree is a local directory containing the files of a branch that can
1064/// be edited. This struct wraps a Python working tree object and provides access
1065/// to its functionality.
1066pub struct GenericWorkingTree(pub Py<PyAny>);
1067
1068impl crate::tree::PyTree for GenericWorkingTree {
1069    fn to_object(&self, py: Python) -> Py<PyAny> {
1070        self.0.clone_ref(py)
1071    }
1072}
1073impl crate::tree::PyMutableTree for GenericWorkingTree {}
1074
1075impl PyWorkingTree for GenericWorkingTree {}
1076
1077impl Clone for GenericWorkingTree {
1078    fn clone(&self) -> Self {
1079        Python::attach(|py| GenericWorkingTree(self.0.clone_ref(py)))
1080    }
1081}
1082
1083impl<'py> IntoPyObject<'py> for GenericWorkingTree {
1084    type Target = PyAny;
1085    type Output = Bound<'py, Self::Target>;
1086    type Error = std::convert::Infallible;
1087
1088    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
1089        Ok(self.0.into_bound(py))
1090    }
1091}
1092
1093/// A builder for creating commits in a working tree.
1094///
1095/// This struct provides a fluent interface for setting the parameters of a commit
1096/// and then creating it.
1097pub struct CommitBuilder(GenericWorkingTree, Py<pyo3::types::PyDict>);
1098
1099impl From<GenericWorkingTree> for CommitBuilder {
1100    /// Create a new CommitBuilder from a WorkingTree.
1101    ///
1102    /// # Parameters
1103    ///
1104    /// * `wt` - The working tree to create commits in.
1105    ///
1106    /// # Returns
1107    ///
1108    /// A new CommitBuilder instance.
1109    fn from(wt: GenericWorkingTree) -> Self {
1110        Python::attach(|py| {
1111            let kwargs = pyo3::types::PyDict::new(py);
1112            CommitBuilder(wt, kwargs.into())
1113        })
1114    }
1115}
1116
1117impl CommitBuilder {
1118    /// Set the committer for this commit.
1119    ///
1120    /// # Parameters
1121    ///
1122    /// * `committer` - The committer's name and email.
1123    ///
1124    /// # Returns
1125    ///
1126    /// Self for method chaining.
1127    pub fn committer(self, committer: &str) -> Self {
1128        Python::attach(|py| {
1129            self.1.bind(py).set_item("committer", committer).unwrap();
1130        });
1131        self
1132    }
1133
1134    /// Set the commit message.
1135    ///
1136    /// # Parameters
1137    ///
1138    /// * `message` - The commit message.
1139    ///
1140    /// # Returns
1141    ///
1142    /// Self for method chaining.
1143    pub fn message(self, message: &str) -> Self {
1144        Python::attach(|py| {
1145            self.1.bind(py).set_item("message", message).unwrap();
1146        });
1147        self
1148    }
1149
1150    /// Specify which files to include in this commit.
1151    ///
1152    /// # Parameters
1153    ///
1154    /// * `specific_files` - The paths of files to include in this commit.
1155    ///
1156    /// # Returns
1157    ///
1158    /// Self for method chaining.
1159    pub fn specific_files(self, specific_files: &[&Path]) -> Self {
1160        let specific_files: Vec<String> = specific_files
1161            .iter()
1162            .map(|x| x.to_string_lossy().to_string())
1163            .collect();
1164        Python::attach(|py| {
1165            self.1
1166                .bind(py)
1167                .set_item("specific_files", specific_files)
1168                .unwrap();
1169        });
1170        self
1171    }
1172
1173    /// Allow pointless commits.
1174    ///
1175    /// # Parameters
1176    ///
1177    /// * `allow_pointless` - Whether to allow commits that don't change any files.
1178    ///
1179    /// # Returns
1180    ///
1181    /// Self for method chaining.
1182    pub fn allow_pointless(self, allow_pointless: bool) -> Self {
1183        Python::attach(|py| {
1184            self.1
1185                .bind(py)
1186                .set_item("allow_pointless", allow_pointless)
1187                .unwrap();
1188        });
1189        self
1190    }
1191
1192    /// Set a reporter for this commit.
1193    ///
1194    /// # Parameters
1195    ///
1196    /// * `reporter` - The commit reporter to use.
1197    ///
1198    /// # Returns
1199    ///
1200    /// Self for method chaining.
1201    pub fn reporter(self, reporter: &dyn crate::commit::PyCommitReporter) -> Self {
1202        Python::attach(|py| {
1203            self.1
1204                .bind(py)
1205                .set_item("reporter", reporter.to_object(py))
1206                .unwrap();
1207        });
1208        self
1209    }
1210
1211    /// Set the timestamp for this commit.
1212    ///
1213    /// # Parameters
1214    ///
1215    /// * `timestamp` - The timestamp for the commit.
1216    ///
1217    /// # Returns
1218    ///
1219    /// Self for method chaining.
1220    pub fn timestamp(self, timestamp: f64) -> Self {
1221        Python::attach(|py| {
1222            self.1.bind(py).set_item("timestamp", timestamp).unwrap();
1223        });
1224        self
1225    }
1226
1227    /// Set a revision property for this commit.
1228    ///
1229    /// Revision properties are key-value pairs that can be attached to commits
1230    /// to store additional metadata beyond the standard commit fields.
1231    ///
1232    /// # Parameters
1233    ///
1234    /// * `key` - The property key (name).
1235    /// * `value` - The property value as a string.
1236    ///
1237    /// # Returns
1238    ///
1239    /// Self for method chaining, or an error if the operation failed.
1240    pub fn set_revprop(self, key: &str, value: &str) -> Result<Self, Error> {
1241        Python::attach(|py| {
1242            // Get or create the revprops dictionary
1243            if self.1.bind(py).get_item("revprops")?.is_none() {
1244                let new_revprops = pyo3::types::PyDict::new(py);
1245                self.1.bind(py).set_item("revprops", new_revprops)?;
1246            }
1247
1248            // Now get the revprops dictionary and set the property value
1249            let revprops = self.1.bind(py).get_item("revprops")?.ok_or_else(|| {
1250                Error::Other(pyo3::PyErr::new::<pyo3::exceptions::PyAssertionError, _>(
1251                    "revprops should exist after setting it",
1252                ))
1253            })?;
1254
1255            let revprops_dict = revprops.cast::<pyo3::types::PyDict>().map_err(|_| {
1256                Error::Other(pyo3::PyErr::new::<pyo3::exceptions::PyTypeError, _>(
1257                    "revprops is not a dictionary",
1258                ))
1259            })?;
1260
1261            revprops_dict.set_item(key, value)?;
1262            Ok(self)
1263        })
1264    }
1265
1266    /// Create the commit.
1267    ///
1268    /// # Returns
1269    ///
1270    /// The revision ID of the new commit, or an error if the commit could not be created.
1271    pub fn commit(self) -> Result<RevisionId, Error> {
1272        Python::attach(|py| {
1273            Ok(self
1274                .0
1275                .to_object(py)
1276                .call_method(py, "commit", (), Some(self.1.bind(py)))?
1277                .extract(py)
1278                .unwrap())
1279        })
1280    }
1281}
1282
1283impl GenericWorkingTree {
1284    /// Open a working tree at the specified path.
1285    ///
1286    /// This method is deprecated, use the module-level `open` function instead.
1287    ///
1288    /// # Parameters
1289    ///
1290    /// * `path` - The path to the working tree.
1291    ///
1292    /// # Returns
1293    ///
1294    /// The working tree, or an error if it could not be opened.
1295    #[deprecated = "Use ::open instead"]
1296    pub fn open(path: &Path) -> Result<GenericWorkingTree, Error> {
1297        open(path)
1298    }
1299
1300    /// Open a working tree containing the specified path.
1301    ///
1302    /// This method is deprecated, use the module-level `open_containing` function instead.
1303    ///
1304    /// # Parameters
1305    ///
1306    /// * `path` - The path to look for a containing working tree.
1307    ///
1308    /// # Returns
1309    ///
1310    /// A tuple containing the working tree and the relative path, or an error
1311    /// if no containing working tree could be found.
1312    #[deprecated = "Use ::open_containing instead"]
1313    pub fn open_containing(path: &Path) -> Result<(GenericWorkingTree, PathBuf), Error> {
1314        open_containing(path)
1315    }
1316
1317    /// Create a commit with the specified parameters.
1318    ///
1319    /// This method is deprecated, use the `build_commit` method instead.
1320    ///
1321    /// # Parameters
1322    ///
1323    /// * `message` - The commit message.
1324    /// * `allow_pointless` - Whether to allow commits that don't change any files.
1325    /// * `committer` - The committer's name and email.
1326    /// * `specific_files` - The paths of files to include in this commit.
1327    ///
1328    /// # Returns
1329    ///
1330    /// The revision ID of the new commit, or an error if the commit could not be created.
1331    #[deprecated = "Use build_commit instead"]
1332    pub fn commit(
1333        &self,
1334        message: &str,
1335        committer: Option<&str>,
1336        timestamp: Option<f64>,
1337        allow_pointless: Option<bool>,
1338        specific_files: Option<&[&Path]>,
1339    ) -> Result<RevisionId, Error> {
1340        let mut builder = self.build_commit().message(message);
1341
1342        if let Some(specific_files) = specific_files {
1343            builder = builder.specific_files(specific_files);
1344        }
1345
1346        if let Some(allow_pointless) = allow_pointless {
1347            builder = builder.allow_pointless(allow_pointless);
1348        }
1349
1350        if let Some(committer) = committer {
1351            builder = builder.committer(committer);
1352        }
1353
1354        if let Some(timestamp) = timestamp {
1355            builder = builder.timestamp(timestamp);
1356        }
1357
1358        builder.commit()
1359    }
1360}
1361
1362/// Open a working tree at the specified path.
1363///
1364/// # Parameters
1365///
1366/// * `path` - The path of the working tree to open.
1367///
1368/// # Returns
1369///
1370/// The working tree, or an error if it could not be opened.
1371pub fn open(path: &Path) -> Result<GenericWorkingTree, Error> {
1372    Python::attach(|py| {
1373        let m = py.import("breezy.workingtree")?;
1374        let c = m.getattr("WorkingTree")?;
1375        let wt = c.call_method1("open", (path.to_string_lossy().to_string(),))?;
1376        Ok(GenericWorkingTree(wt.unbind()))
1377    })
1378}
1379
1380/// Open a working tree containing the specified path.
1381///
1382/// This function searches for a working tree containing the specified path
1383/// and returns both the working tree and the path relative to the working tree.
1384///
1385/// # Parameters
1386///
1387/// * `path` - The path to look for a containing working tree.
1388///
1389/// # Returns
1390///
1391/// A tuple containing the working tree and the relative path, or an error
1392/// if no containing working tree could be found.
1393pub fn open_containing(path: &Path) -> Result<(GenericWorkingTree, PathBuf), Error> {
1394    Python::attach(|py| {
1395        let m = py.import("breezy.workingtree")?;
1396        let c = m.getattr("WorkingTree")?;
1397        let (wt, p): (Bound<PyAny>, String) = c
1398            .call_method1("open_containing", (path.to_string_lossy(),))?
1399            .extract()?;
1400        Ok((GenericWorkingTree(wt.unbind()), PathBuf::from(p)))
1401    })
1402}
1403
1404/// Implementation of From<Py<PyAny>> for GenericWorkingTree.
1405impl From<Py<PyAny>> for GenericWorkingTree {
1406    /// Create a new WorkingTree from a Python object.
1407    ///
1408    /// # Parameters
1409    ///
1410    /// * `obj` - The Python object representing a working tree.
1411    ///
1412    /// # Returns
1413    ///
1414    /// A new WorkingTree instance.
1415    fn from(obj: Py<PyAny>) -> Self {
1416        GenericWorkingTree(obj)
1417    }
1418}