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
19pub 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 let mut processed: BTreeSet<Oid> = Default::default();
42
43 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 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 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 let paths = &[
162 &[&root, &l_0, &l_1, &m, &c_0, &c_1 ],
164 &[&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 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}