Skip to main content

silver_platter/
utils.rs

1//! Utility functions for working with branches.
2use breezyshim::branch::{Branch, PyBranch};
3use breezyshim::error::Error as BrzError;
4use breezyshim::merge::{Error as MergeError, MergeType, Merger, MERGE_HOOKS};
5use breezyshim::repository::Repository;
6use breezyshim::tree::WorkingTree;
7use breezyshim::workingtree::GenericWorkingTree;
8use breezyshim::RevisionId;
9use std::collections::HashMap;
10
11/// A temporary sprout of a branch.
12pub struct TempSprout {
13    /// The working tree of the sprout.
14    pub workingtree: GenericWorkingTree,
15
16    /// The temporary directory that the sprout is in.
17    pub tempdir: Option<tempfile::TempDir>,
18}
19
20impl TempSprout {
21    /// Create a temporary sprout of a branch.
22    pub fn new(
23        branch: &dyn PyBranch,
24        additional_colocated_branches: Option<HashMap<String, String>>,
25    ) -> Result<Self, BrzError> {
26        let (wt, td) = create_temp_sprout(branch, additional_colocated_branches, None, None)?;
27        Ok(Self {
28            workingtree: wt,
29            tempdir: td,
30        })
31    }
32
33    /// Create a temporary sprout of a branch in a specific directory.
34    pub fn new_in(
35        branch: &dyn PyBranch,
36        additional_colocated_branches: Option<HashMap<String, String>>,
37        dir: &std::path::Path,
38    ) -> Result<Self, BrzError> {
39        let (wt, tempdir) =
40            create_temp_sprout(branch, additional_colocated_branches, Some(dir), None)?;
41        Ok(Self {
42            workingtree: wt,
43            tempdir,
44        })
45    }
46
47    /// Create a temporary sprout of a branch with a specific path.
48    pub fn new_in_path(
49        branch: &dyn PyBranch,
50        additional_colocated_branches: Option<HashMap<String, String>>,
51        path: &std::path::Path,
52    ) -> Result<Self, BrzError> {
53        let (wt, tempdir) =
54            create_temp_sprout(branch, additional_colocated_branches, None, Some(path))?;
55        Ok(Self {
56            workingtree: wt,
57            tempdir,
58        })
59    }
60
61    /// Return the tree of the sprout.
62    pub fn tree(&self) -> &dyn WorkingTree {
63        &self.workingtree
64    }
65}
66
67impl std::ops::Deref for TempSprout {
68    type Target = dyn WorkingTree;
69
70    fn deref(&self) -> &Self::Target {
71        &self.workingtree
72    }
73}
74
75/// Create a temporary sprout of a branch.
76///
77/// This attempts to fetch the least amount of history as possible.
78pub fn create_temp_sprout(
79    branch: &dyn PyBranch,
80    additional_colocated_branches: Option<HashMap<String, String>>,
81    dir: Option<&std::path::Path>,
82    path: Option<&std::path::Path>,
83) -> Result<(GenericWorkingTree, Option<tempfile::TempDir>), BrzError> {
84    let (td, path) = if let Some(path) = path {
85        // ensure that path is absolute
86        assert!(path.is_absolute());
87        (None, path.to_path_buf())
88    } else {
89        let td = if let Some(dir) = dir {
90            tempfile::tempdir_in(dir).unwrap()
91        } else {
92            tempfile::tempdir().unwrap()
93        };
94        let path = td.path().to_path_buf();
95        (Some(td), path)
96    };
97
98    let use_stacking =
99        branch.format().supports_stacking() && branch.repository().format().supports_chks();
100    let to_url: url::Url = url::Url::from_directory_path(path).unwrap();
101
102    // preserve whatever source format we have.
103    let to_dir =
104        branch
105            .controldir()
106            .sprout(to_url, Some(branch), Some(true), Some(use_stacking), None)?;
107
108    // TODO(jelmer): Fetch these during the initial clone
109    for (from_branch_name, to_branch_name) in additional_colocated_branches.unwrap_or_default() {
110        let controldir = branch.controldir();
111        match controldir.open_branch(Some(from_branch_name.as_str())) {
112            Ok(add_branch) => {
113                let local_add_branch = to_dir.create_branch(Some(to_branch_name.as_str()))?;
114                add_branch.push(
115                    local_add_branch.as_ref(),
116                    false,
117                    None,
118                    Some(Box::new(|_ps| true)),
119                )?;
120            }
121            Err(err) => {
122                log::warn!("Unable to clone branch {}: {}", from_branch_name, err);
123                return Err(err);
124            }
125        }
126    }
127
128    let wt = to_dir.open_workingtree()?;
129    Ok((wt, td))
130}
131
132/// Check if there are any merge conflicts between two branches.
133pub fn merge_conflicts<B1: PyBranch, B2: PyBranch>(
134    main_branch: &B1,
135    other_branch: &B2,
136    other_revision: Option<&RevisionId>,
137) -> Result<bool, BrzError> {
138    let other_revision = other_revision.map_or_else(|| other_branch.last_revision(), |r| r.clone());
139    let other_repository = other_branch.repository();
140    let graph = other_repository.get_graph();
141
142    if graph.is_ancestor(&main_branch.last_revision(), &other_revision)? {
143        return Ok(false);
144    }
145
146    other_repository.fetch(
147        &main_branch.repository(),
148        Some(&main_branch.last_revision()),
149    )?;
150
151    // Reset custom merge hooks, since they could make it harder to detect
152    // conflicted merges that would appear on the hosting site.
153    let old_file_contents_mergers = MERGE_HOOKS.get("merge_file_content").unwrap();
154    MERGE_HOOKS.clear("merge_file_contents").unwrap();
155
156    let other_tree = other_repository.revision_tree(&other_revision).unwrap();
157    let result = match Merger::from_revision_ids(
158        &other_tree,
159        other_branch,
160        &main_branch.last_revision(),
161        other_branch,
162    ) {
163        Ok(mut merger) => {
164            merger.set_merge_type(MergeType::Merge3);
165            let tree_merger = merger.make_merger().unwrap();
166            let tt = tree_merger.make_preview_transform().unwrap();
167            !tt.cooked_conflicts().unwrap().is_empty()
168        }
169        Err(MergeError::UnrelatedBranches) => {
170            // Unrelated branches don't technically *have* to lead to
171            // conflicts, but there's not a lot to be salvaged here, either.
172            true
173        }
174    };
175    for hook in old_file_contents_mergers {
176        MERGE_HOOKS.add("merge_file_content", hook).unwrap();
177    }
178    Ok(result)
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184    use breezyshim::testing::TestEnv;
185    use serial_test::serial;
186
187    #[test]
188    #[serial]
189    fn test_sprout() {
190        let _test_env = TestEnv::new();
191        let base = tempfile::tempdir().unwrap();
192        let wt = breezyshim::controldir::create_standalone_workingtree(
193            base.path(),
194            &breezyshim::controldir::ControlDirFormat::default(),
195        )
196        .unwrap();
197        let revid = wt
198            .build_commit()
199            .message("Initial commit")
200            .allow_pointless(true)
201            .commit()
202            .unwrap();
203
204        let sprout = TempSprout::new(&wt.branch(), None).unwrap();
205
206        assert_eq!(sprout.last_revision().unwrap(), revid);
207        let tree = sprout.tree();
208        assert_eq!(tree.last_revision().unwrap(), revid);
209        std::mem::drop(sprout);
210    }
211
212    #[test]
213    #[serial]
214    fn test_sprout_in() {
215        let _test_env = TestEnv::new();
216        let base = tempfile::tempdir().unwrap();
217        let wt = breezyshim::controldir::create_standalone_workingtree(
218            base.path(),
219            &breezyshim::controldir::ControlDirFormat::default(),
220        )
221        .unwrap();
222        let revid = wt
223            .build_commit()
224            .message("Initial commit")
225            .allow_pointless(true)
226            .commit()
227            .unwrap();
228
229        let sprout = TempSprout::new_in(&wt.branch(), None, base.path()).unwrap();
230
231        assert_eq!(sprout.last_revision().unwrap(), revid);
232        let tree = sprout.tree();
233        assert_eq!(tree.last_revision().unwrap(), revid);
234        std::mem::drop(sprout);
235    }
236
237    #[test]
238    #[serial]
239    fn test_sprout_in_path() {
240        let _test_env = TestEnv::new();
241        let base = tempfile::tempdir().unwrap();
242        let target = tempfile::tempdir().unwrap();
243        let wt = breezyshim::controldir::create_standalone_workingtree(
244            base.path(),
245            &breezyshim::controldir::ControlDirFormat::default(),
246        )
247        .unwrap();
248        let revid = wt
249            .build_commit()
250            .message("Initial commit")
251            .allow_pointless(true)
252            .commit()
253            .unwrap();
254
255        let sprout = TempSprout::new_in_path(&wt.branch(), None, target.path()).unwrap();
256
257        assert_eq!(sprout.last_revision().unwrap(), revid);
258        let tree = sprout.tree();
259        assert_eq!(tree.last_revision().unwrap(), revid);
260        std::mem::drop(sprout);
261    }
262}