Skip to main content

git_disjoint/
disjoint_branch.rs

1use std::{collections::HashSet, error::Error, fmt::Display};
2
3use git2::Commit;
4use indexmap::IndexMap;
5
6use crate::{branch_name::BranchName, issue_group::IssueGroup, issue_group_map::IssueGroupMap};
7
8#[derive(Debug)]
9pub struct DisjointBranch<'repo> {
10    // REFACTOR: make this private
11    pub branch_name: BranchName,
12    // REFACTOR: make this private
13    pub commits: Vec<Commit<'repo>>,
14}
15
16#[derive(Debug)]
17pub struct DisjointBranchMap<'repo>(IndexMap<IssueGroup, DisjointBranch<'repo>>);
18
19#[derive(Debug)]
20#[non_exhaustive]
21pub struct FromIssueGroupMapError {
22    kind: FromIssueGroupMapErrorKind,
23}
24
25impl Display for FromIssueGroupMapError {
26    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27        match &self.kind {
28            FromIssueGroupMapErrorKind::InvalidUtf8(commit) => {
29                write!(f, "commit summary contains invalid UTF-8: {}", commit)
30            }
31        }
32    }
33}
34
35impl Error for FromIssueGroupMapError {
36    fn source(&self) -> Option<&(dyn Error + 'static)> {
37        match &self.kind {
38            FromIssueGroupMapErrorKind::InvalidUtf8(_) => None,
39        }
40    }
41}
42
43#[derive(Debug)]
44pub enum FromIssueGroupMapErrorKind {
45    #[non_exhaustive]
46    InvalidUtf8(String),
47}
48
49impl From<FromIssueGroupMapErrorKind> for FromIssueGroupMapError {
50    fn from(kind: FromIssueGroupMapErrorKind) -> Self {
51        Self { kind }
52    }
53}
54
55impl<'repo> DisjointBranchMap<'repo> {
56    pub fn iter(&self) -> indexmap::map::Iter<'_, IssueGroup, DisjointBranch<'repo>> {
57        self.0.iter()
58    }
59
60    pub fn is_empty(&self) -> bool {
61        self.0.is_empty()
62    }
63}
64
65impl<'repo> FromIterator<(IssueGroup, DisjointBranch<'repo>)> for DisjointBranchMap<'repo> {
66    fn from_iter<T: IntoIterator<Item = (IssueGroup, DisjointBranch<'repo>)>>(iter: T) -> Self {
67        Self(iter.into_iter().collect())
68    }
69}
70
71impl<'repo> IntoIterator for DisjointBranchMap<'repo> {
72    type Item = (IssueGroup, DisjointBranch<'repo>);
73
74    type IntoIter = indexmap::map::IntoIter<IssueGroup, DisjointBranch<'repo>>;
75
76    fn into_iter(self) -> Self::IntoIter {
77        self.0.into_iter()
78    }
79}
80
81impl<'repo> TryFrom<IssueGroupMap<'repo>> for DisjointBranchMap<'repo> {
82    type Error = FromIssueGroupMapError;
83
84    /// Plan out branch names to avoid collisions.
85    ///
86    /// This function does not take into account existing branch names in the local
87    /// or remote repository. It only looks at branch names that git-disjoint is
88    /// going to generate to make sure one invocation of git-disjoint won't try to
89    /// create a branch with the same name twice.
90    fn try_from(commits_by_issue_group: IssueGroupMap<'repo>) -> Result<Self, Self::Error> {
91        let mut suffix: u32 = 0;
92        let mut seen_branch_names = HashSet::new();
93        commits_by_issue_group
94            .into_iter()
95            .map(|(issue_group, commits)| {
96                // Grab the first summary to convert into a branch name.
97                // We only choose the first summary because we know each Vec is
98                // non-empty and the first element is convenient.
99                let summary = {
100                    let commit = &commits[0];
101                    commit.summary().ok_or_else(|| {
102                        FromIssueGroupMapErrorKind::InvalidUtf8(commit.id().to_string())
103                    })?
104                };
105                let generated_branch_name = BranchName::from_issue_group(&issue_group, summary);
106                let mut proposed_branch_name = generated_branch_name.clone();
107
108                while seen_branch_names.contains(&proposed_branch_name) {
109                    suffix += 1;
110                    // OPTIMIZE: no need to call sanitize_git_ref here again
111                    proposed_branch_name = format!("{generated_branch_name}_{suffix}").into();
112                }
113
114                seen_branch_names.insert(proposed_branch_name.clone());
115
116                Ok((
117                    issue_group,
118                    DisjointBranch {
119                        branch_name: proposed_branch_name,
120                        commits,
121                    },
122                ))
123            })
124            .collect()
125    }
126}