1#![deny(missing_docs)]
7
8use std::fmt::Display;
9use std::ops::Deref;
10use std::path::Path;
11
12use iddqd::IdOrdMap;
13use walkdir::WalkDir;
14
15mod candidate_is_same;
16mod diff_entry;
17mod diff_tag;
18mod display_diff;
19mod display_diff_opts;
20mod error;
21mod hash_file;
22mod path_info;
23mod strip_prefix;
24
25pub use diff_entry::DiffEntry;
26pub use diff_tag::DiffTag;
27pub use display_diff_opts::DisplayDiffOpts;
28pub use error::Error;
29pub use error::HashError;
30pub use error::MetadataError;
31pub use error::Result;
32pub use error::StripPrefixError;
33pub use error::TraverseError;
34pub use error::WalkDirMetadataError;
35
36use candidate_is_same::candidate_is_same;
37use display_diff::DisplayDiff;
38use path_info::PathInfo;
39use strip_prefix::strip_prefix;
40
41#[derive(Debug)]
43pub struct Diff<'a> {
44 entries: IdOrdMap<DiffEntry<'a>>,
45}
46
47impl<'a> Deref for Diff<'a> {
48 type Target = IdOrdMap<DiffEntry<'a>>;
49
50 fn deref(&self) -> &Self::Target {
51 &self.entries
52 }
53}
54
55impl<'a> IntoIterator for Diff<'a> {
56 type Item = DiffEntry<'a>;
57
58 type IntoIter = iddqd::id_ord_map::IntoIter<Self::Item>;
59
60 fn into_iter(self) -> Self::IntoIter {
61 self.entries.into_iter()
62 }
63}
64
65impl<'a> IntoIterator for &'a Diff<'a> {
66 type Item = &'a DiffEntry<'a>;
67
68 type IntoIter = iddqd::id_ord_map::Iter<'a, DiffEntry<'a>>;
69
70 fn into_iter(self) -> Self::IntoIter {
71 (&self.entries).into_iter()
72 }
73}
74
75impl<'a> Diff<'a> {
76 pub fn new(old: &'a Path, new: &'a Path) -> Result<Self> {
91 let mut diff = Self {
92 entries: IdOrdMap::new(),
93 };
94
95 diff.walk_removed_tree(old, new)?;
96 diff.walk_added_tree(new)?;
97
98 Ok(diff)
99 }
100
101 fn walk_removed_tree(&mut self, old: &'a Path, new: &'a Path) -> Result<()> {
102 let walker = WalkDir::new(old).follow_links(true);
103 let mut iterator = walker.into_iter();
104
105 loop {
106 let removed_entry = match iterator.next() {
107 Some(entry) => entry.map_err(|inner| {
108 Error::Traverse(TraverseError {
109 path: old.to_path_buf(),
110 inner,
111 })
112 }),
113 None => break,
114 }?;
115
116 if removed_entry.depth() == 0 {
117 continue;
118 }
119
120 let relative = strip_prefix(removed_entry.path(), old)?.to_path_buf();
121
122 let removed_metadata =
123 removed_entry
124 .metadata()
125 .map_err(|inner| WalkDirMetadataError {
126 path: removed_entry.path().to_owned(),
127 inner,
128 })?;
129
130 let mut entry = DiffEntry {
131 relative,
132 tag: DiffTag::Delete,
133 deleted: None,
134 inserted: None,
135 };
136
137 let candidate = new.join(&entry.relative);
138 let candidate_metadata = match candidate.metadata() {
139 Ok(metadata) => Some(metadata),
140 Err(err) => {
141 if err.kind() == std::io::ErrorKind::NotFound {
142 None
143 } else {
144 return Err(MetadataError {
145 path: candidate.clone(),
146 inner: err,
147 }
148 .into());
149 }
150 }
151 };
152
153 entry.tag = match candidate_metadata.as_ref() {
154 Some(candidate_metadata) => candidate_is_same(
155 removed_entry.path(),
156 &removed_metadata,
157 &candidate,
158 candidate_metadata,
159 )?,
160 None => DiffTag::Delete,
161 };
162
163 entry.inserted = candidate_metadata.map(|metadata| PathInfo {
164 metadata,
165 base: new,
166 });
167
168 if removed_entry.file_type().is_dir()
169 && let DiffTag::Delete = entry.tag
170 {
171 iterator.skip_current_dir();
173 }
174
175 entry.deleted = Some(PathInfo {
176 metadata: removed_metadata,
177 base: old,
178 });
179
180 if let Some(overwritten) = self.entries.insert_overwrite(entry) {
181 tracing::debug!(?overwritten, "Got two diff entries for a single path");
182 }
183 }
184 Ok(())
185 }
186
187 fn walk_added_tree(&mut self, new: &'a Path) -> Result<()> {
188 let walker = WalkDir::new(new).follow_links(true);
189 let mut iterator = walker.into_iter();
190
191 loop {
192 let added_entry = match iterator.next() {
193 Some(entry) => entry.map_err(|inner| {
194 Error::Traverse(TraverseError {
195 path: new.to_path_buf(),
196 inner,
197 })
198 }),
199 None => break,
200 }?;
201
202 if added_entry.depth() == 0 {
203 continue;
204 }
205
206 let relative = strip_prefix(added_entry.path(), new)?.to_path_buf();
207
208 match self.entries.get(relative.as_path()) {
209 Some(diff_entry) => {
210 if let DiffTag::Delete = diff_entry.tag {
211 iterator.skip_current_dir();
213 continue;
214 }
215 }
216 None => {
217 if added_entry.file_type().is_dir() {
218 iterator.skip_current_dir();
219 }
220
221 if let Some(overwritten) = self.entries.insert_overwrite(DiffEntry {
222 relative,
223 tag: DiffTag::Insert,
224 deleted: None,
225 inserted: Some(PathInfo {
226 metadata: added_entry.metadata().map_err(|inner| {
227 WalkDirMetadataError {
228 path: added_entry.path().to_owned(),
229 inner,
230 }
231 })?,
232 base: new,
233 }),
234 }) {
235 tracing::debug!(?overwritten, "Got two diff entries for a single path");
236 };
237 }
238 }
239 }
240 Ok(())
241 }
242
243 pub fn display(&'a self, opts: DisplayDiffOpts) -> impl Display + 'a {
248 DisplayDiff { diff: self, opts }
249 }
250}
251
252impl<'a> Display for Diff<'a> {
254 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
255 self.display(Default::default()).fmt(f)
256 }
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262 use testlib::TempTree;
263
264 #[test]
265 fn test_same_contents() -> Result<()> {
266 let mut old = TempTree::new().unwrap();
267 old.file("puppy", "puppy").unwrap();
268
269 let mut new = TempTree::new().unwrap();
270 new.file("puppy", "puppy").unwrap();
271
272 let diff = Diff::new(old.as_ref(), new.as_ref())?;
273
274 assert_eq!(
275 (&diff)
276 .into_iter()
277 .map(DiffEntry::as_pair)
278 .collect::<Vec<_>>(),
279 vec![(Path::new("puppy"), DiffTag::Equal)]
280 );
281
282 Ok(())
283 }
284
285 #[test]
286 fn test_different_contents() -> Result<()> {
287 let mut old = TempTree::new().unwrap();
288 old.file("puppy", "puppy").unwrap();
289
290 let mut new = TempTree::new().unwrap();
291 new.file("puppy", "doggy").unwrap();
292
293 let diff = Diff::new(old.as_ref(), new.as_ref())?;
294
295 assert_eq!(
296 (&diff)
297 .into_iter()
298 .map(DiffEntry::as_pair)
299 .collect::<Vec<_>>(),
300 vec![(Path::new("puppy"), DiffTag::Replace)]
301 );
302
303 Ok(())
304 }
305
306 #[test]
307 fn test_complex() -> Result<()> {
308 let mut old = TempTree::new().unwrap();
309 old.dir("a")
310 .unwrap()
311 .file("a/1", "1")
312 .unwrap()
313 .file("a/2", "2")
314 .unwrap()
315 .dir("b")
316 .unwrap()
317 .file("b/1", "1")
318 .unwrap()
319 .file("b/2", "2")
320 .unwrap()
321 .dir("c")
322 .unwrap()
323 .file("c/1", "1")
324 .unwrap()
325 .file("c/2", "2")
326 .unwrap();
327
328 let mut new = TempTree::new().unwrap();
329 new.dir("a")
330 .unwrap()
331 .file("a/1", "1")
332 .unwrap()
333 .file("a/2", "2")
334 .unwrap()
335 .dir("b")
336 .unwrap()
337 .file("b/1", "1x")
338 .unwrap()
339 .file("b/2", "2x")
340 .unwrap();
341
342 let diff = Diff::new(old.as_ref(), new.as_ref())?;
343
344 assert_eq!(
345 (&diff)
346 .into_iter()
347 .map(DiffEntry::as_pair)
348 .collect::<Vec<_>>(),
349 vec![
350 (Path::new("a"), DiffTag::Replace),
351 (Path::new("a/1"), DiffTag::Equal),
352 (Path::new("a/2"), DiffTag::Equal),
353 (Path::new("b"), DiffTag::Replace),
354 (Path::new("b/1"), DiffTag::Replace),
355 (Path::new("b/2"), DiffTag::Replace),
356 (Path::new("c"), DiffTag::Delete),
357 ]
358 );
359
360 Ok(())
361 }
362}