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