git_stack/legacy/git/
branches.rs1#[derive(Clone, Default, Debug, PartialEq, Eq)]
2pub struct Branches {
3 branches: std::collections::BTreeMap<git2::Oid, Vec<crate::legacy::git::Branch>>,
4}
5
6impl Branches {
7 pub fn new(branches: impl IntoIterator<Item = crate::legacy::git::Branch>) -> Self {
8 let mut grouped_branches = std::collections::BTreeMap::new();
9 for branch in branches {
10 grouped_branches
11 .entry(branch.id)
12 .or_insert_with(Vec::new)
13 .push(branch);
14 }
15 Self {
16 branches: grouped_branches,
17 }
18 }
19
20 pub fn update(&mut self, repo: &dyn crate::legacy::git::Repo) {
21 let mut new = Self::new(self.branches.values().flatten().filter_map(|b| {
22 if let Some(remote) = b.remote.as_deref() {
23 repo.find_remote_branch(remote, &b.name)
24 } else {
25 repo.find_local_branch(&b.name)
26 }
27 }));
28 std::mem::swap(&mut new, self);
29 }
30
31 pub fn insert(&mut self, branch: crate::legacy::git::Branch) {
32 let branches = self.branches.entry(branch.id).or_default();
33 if !branches
34 .iter()
35 .any(|b| b.remote == branch.remote && b.name == branch.name)
36 {
37 branches.push(branch);
38 }
39 }
40
41 pub fn extend(&mut self, branches: impl Iterator<Item = crate::legacy::git::Branch>) {
42 for branch in branches {
43 self.insert(branch);
44 }
45 }
46
47 pub fn contains_oid(&self, oid: git2::Oid) -> bool {
48 self.branches.contains_key(&oid)
49 }
50
51 pub fn get(&self, oid: git2::Oid) -> Option<&[crate::legacy::git::Branch]> {
52 self.branches.get(&oid).map(|v| v.as_slice())
53 }
54
55 pub fn remove(&mut self, oid: git2::Oid) -> Option<Vec<crate::legacy::git::Branch>> {
56 self.branches.remove(&oid)
57 }
58
59 pub fn oids(&self) -> impl Iterator<Item = git2::Oid> + '_ {
60 self.branches.keys().copied()
61 }
62
63 pub fn iter(&self) -> impl Iterator<Item = (git2::Oid, &[crate::legacy::git::Branch])> + '_ {
64 self.branches
65 .iter()
66 .map(|(oid, branch)| (*oid, branch.as_slice()))
67 }
68
69 pub fn is_empty(&self) -> bool {
70 self.branches.is_empty()
71 }
72
73 pub fn len(&self) -> usize {
74 self.branches.len()
75 }
76
77 pub fn all(&self) -> Self {
78 self.clone()
79 }
80
81 pub fn descendants(&self, repo: &dyn crate::legacy::git::Repo, base_oid: git2::Oid) -> Self {
82 let branches = self
83 .branches
84 .iter()
85 .filter(|(branch_oid, branch)| {
86 let is_base_descendant = repo
87 .merge_base(**branch_oid, base_oid)
88 .map(|merge_oid| merge_oid == base_oid)
89 .unwrap_or(false);
90 if is_base_descendant {
91 true
92 } else {
93 let first_branch = &branch.first().expect("we always have at least one branch");
94 log::trace!(
95 "Branch {} is not on the branch of {}",
96 first_branch,
97 base_oid
98 );
99 false
100 }
101 })
102 .map(|(oid, branches)| {
103 let branches: Vec<_> = branches.clone();
104 (*oid, branches)
105 })
106 .collect();
107 Self { branches }
108 }
109
110 pub fn dependents(
111 &self,
112 repo: &dyn crate::legacy::git::Repo,
113 base_oid: git2::Oid,
114 head_oid: git2::Oid,
115 ) -> Self {
116 let branches = self
117 .branches
118 .iter()
119 .filter(|(branch_oid, branch)| {
120 let is_shared_base = repo
121 .merge_base(**branch_oid, head_oid)
122 .map(|merge_oid| merge_oid == base_oid && **branch_oid != base_oid)
123 .unwrap_or(false);
124 let is_base_descendant = repo
125 .merge_base(**branch_oid, base_oid)
126 .map(|merge_oid| merge_oid == base_oid)
127 .unwrap_or(false);
128 if is_shared_base {
129 let first_branch = &branch.first().expect("we always have at least one branch");
130 log::trace!(
131 "Branch {} is not on the branch of HEAD ({})",
132 first_branch,
133 head_oid
134 );
135 false
136 } else if !is_base_descendant {
137 let first_branch = &branch.first().expect("we always have at least one branch");
138 log::trace!(
139 "Branch {} is not on the branch of {}",
140 first_branch,
141 base_oid
142 );
143 false
144 } else {
145 true
146 }
147 })
148 .map(|(oid, branches)| {
149 let branches: Vec<_> = branches.clone();
150 (*oid, branches)
151 })
152 .collect();
153 Self { branches }
154 }
155
156 pub fn branch(
157 &self,
158 repo: &dyn crate::legacy::git::Repo,
159 base_oid: git2::Oid,
160 head_oid: git2::Oid,
161 ) -> Self {
162 let branches = self
163 .branches
164 .iter()
165 .filter(|(branch_oid, branch)| {
166 let is_head_ancestor = repo
167 .merge_base(**branch_oid, head_oid)
168 .map(|merge_oid| **branch_oid == merge_oid)
169 .unwrap_or(false);
170 let is_base_descendant = repo
171 .merge_base(**branch_oid, base_oid)
172 .map(|merge_oid| merge_oid == base_oid)
173 .unwrap_or(false);
174 if !is_head_ancestor {
175 let first_branch = &branch.first().expect("we always have at least one branch");
176 log::trace!(
177 "Branch {} is not on the branch of HEAD ({})",
178 first_branch,
179 head_oid
180 );
181 false
182 } else if !is_base_descendant {
183 let first_branch = &branch.first().expect("we always have at least one branch");
184 log::trace!(
185 "Branch {} is not on the branch of {}",
186 first_branch,
187 base_oid
188 );
189 false
190 } else {
191 true
192 }
193 })
194 .map(|(oid, branches)| {
195 let branches: Vec<_> = branches.clone();
196 (*oid, branches)
197 })
198 .collect();
199 Self { branches }
200 }
201}
202
203impl IntoIterator for Branches {
204 type Item = (git2::Oid, Vec<crate::legacy::git::Branch>);
205 type IntoIter =
206 std::collections::btree_map::IntoIter<git2::Oid, Vec<crate::legacy::git::Branch>>;
207
208 fn into_iter(self) -> Self::IntoIter {
209 self.branches.into_iter()
210 }
211}
212
213pub fn find_protected_base<'b>(
214 repo: &dyn crate::legacy::git::Repo,
215 protected_branches: &'b Branches,
216 head_oid: git2::Oid,
217) -> Option<&'b crate::legacy::git::Branch> {
218 if let Some(head_branches) = protected_branches.get(head_oid) {
220 return head_branches.first();
221 }
222
223 let protected_base_oids = protected_branches
224 .oids()
225 .filter_map(|oid| {
226 let merge_oid = repo.merge_base(head_oid, oid)?;
227 Some((merge_oid, oid))
228 })
229 .collect::<Vec<_>>();
230
231 match protected_base_oids.len() {
233 0 => {
234 return None;
235 }
236 1 => {
237 let (_, protected_oid) = protected_base_oids[0];
238 return protected_branches
239 .get(protected_oid)
240 .expect("protected_oid came from protected_branches")
241 .first();
242 }
243 _ => {}
244 }
245
246 let mut next_oid = Some(head_oid);
248 while let Some(parent_oid) = next_oid {
249 if let Some((_, closest_common_oid)) = protected_base_oids
250 .iter()
251 .filter(|(base, _)| *base == parent_oid)
252 .min_by_key(|(base, branch)| {
253 (
254 repo.commit_count(*base, head_oid),
255 repo.commit_count(*base, *branch),
256 )
257 })
258 {
259 return protected_branches
260 .get(*closest_common_oid)
261 .expect("protected_oid came from protected_branches")
262 .first();
263 }
264 next_oid = repo
265 .parent_ids(parent_oid)
266 .expect("child_oid came from verified source")
267 .first()
268 .copied();
269 }
270
271 if let Some((_, closest_common_oid)) =
273 protected_base_oids.iter().min_by_key(|(base, protected)| {
274 let to_protected = repo.commit_count(*base, *protected);
275 let to_head = repo.commit_count(*base, head_oid);
276 (to_protected, to_head)
277 })
278 {
279 return protected_branches
280 .get(*closest_common_oid)
281 .expect("protected_oid came from protected_branches")
282 .first();
283 }
284
285 None
286}
287
288pub fn infer_base(repo: &dyn crate::legacy::git::Repo, head_oid: git2::Oid) -> Option<git2::Oid> {
289 let head_commit = repo.find_commit(head_oid)?;
290 let head_committer = head_commit.committer.clone();
291
292 let mut next_oid = head_oid;
293 loop {
294 let next_commit = repo.find_commit(next_oid)?;
295 if next_commit.committer != head_committer {
296 return Some(next_oid);
297 }
298 let parent_ids = repo.parent_ids(next_oid).ok()?;
299 match parent_ids.len() {
300 1 => {
301 next_oid = parent_ids[0];
302 }
303 _ => {
304 return Some(next_oid);
306 }
307 }
308 }
309}