git_disjoint/
issue_group_map.rs1use std::{
2 collections::HashSet,
3 error::Error,
4 fmt::Display,
5 io::{self, Write},
6};
7
8use git2::Commit;
9use indexmap::IndexMap;
10
11use crate::{
12 cli::{
13 CommitGrouping, CommitsToConsider, OverlayCommitsIntoOnePullRequest,
14 PromptUserToChooseCommits,
15 },
16 interact::{prompt_user, IssueGroupWhitelist, SelectIssuesError},
17 issue::Issue,
18 issue_group::{self, GitCommitSummary, IssueGroup},
19};
20
21#[derive(Debug, Default)]
22pub struct IssueGroupMap<'repo>(IndexMap<IssueGroup, Vec<Commit<'repo>>>);
23
24impl<'repo> IntoIterator for IssueGroupMap<'repo> {
25 type Item = (IssueGroup, Vec<Commit<'repo>>);
26
27 type IntoIter = indexmap::map::IntoIter<IssueGroup, Vec<Commit<'repo>>>;
28
29 fn into_iter(self) -> Self::IntoIter {
30 self.0.into_iter()
31 }
32}
33
34impl<'repo> FromIterator<(IssueGroup, Vec<Commit<'repo>>)> for IssueGroupMap<'repo> {
35 fn from_iter<T: IntoIterator<Item = (IssueGroup, Vec<Commit<'repo>>)>>(iter: T) -> Self {
36 Self(iter.into_iter().collect())
37 }
38}
39
40#[derive(Debug)]
41#[non_exhaustive]
42pub struct FromCommitsError {
43 kind: FromCommitsErrorKind,
44}
45
46impl Display for FromCommitsError {
47 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48 match &self.kind {
49 FromCommitsErrorKind::FromCommit(_) => write!(f, "unable to get commit summary"),
50 FromCommitsErrorKind::IO(_) => write!(f, "unable to write to stream"),
51 }
52 }
53}
54
55impl Error for FromCommitsError {
56 fn source(&self) -> Option<&(dyn Error + 'static)> {
57 match &self.kind {
58 FromCommitsErrorKind::FromCommit(err) => Some(err),
59 FromCommitsErrorKind::IO(err) => Some(err),
60 }
61 }
62}
63
64#[derive(Debug)]
65pub enum FromCommitsErrorKind {
66 #[non_exhaustive]
67 FromCommit(issue_group::FromCommitError),
68 #[non_exhaustive]
69 IO(io::Error),
70}
71
72impl From<issue_group::FromCommitError> for FromCommitsError {
73 fn from(err: issue_group::FromCommitError) -> Self {
74 Self {
75 kind: FromCommitsErrorKind::FromCommit(err),
76 }
77 }
78}
79
80impl From<io::Error> for FromCommitsError {
81 fn from(err: io::Error) -> Self {
82 Self {
83 kind: FromCommitsErrorKind::IO(err),
84 }
85 }
86}
87
88impl<'repo> IssueGroupMap<'repo> {
89 fn with_capacity(n: usize) -> Self {
90 Self(IndexMap::with_capacity(n))
91 }
92
93 fn insert(&mut self, key: IssueGroup, value: Vec<Commit<'repo>>) {
94 self.0.insert(key, value);
95 }
96
97 pub fn try_from_commits<I>(
98 commits: I,
99 commits_to_consider: CommitsToConsider,
100 commit_grouping: CommitGrouping,
101 ) -> Result<Self, FromCommitsError>
102 where
103 I: IntoIterator<Item = Commit<'repo>>,
104 {
105 let mut suffix: u32 = 0;
106 let mut seen_issue_groups = HashSet::new();
107 let commits_by_issue: IndexMap<IssueGroup, Vec<Commit>> = commits
108 .into_iter()
109 .map(
111 |commit| -> Result<Option<(IssueGroup, Commit)>, FromCommitsError> {
112 let issue = commit.message().and_then(Issue::parse_from_commit_message);
113 if commit_grouping == CommitGrouping::ByIssue {
118 if let Some(issue) = issue {
119 return Ok(Some((issue.into(), commit)));
120 }
121 }
122
123 if commit_grouping == CommitGrouping::Individual
128 || commits_to_consider == CommitsToConsider::All
129 {
130 let summary: GitCommitSummary = (&commit).try_into()?;
131 let mut proposed_issue_group = summary.clone();
132
133 while seen_issue_groups.contains(&proposed_issue_group) {
139 suffix += 1;
140 proposed_issue_group = GitCommitSummary(format!("{summary}_{suffix}"));
141 }
142
143 seen_issue_groups.insert(proposed_issue_group.clone());
144
145 return Ok(Some((IssueGroup::Commit(proposed_issue_group), commit)));
146 }
147
148 writeln!(
150 io::stderr(),
151 "Warning: ignoring commit without issue trailer: {:?}",
152 commit.id()
153 )?;
154 Ok(None)
155 },
156 )
157 .filter_map(Result::transpose)
158 .try_fold(
159 Default::default(),
160 |mut map,
161 maybe_tuple|
162 -> Result<IndexMap<IssueGroup, Vec<Commit>>, FromCommitsError> {
163 let (issue, commit) = maybe_tuple?;
164 let commits = map.entry(issue).or_default();
165 commits.push(commit);
166 Ok(map)
167 },
168 )?;
169
170 Ok(Self(commits_by_issue))
171 }
172
173 pub fn select_issues(
174 self,
175 choose: PromptUserToChooseCommits,
176 overlay: OverlayCommitsIntoOnePullRequest,
177 ) -> Result<Self, SelectIssuesError> {
178 let selected_issue_groups: IssueGroupWhitelist = {
179 if choose == PromptUserToChooseCommits::No
180 && overlay == OverlayCommitsIntoOnePullRequest::No
181 {
182 IssueGroupWhitelist::WhitelistDNE
183 } else {
184 let keys = self.0.keys();
185 IssueGroupWhitelist::Whitelist(prompt_user(keys)?)
186 }
187 };
188
189 Ok(match &selected_issue_groups {
190 IssueGroupWhitelist::Whitelist(whitelist) => self
192 .into_iter()
193 .filter(|(issue_group, _commits)| whitelist.contains(issue_group))
194 .collect(),
195 IssueGroupWhitelist::WhitelistDNE => self,
197 })
198 }
199
200 pub fn apply_overlay(self, overlay: OverlayCommitsIntoOnePullRequest) -> Self {
201 match overlay {
202 OverlayCommitsIntoOnePullRequest::Yes => self
205 .into_iter()
206 .reduce(|mut accumulator, mut item| {
207 accumulator.1.append(&mut item.1);
208 accumulator
209 })
210 .map(|(issue_group, commits)| {
212 let mut map = Self::with_capacity(1);
213 map.insert(issue_group, commits);
214 map
215 })
216 .unwrap_or_default(),
217 OverlayCommitsIntoOnePullRequest::No => self,
219 }
220 }
221}