punktf_lib/visit/diff/
mod.rs

1//! A [`Visitor`](`crate::visit::Visitor`) implementation which creates events for
2//! files which differ from the content it would have once deployed.
3
4use crate::{
5	profile::LayeredProfile,
6	profile::{source::PunktfSource, transform::Transform},
7	visit::*,
8};
9use std::path::Path;
10
11/// Applies any relevant [`Transform`](`crate::profile::transform::Transform`)
12/// for the given file.
13fn transform_content(
14	profile: &LayeredProfile,
15	file: &File<'_>,
16	content: String,
17) -> color_eyre::Result<String> {
18	let mut content = content;
19
20	// Copy so we exec_dotfile is not referenced by this in case an error occurs.
21	let exec_transformers: Vec<_> = file.dotfile().transformers.to_vec();
22
23	// Apply transformers.
24	// Order:
25	//   - Transformers which are specified in the profile root
26	//   - Transformers which are specified on a specific dotfile of a profile
27	for transformer in profile.transformers().chain(exec_transformers.iter()) {
28		content = transformer.transform(content)?;
29	}
30
31	Ok(content)
32}
33
34/// An event which is emitted for every differing item.
35#[derive(Debug)]
36pub enum Event<'a> {
37	/// File does currently not exist but would be created.
38	NewFile {
39		/// Relative path to the punktf source.
40		relative_source_path: &'a Path,
41
42		/// Absolute path to the target location.
43		target_path: &'a Path,
44	},
45
46	/// Directory does currently not exist but would be created.
47	NewDirectory {
48		/// Relative path to the punktf source.
49		relative_source_path: &'a Path,
50
51		/// Absolute path to the target location.
52		target_path: &'a Path,
53	},
54
55	/// File does exist but the contents would changed.
56	Diff {
57		/// Relative path to the punktf source.
58		relative_source_path: &'a Path,
59
60		/// Absolute path to the target location.
61		target_path: &'a Path,
62
63		/// Contents of the current file on the filesystem.
64		old_content: String,
65
66		/// Contents of the file after a deployment.
67		///
68		/// #NOTE
69		/// If the contents come from a template item, it will be already
70		/// fully resolved.
71		new_content: String,
72	},
73}
74
75impl Event<'_> {
76	/// Returns the absolute target path for the diff.
77	pub const fn target_path(&self) -> &Path {
78		match self {
79			Self::NewFile { target_path, .. } => target_path,
80			Self::NewDirectory { target_path, .. } => target_path,
81			Self::Diff { target_path, .. } => target_path,
82		}
83	}
84}
85
86/// A [`Visitor`](`crate::visit::Visitor`) implementation which checks for
87/// changes which would be made by a deployment.
88/// For each change an [`Event`] is emitted which can be processed by [`Diff.0`].
89#[derive(Debug, Clone, Copy)]
90pub struct Diff<F>(F);
91
92impl<F> Diff<F>
93where
94	F: Fn(Event<'_>),
95{
96	/// Creates a new instance of the visitor.
97	pub const fn new(f: F) -> Self {
98		Self(f)
99	}
100
101	/// Runs the visitor to completion for a given profile.
102	pub fn diff(self, source: &PunktfSource, profile: &mut LayeredProfile) {
103		let mut resolver = ResolvingVisitor(self);
104		let walker = Walker::new(profile);
105
106		if let Err(err) = walker.walk(source, &mut resolver) {
107			log::error!("Failed to execute diff: {err}");
108		}
109	}
110
111	/// Emits the given event.
112	fn dispatch(&self, event: Event<'_>) {
113		(self.0)(event)
114	}
115}
116
117/// Reads the contents of the given file at `path`.
118///
119/// Handles common errors by logging them using `display_path` as identifier.
120///
121/// Will either return the files contents or directly exit the outer function
122/// with `Ok(())`.
123macro_rules! safe_read_file_content {
124	($path:expr, $display_path:expr) => {{
125		match std::fs::read_to_string($path) {
126			Ok(old) => old,
127			Err(err) if err.kind() == std::io::ErrorKind::InvalidData => {
128				log::info!("[{}] Ignored - Binary data", $display_path);
129				return Ok(());
130			}
131			Err(err) => {
132				log::error!("[{}] Error - Failed to read file: {err}", $display_path);
133				return Ok(());
134			}
135		}
136	}};
137}
138
139impl<F> Visitor for Diff<F>
140where
141	F: Fn(Event<'_>),
142{
143	/// Accepts a file item and checks if it differs in any way to the counter
144	/// part on the filesystem (deployed item).
145	///
146	/// If so, a change [`Event::NewFile`]/[`Event::Diff`] is emitted.
147	fn accept_file<'a>(
148		&mut self,
149		_: &PunktfSource,
150		profile: &LayeredProfile,
151		file: &File<'a>,
152	) -> Result {
153		if file.target_path.exists() {
154			let old =
155				safe_read_file_content!(&file.target_path, file.relative_source_path.display());
156
157			let new =
158				safe_read_file_content!(&file.source_path, file.relative_source_path.display());
159
160			let new = match transform_content(profile, file, new) {
161				Ok(new) => new,
162				Err(err) => {
163					log::error!(
164						"[{}] Error - Failed to apply transformer: {err}",
165						file.relative_source_path.display(),
166					);
167					return Ok(());
168				}
169			};
170
171			if new != old {
172				self.dispatch(Event::Diff {
173					relative_source_path: &file.relative_source_path,
174					target_path: &file.target_path,
175					old_content: old,
176					new_content: new,
177				});
178			}
179		} else {
180			self.dispatch(Event::NewFile {
181				relative_source_path: &file.relative_source_path,
182				target_path: &file.target_path,
183			})
184		}
185
186		Ok(())
187	}
188
189	/// Accepts a directory item and simply checks if it already exists on the filesystem.
190	///
191	/// If no, a change [`Event::NewDirectory`] is emitted.
192	fn accept_directory<'a>(
193		&mut self,
194		_: &PunktfSource,
195		_: &LayeredProfile,
196		directory: &Directory<'a>,
197	) -> Result {
198		if !directory.target_path.exists() {
199			self.dispatch(Event::NewDirectory {
200				relative_source_path: &directory.relative_source_path,
201				target_path: &directory.target_path,
202			})
203		}
204
205		Ok(())
206	}
207
208	/// Accepts a rejected item and does nothing besides logging an info message.
209	///
210	/// # NOTE
211	/// Links are currently not supported for diffing.
212	fn accept_link(&mut self, _: &PunktfSource, _: &LayeredProfile, link: &Symlink) -> Result {
213		log::info!(
214			"[{}] Ignoring - Symlinks are not supported for diffs",
215			link.source_path.display()
216		);
217
218		Ok(())
219	}
220
221	/// Accepts a rejected item and does nothing besides logging an info message.
222	fn accept_rejected<'a>(
223		&mut self,
224		_: &PunktfSource,
225		_: &LayeredProfile,
226		rejected: &Rejected<'a>,
227	) -> Result {
228		log::info!(
229			"[{}] Rejected - {}",
230			rejected.relative_source_path.display(),
231			rejected.reason,
232		);
233
234		Ok(())
235	}
236
237	/// Accepts a rejected item and does nothing besides logging an error message.
238	fn accept_errored<'a>(
239		&mut self,
240		_: &PunktfSource,
241		_: &LayeredProfile,
242		errored: &Errored<'a>,
243	) -> Result {
244		log::error!(
245			"[{}] Error - {}",
246			errored.relative_source_path.display(),
247			errored
248		);
249
250		Ok(())
251	}
252}
253
254impl<F> TemplateVisitor for Diff<F>
255where
256	F: Fn(Event<'_>),
257{
258	/// Accepts a file template item and checks if it differs in any way to the
259	/// counter part on the filesystem (deployed item).
260	///
261	/// If so, a change [`Event::NewFile`]/[`Event::Diff`] is emitted.
262	fn accept_template<'a>(
263		&mut self,
264		_: &PunktfSource,
265		profile: &LayeredProfile,
266		file: &File<'a>,
267		// Returns a function to resolve the content to make the resolving lazy
268		// for upstream visitors.
269		resolve_content: impl FnOnce(&str) -> color_eyre::Result<String>,
270	) -> Result {
271		if file.target_path.exists() {
272			let old =
273				safe_read_file_content!(&file.target_path, file.relative_source_path.display());
274
275			let new =
276				safe_read_file_content!(&file.source_path, file.relative_source_path.display());
277
278			let new = match resolve_content(&new) {
279				Ok(content) => content,
280				Err(err) => {
281					log::error!(
282						"[{}] Error - Failed to resolve template: {err}",
283						file.source_path.display()
284					);
285
286					return Ok(());
287				}
288			};
289
290			let new = match transform_content(profile, file, new) {
291				Ok(new) => new,
292				Err(err) => {
293					log::error!(
294						"[{}] Error - Failed to apply transformer: {err}",
295						file.relative_source_path.display(),
296					);
297					return Ok(());
298				}
299			};
300
301			if new != old {
302				self.dispatch(Event::Diff {
303					relative_source_path: &file.relative_source_path,
304					target_path: &file.target_path,
305					old_content: old,
306					new_content: new,
307				});
308			}
309		} else {
310			self.dispatch(Event::NewFile {
311				relative_source_path: &file.relative_source_path,
312				target_path: &file.target_path,
313			})
314		}
315
316		Ok(())
317	}
318}