git_scanner/
git_file_future.rs

1#![warn(clippy::all)]
2use git2::Oid;
3use std::collections::HashMap;
4use std::path::{Path, PathBuf};
5
6/// Track file changes for a file - renames and deletes
7#[derive(Debug, Clone)]
8pub struct GitFileFutureRegistry {
9    rev_changes: HashMap<Oid, RevChange>,
10}
11
12#[derive(Debug, Clone)]
13struct RevChange {
14    files: HashMap<PathBuf, FileNameChange>,
15    /// first child is generally used only, it is the main branch - don't divert into other branches!
16    children: Vec<Oid>,
17}
18
19#[derive(Debug, Clone)]
20pub enum FileNameChange {
21    Renamed(PathBuf),
22    Deleted(),
23}
24
25impl RevChange {
26    pub fn new() -> Self {
27        RevChange {
28            files: HashMap::new(),
29            children: Vec::new(),
30        }
31    }
32}
33
34impl GitFileFutureRegistry {
35    pub fn new() -> Self {
36        GitFileFutureRegistry {
37            rev_changes: HashMap::new(),
38        }
39    }
40
41    pub fn register(
42        &mut self,
43        id: &Oid,
44        parent_ids: &[Oid],
45        file_changes: &[(PathBuf, FileNameChange)],
46    ) {
47        let entry = self.rev_changes.entry(*id).or_insert_with(RevChange::new);
48        (*entry).files.extend(file_changes.iter().cloned());
49        for parent_id in parent_ids {
50            let pentry = self
51                .rev_changes
52                .entry(*parent_id)
53                .or_insert_with(RevChange::new);
54            (*pentry).children.push(*id);
55        }
56    }
57
58    /// what is this called in the final revision?
59    /// returns None if it is deleted, or Some(final name)
60    pub fn final_name(&self, ref_id: &Oid, file: &Path) -> Option<PathBuf> {
61        let mut current_name: &PathBuf = &file.to_path_buf();
62        let mut current_ref: Oid = *ref_id;
63        loop {
64            let current_change = self.rev_changes.get(&current_ref).unwrap();
65            match current_change.files.get(current_name) {
66                Some(FileNameChange::Renamed(new_name)) => {
67                    current_name = new_name;
68                }
69                Some(FileNameChange::Deleted()) => return None,
70                None => (),
71            }
72            if let Some(first_child) = current_change.children.get(0) {
73                current_ref = *first_child;
74            // and loop will continue
75            } else {
76                // no children, so finished looking into the future
77                return Some(current_name.to_path_buf());
78            }
79        }
80    }
81}
82
83#[cfg(test)]
84mod test {
85    use super::*;
86    use failure::Error;
87    use pretty_assertions::assert_eq;
88
89    fn pb(name: &str) -> PathBuf {
90        PathBuf::from(name)
91    }
92
93    #[test]
94    fn trivial_repo_returns_original_name() -> Result<(), Error> {
95        let mut registry = GitFileFutureRegistry::new();
96        let my_id = Oid::from_str("01")?;
97        registry.register(&my_id, &[], &[]);
98        assert_eq!(
99            registry.final_name(&my_id, &pb("foo.txt")),
100            Some(pb("foo.txt"))
101        );
102        Ok(())
103    }
104
105    #[test]
106    fn simple_rename_returns_old_name() -> Result<(), Error> {
107        let mut registry = GitFileFutureRegistry::new();
108        let my_id = Oid::from_str("01")?;
109
110        registry.register(
111            &my_id,
112            &[],
113            &[(pb("foo.txt"), FileNameChange::Renamed(pb("bar.txt")))],
114        );
115        assert_eq!(
116            registry.final_name(&my_id, &pb("foo.txt")),
117            Some(pb("bar.txt"))
118        );
119        Ok(())
120    }
121
122    #[test]
123    fn renames_and_deletes_applied_across_history() -> Result<(), Error> {
124        // my bad - this should be a few isolated tests not one big test-all test.
125        // classic how my standards slip for side projects!
126        let mut registry = GitFileFutureRegistry::new();
127        /*
128                   +-----+
129                   |01   |
130                   |add a|
131                   |add z|
132                   +--+--+
133                      |
134               +------v------+
135               |02           |
136               |rename a to b|
137               |delete z     |
138               +-------------+
139               |             |
140        +------v------+ +----v--------+
141        |04           | |05           |
142        |rename b to c| |rename b to d|
143        +--------------+--------------+
144                       |
145              +--------v---------+
146              |06 merge          |
147              |rename c to afinal|
148              |create new z      |
149              +------------------+
150                */
151        let id_1 = Oid::from_str("01")?;
152        let id_2 = Oid::from_str("02")?;
153        let id_4 = Oid::from_str("04")?;
154        let id_5 = Oid::from_str("05")?;
155        let id_6 = Oid::from_str("06")?;
156
157        registry.register(
158            &id_6,
159            &[id_4, id_5],
160            &[(pb("c"), FileNameChange::Renamed(pb("afinal")))],
161        );
162        // NOTE: topological order should (I think?) register rev 4 before rev 5 as it's first
163        registry.register(
164            &id_4,
165            &[id_2],
166            &[(pb("b"), FileNameChange::Renamed(pb("c")))],
167        );
168        registry.register(
169            &id_5,
170            &[id_2],
171            &[(pb("b"), FileNameChange::Renamed(pb("d")))],
172        );
173        registry.register(
174            &id_2,
175            &[id_1],
176            &[
177                (pb("a"), FileNameChange::Renamed(pb("b"))),
178                (pb("z"), FileNameChange::Deleted()),
179            ],
180        );
181        registry.register(&id_1, &[], &[]);
182
183        // original a is afinal
184        // original z is gone
185        assert_eq!(registry.final_name(&id_1, &pb("a")), Some(pb("afinal")));
186        assert_eq!(registry.final_name(&id_1, &pb("z")), None);
187        // from the perspective of the filesystem after node 2, we know nothing of a any more, only b
188        assert_eq!(registry.final_name(&id_2, &pb("b")), Some(pb("afinal")));
189
190        Ok(())
191    }
192}