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) .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) .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}