sequoia_git/
git.rs

1use std::{
2    collections::{
3        BTreeSet
4    },
5};
6
7use anyhow::Result;
8use git2::{
9    Repository,
10    Oid,
11};
12
13use crate::{
14    Error,
15};
16
17const TRACE: bool = false;
18
19/// Returns whether `ancestor` is an ancestor of `target`.
20///
21/// A commit is considered to be an ancestor of another commit if
22/// there is a path from the first commit to the target commit.  Note:
23/// this does not authenticate the path, or even check whether the
24/// commits are signed.  Commits are considered their own ancestors.
25///
26/// Returns `Ok(())` if there is a path.  If there is no path, returns
27/// `Err(Error::NoPathConnecting)`.
28pub fn git_is_ancestor(git: &Repository, ancestor: Oid, target: Oid) -> Result<()> {
29    tracer!(TRACE, "is_ancestor");
30    t!("Looking for {}..{}", ancestor, target);
31
32    if ancestor.is_zero() {
33        return Err(Error::NoPathConnecting(ancestor, target).into());
34    }
35
36    if ancestor == target {
37        return Ok(());
38    }
39
40    // Whether we've already processed a commit.
41    let mut processed: BTreeSet<Oid> = Default::default();
42
43    // Commits that we still need to process.
44    let mut pending: BTreeSet<Oid> = Default::default();
45    pending.insert(target.clone());
46
47    while let Some(commit_id) = pending.pop_first() {
48        t!("Visiting commit {:?}", commit_id);
49        processed.insert(commit_id.clone());
50
51        let commit = git.find_commit(commit_id)?;
52
53        for parent in commit.parents() {
54            let parent_id = parent.id();
55            if processed.contains(&parent_id) || pending.contains(&parent_id) {
56                // There is a valid path from PARENT to TARGET, which
57                // is not via COMMIT.  There is no need to find a
58                // second path.
59                continue;
60            }
61
62            if parent_id == ancestor {
63                t!("Reached ancestor!");
64                return Ok(());
65            }
66
67            pending.insert(parent_id.clone());
68        }
69    }
70
71    Err(Error::NoPathConnecting(ancestor, target).into())
72}
73
74#[cfg(test)]
75mod test {
76    use super::*;
77
78    use std::path::Path;
79
80    use tempfile::TempDir;
81
82    use git2::Commit;
83    use git2::Repository;
84
85    fn commit_file<'repo, P>(repo: &'repo Repository,
86                             filename: P, content: &[u8],
87                             commit_message: &str,
88                             parents: &[&Commit<'repo>]) -> Commit<'repo>
89        where P: AsRef<Path>
90    {
91        let filename = filename.as_ref();
92
93        let filename_abs = repo.workdir().unwrap().join(&filename);
94        std::fs::write(&filename_abs, content).unwrap();
95
96        let mut index = repo.index().unwrap();
97        index.add_path(&filename).unwrap();
98
99        let oid = index.write_tree().unwrap();
100        let tree = repo.find_tree(oid).unwrap();
101
102        let sig = repo.signature().unwrap();
103
104        let commit_oid = repo.commit(
105            None, &sig, &sig, commit_message, &tree, parents)
106            .unwrap();
107
108        let commit = repo.find_commit(commit_oid).unwrap();
109
110        commit
111    }
112
113    #[test]
114    fn ancestor() -> Result<()> {
115        let dir = TempDir::new()?;
116
117        let repo = Repository::init(&dir)
118            .expect("Initialize git repository");
119
120        let mut config = repo.config().unwrap();
121        config.set_str("user.name", "name").unwrap();
122        config.set_str("user.email", "email").unwrap();
123
124        //   root
125        //   /  \
126        // l.0  r.0
127        //  |    |
128        // l.1  r.1
129        //   \  /
130        //  merge
131        //    |
132        //   c.0
133        //    |
134        //   c.1
135
136        let root = commit_file(
137            &repo, "root", b"root", "root", &[]);
138
139        let l_0 = commit_file(
140            &repo, "l", b"0", "commit l.0", &[ &root ]);
141        let l_1 = commit_file(
142            &repo, "l", b"1", "commit l.1", &[ &l_0 ]);
143
144        let r_0 = commit_file(
145            &repo, "r", b"0", "commit r.0", &[ &root ]);
146        let r_1 = commit_file(
147            &repo, "r", b"1", "commit r.1", &[ &r_0 ]);
148
149        let m = commit_file(
150            &repo, "m", b"merge!", "commit merge", &[ &l_1, &r_1 ]);
151
152        let c_0 = commit_file(
153            &repo, "c", b"0", "commit c.0", &[ &m ]);
154
155        let c_1 = commit_file(
156            &repo, "c", b"1", "commit c.1", &[ &c_0 ]);
157
158        let all_commits = &[&root, &l_0, &l_1, &r_0, &r_1, &m, &c_0, &c_1 ];
159
160        // All the commits in a topological order.
161        let paths = &[
162            // Via l.
163            &[&root, &l_0, &l_1, &m, &c_0, &c_1 ],
164            // Via r.
165            &[&root, &r_0, &r_1, &m, &c_0, &c_1 ],
166        ];
167
168        let t = |ancestor: &Commit, commit: &Commit, expect: bool| {
169            match (expect,
170                   git_is_ancestor(&repo, ancestor.id(), commit.id()).is_ok())
171            {
172                (true, true) => (),
173                (false, false) => (),
174                (true, false) => {
175                    panic!("Expected {} ({}) to be an ancestor of {} ({})",
176                           ancestor.summary().unwrap_or("<unknown>"),
177                           ancestor.id(),
178                           commit.summary().unwrap_or("<unknown>"),
179                           commit.id());
180                }
181                (false, true) => {
182                    panic!("Expected {} ({}) to NOT be an ancestor of {} ({})",
183                           ancestor.summary().unwrap_or("<unknown>"),
184                           ancestor.id(),
185                           commit.summary().unwrap_or("<unknown>"),
186                           commit.id());
187                }
188            }
189        };
190
191        // Commits are their own ancestors.
192        for c in all_commits.iter() {
193            t(c, c, true);
194        }
195
196        for path in paths.iter() {
197            for i in 0..(path.len() - 1) {
198                for j in (i + 1)..path.len() {
199                    t(&path[i], &path[j], true);
200                    t(&path[j], &path[i], false);
201                }
202            }
203        }
204
205        t(&l_0, &r_0, false);
206        t(&l_0, &r_1, false);
207        t(&l_1, &r_0, false);
208        t(&l_1, &r_1, false);
209
210        t(&r_0, &l_0, false);
211        t(&r_0, &l_1, false);
212        t(&r_1, &l_0, false);
213        t(&r_1, &l_1, false);
214
215        Ok(())
216    }
217}