Skip to main content

lux_lib/build/
patch.rs

1use crate::fs;
2use bon::Builder;
3use diffy::{self, ApplyError, ParsePatchError};
4use miette::Diagnostic;
5use std::{collections::HashMap, path::PathBuf};
6use thiserror::Error;
7#[derive(Builder)]
8#[builder(start_fn = new, finish_fn(name = _build, vis = ""))]
9pub(crate) struct Patch<'a> {
10    #[builder(start_fn)]
11    dir: &'a PathBuf,
12    #[builder(start_fn)]
13    patches: &'a HashMap<PathBuf, String>,
14}
15
16impl<State> PatchBuilder<'_, State>
17where
18    State: patch_builder::State + patch_builder::IsComplete,
19{
20    pub fn apply(self) -> Result<(), Error> {
21        do_apply(self._build())
22    }
23}
24
25#[derive(Error, Debug, Diagnostic)]
26#[non_exhaustive]
27pub enum Error {
28    #[error("error parsing patch {0}: {1}")]
29    Parse(PathBuf, ParsePatchError),
30    #[error(transparent)]
31    #[diagnostic(transparent)]
32    Fs(#[from] fs::Error),
33    #[error("error applying patch {0}: {1}")]
34    Apply(PathBuf, ApplyError),
35}
36
37#[tracing::instrument(level = "trace", skip_all)]
38pub(crate) fn do_apply(args: Patch<'_>) -> Result<(), Error> {
39    for (path, patch_str) in args.patches {
40        let patch =
41            diffy::Patch::from_str(patch_str).map_err(|err| Error::Parse(path.clone(), err))?;
42
43        let original_file = patch
44            .original()
45            .map(|file| {
46                PathBuf::from(file)
47                    .components()
48                    .skip(1) // remove a/
49                    .collect::<PathBuf>()
50            })
51            .filter(|relative_path| relative_path != &PathBuf::from("dev/null"))
52            .map(|relative_path| args.dir.join(relative_path));
53
54        let original_content = original_file
55            .as_ref()
56            .map(fs::sync::read_to_string)
57            .map_or(Ok(None), |v| v.map(Some))?
58            .unwrap_or_default();
59
60        let modified = patch
61            .modified()
62            .map(|file| {
63                PathBuf::from(file)
64                    .components()
65                    .skip(1) // remove b/
66                    .collect::<PathBuf>()
67            })
68            .filter(|relative_path| relative_path != &PathBuf::from("dev/null"))
69            .map(|relative_path| args.dir.join(relative_path))
70            .map(|modified_file| {
71                let modified_content = diffy::apply(&original_content, &patch)
72                    .map_err(|err| Error::Apply(path.clone(), err))?;
73                Ok::<_, Error>((modified_file, modified_content))
74            })
75            .map_or(Ok(None), |v| v.map(Some))?
76            .map(|(file, modified_content)| {
77                let parent = file.parent().unwrap_or(args.dir);
78                fs::sync::create_dir_all(parent)?;
79                Ok::<_, Error>(fs::sync::write(&file, modified_content)?)
80            })
81            .map_or(Ok(None), |v| v.map(Some))?;
82
83        if modified.is_none() {
84            if let Some(original) = original_file {
85                fs::sync::remove_file(&original)?;
86            }
87        }
88    }
89    Ok(())
90}
91
92#[cfg(test)]
93mod tests {
94
95    use super::*;
96    use crate::fs;
97    use assert_fs::TempDir;
98    use std::path::PathBuf;
99
100    #[test]
101    fn test_simple_patch() {
102        let temp_dir = TempDir::new().unwrap();
103        let test_file =
104            PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/patch_test/pack.lua");
105        let orig_content = crate::fs::sync::read_to_string(&test_file).unwrap();
106        let added_line = r#"_G._ENV = rawget(_G, "_ENV") -- to satisfy tarantool strict mode"#;
107        assert!(!orig_content.contains(added_line));
108        let scripts_dir = temp_dir.join("scripts");
109        fs::sync::create_dir_all(&scripts_dir).unwrap();
110        let patch_file = scripts_dir.join("pack.lua");
111        fs::sync::copy(&test_file, &patch_file).unwrap();
112        let patches = vec![(
113            PathBuf::from("test_patch.diff"),
114            r#"
115diff --git a/scripts/pack.lua b/scripts/pack.lua
116index 959c7ed..9c6a9a1 100644
117--- a/scripts/pack.lua
118+++ b/scripts/pack.lua
119@@ -38,4 +38,5 @@ scandir( root )
120
121 acc={(io.open("../ABOUT"):read("*all").."\n"):gsub( "([^\n]-\n)","-- %1" ),[[
122+_G._ENV = rawget(_G, "_ENV") -- to satisfy tarantool strict mode
123 local _ENV,       loaded, packages, release, require_
124     = _ENV or _G, {},     {},       true,    require
125"#
126            .to_string(),
127        )]
128        .into_iter()
129        .collect();
130
131        Patch::new(&temp_dir.join(""), &patches).apply().unwrap();
132
133        let patched_content = crate::fs::sync::read_to_string(&patch_file).unwrap();
134        assert!(patched_content.contains(added_line));
135    }
136
137    #[test]
138    fn test_git_patch_create_file() {
139        let temp_dir = TempDir::new().unwrap();
140        let patches = vec![(
141            PathBuf::from("test.patch"),
142            r#"
143diff --git a/foo/README.md b/foo/README.md
144new file mode 100644
145index 0000000..1cbadfb
146--- /dev/null
147+++ b/foo/README.md
148@@ -0,0 +1 @@
149+# title
150"#
151            .to_string(),
152        )]
153        .into_iter()
154        .collect();
155        Patch::new(&temp_dir.join(""), &patches).apply().unwrap();
156        let patch_file = temp_dir.join("foo/README.md");
157        let patched_content = crate::fs::sync::read_to_string(&patch_file).unwrap();
158        assert!(patched_content.contains("# title"));
159    }
160
161    #[test]
162    fn test_git_patch_delete_file() {
163        let temp_dir = TempDir::new().unwrap();
164        let test_file = temp_dir.join("README.md");
165        crate::fs::sync::write(&test_file, "# title").unwrap();
166        let patches = vec![(
167            PathBuf::from("test.patch"),
168            r#"
169diff --git a/README.md b/README.md
170deleted file mode 100644
171index 1cbadfb..0000000
172--- a/README.md
173+++ /dev/null
174@@ -1 +0,0 @@
175-# title
176"#
177            .to_string(),
178        )]
179        .into_iter()
180        .collect();
181
182        Patch::new(&temp_dir.join(""), &patches).apply().unwrap();
183        assert!(!test_file.exists());
184    }
185}