git_stack/legacy/git/
branches.rs

1#[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    // We're being asked about a protected branch
219    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    // Not much choice for applicable base
232    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    // Prefer protected branch from first parent
247    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    // Prefer most direct ancestors
272    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                // Assume merge-commits are topic branches being merged into the upstream
305                return Some(next_oid);
306            }
307        }
308    }
309}