Skip to main content

breezyshim/
patches.rs

1//! Patching support for Breezy.
2use crate::transform::TreeTransform;
3use patchkit::unified::{HunkLine, UnifiedPatch};
4use pyo3::intern;
5use pyo3::prelude::*;
6use pyo3::types::{PyBytes, PyList};
7
8fn py_patches(iter_patches: impl Iterator<Item = UnifiedPatch>) -> PyResult<Py<PyAny>> {
9    Python::attach(|py| {
10        let m = py.import("breezy.patches")?;
11        let patchc = m.getattr("Patch")?;
12        let hunkc = m.getattr("Hunk")?;
13        let insertlinec = m.getattr("InsertLine")?;
14        let removelinec = m.getattr("RemoveLine")?;
15        let contextlinec = m.getattr("ContextLine")?;
16        let mut ret = vec![];
17        for patch in iter_patches {
18            let pypatch = patchc.call1((
19                PyBytes::new(py, &patch.orig_name),
20                PyBytes::new(py, &patch.mod_name),
21                patch.orig_ts,
22                patch.mod_ts,
23            ))?;
24            let pyhunks = pypatch.getattr("hunks")?;
25
26            for hunk in patch.hunks {
27                let pyhunk = hunkc.call1((
28                    hunk.orig_pos,
29                    hunk.orig_range,
30                    hunk.mod_pos,
31                    hunk.mod_range,
32                    hunk.tail,
33                ))?;
34                pyhunks.call_method1("append", (&pyhunk,))?;
35
36                let pylines = pyhunk.getattr("lines")?;
37
38                for line in hunk.lines {
39                    pylines.call_method1(
40                        "append",
41                        (match line {
42                            HunkLine::ContextLine(l) => {
43                                contextlinec.call1((PyBytes::new(py, l.as_slice()),))?
44                            }
45                            HunkLine::InsertLine(l) => {
46                                insertlinec.call1((PyBytes::new(py, l.as_slice()),))?
47                            }
48                            HunkLine::RemoveLine(l) => {
49                                removelinec.call1((PyBytes::new(py, l.as_slice()),))?
50                            }
51                        },),
52                    )?;
53                }
54            }
55            ret.push(pypatch);
56        }
57        Ok(PyList::new(py, ret.iter())?.unbind().into())
58    })
59}
60
61/// Apply patches to a TreeTransform.
62///
63/// # Arguments
64/// * `tt`: TreeTransform instance
65/// * `patches`: List of patches
66/// * `prefix`: Number leading path segments to strip
67pub fn apply_patches(
68    tt: &TreeTransform,
69    patches: impl Iterator<Item = UnifiedPatch>,
70    prefix: Option<usize>,
71) -> crate::Result<()> {
72    Python::attach(|py| {
73        let patches = py_patches(patches)?;
74        let m = py.import("breezy.tree")?;
75        let apply_patches = m.getattr("apply_patches")?;
76        apply_patches.call1((tt.as_pyobject(), patches, prefix))?;
77        Ok(())
78    })
79}
80
81/// Represents patches that have been applied to a tree.
82///
83/// This struct provides a way to temporarily apply patches to a tree
84/// and automatically revert them when the object is dropped.
85pub struct AppliedPatches(Py<PyAny>, Py<PyAny>);
86
87impl AppliedPatches {
88    /// Create a new AppliedPatches instance.
89    ///
90    /// # Arguments
91    ///
92    /// * `tree` - The tree to apply patches to
93    /// * `patches` - List of patches to apply
94    /// * `prefix` - Number of leading path segments to strip from patch paths
95    ///
96    /// # Returns
97    ///
98    /// A new AppliedPatches object, which will revert the patches when dropped
99    pub fn new<T: crate::tree::PyTree>(
100        tree: &T,
101        patches: Vec<UnifiedPatch>,
102        prefix: Option<usize>,
103    ) -> crate::Result<Self> {
104        let (ap, tree) = Python::attach(|py| -> Result<_, PyErr> {
105            let patches = py_patches(patches.into_iter())?;
106            let m = py.import("breezy.patches")?;
107            let c = m.getattr("AppliedPatches")?;
108            let ap = c.call1((tree.to_object(py), patches, prefix))?;
109            let tree = ap.call_method0(intern!(py, "__enter__"))?;
110            Ok((ap.unbind(), tree.unbind()))
111        })?;
112        Ok(Self(tree, ap))
113    }
114}
115
116impl Drop for AppliedPatches {
117    fn drop(&mut self) {
118        Python::attach(|py| -> Result<(), PyErr> {
119            self.1.call_method1(
120                py,
121                intern!(py, "__exit__"),
122                (py.None(), py.None(), py.None()),
123            )?;
124            Ok(())
125        })
126        .unwrap();
127    }
128}
129
130impl<'py> IntoPyObject<'py> for AppliedPatches {
131    type Target = PyAny;
132    type Output = Bound<'py, Self::Target>;
133    type Error = std::convert::Infallible;
134
135    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
136        Ok(self.0.clone_ref(py).into_bound(py))
137    }
138}
139
140impl crate::tree::PyTree for AppliedPatches {
141    fn to_object(&self, py: Python) -> Py<PyAny> {
142        self.0.clone_ref(py)
143    }
144}
145
146#[cfg(test)]
147mod applied_patches_tests {
148    use super::*;
149    use crate::controldir::ControlDirFormat;
150    use crate::tree::MutableTree;
151    use crate::tree::Tree;
152    use crate::workingtree::WorkingTree;
153    use serial_test::serial;
154
155    #[test]
156    #[serial]
157    fn test_apply_simple() {
158        let env = crate::testing::TestEnv::new();
159        let td = tempfile::tempdir().unwrap();
160        let tree = crate::controldir::create_standalone_workingtree(
161            td.path(),
162            &ControlDirFormat::default(),
163        )
164        .unwrap();
165        std::fs::write(td.path().join("a"), "a\n").unwrap();
166        tree.add(&[std::path::Path::new("a")]).unwrap();
167        tree.build_commit()
168            .message("Add a")
169            .reporter(&crate::commit::NullCommitReporter::new())
170            .commit()
171            .unwrap();
172        let patch = UnifiedPatch::parse_patch(patchkit::unified::splitlines(
173            br#"--- a/a
174+++ b/a
175@@ -1 +1 @@
176-a
177+b
178"#,
179        ))
180        .unwrap();
181
182        let newtree = crate::patches::AppliedPatches::new(&tree, vec![patch], None).unwrap();
183        assert_eq!(
184            b"b\n".to_vec(),
185            newtree.get_file_text(std::path::Path::new("a")).unwrap()
186        );
187        std::mem::drop(newtree);
188        std::mem::drop(env);
189    }
190
191    #[test]
192    #[serial]
193    fn test_apply_delete() {
194        let env = crate::testing::TestEnv::new();
195        let td = tempfile::tempdir().unwrap();
196        let tree = crate::controldir::create_standalone_workingtree(
197            td.path(),
198            &ControlDirFormat::default(),
199        )
200        .unwrap();
201        std::fs::write(td.path().join("a"), "a\n").unwrap();
202        tree.add(&[std::path::Path::new("a")]).unwrap();
203        tree.build_commit()
204            .reporter(&crate::commit::NullCommitReporter::new())
205            .message("Add a")
206            .commit()
207            .unwrap();
208        let patch = UnifiedPatch::parse_patch(patchkit::unified::splitlines(
209            br#"--- a/a
210+++ /dev/null
211@@ -1 +0,0 @@
212-a
213"#,
214        ))
215        .unwrap();
216        let newtree = crate::patches::AppliedPatches::new(&tree, vec![patch], None).unwrap();
217        assert!(!newtree.has_filename(std::path::Path::new("a")));
218        std::mem::drop(env);
219    }
220
221    #[test]
222    #[serial]
223    fn test_apply_add() {
224        let env = crate::testing::TestEnv::new();
225        let td = tempfile::tempdir().unwrap();
226        let tree = crate::controldir::create_standalone_workingtree(
227            td.path(),
228            &ControlDirFormat::default(),
229        )
230        .unwrap();
231        std::fs::write(td.path().join("a"), "a\n").unwrap();
232        tree.add(&[std::path::Path::new("a")]).unwrap();
233        tree.build_commit()
234            .reporter(&crate::commit::NullCommitReporter::new())
235            .message("Add a")
236            .commit()
237            .unwrap();
238        let patch = UnifiedPatch::parse_patch(patchkit::unified::splitlines(
239            br#"--- /dev/null
240+++ b/b
241@@ -0,0 +1 @@
242+b
243"#,
244        ))
245        .unwrap();
246        let newtree = crate::patches::AppliedPatches::new(&tree, vec![patch], None).unwrap();
247        assert_eq!(
248            b"b\n".to_vec(),
249            newtree.get_file_text(std::path::Path::new("b")).unwrap()
250        );
251        std::mem::drop(env);
252    }
253}