1use std::fmt::Display;
2use std::fmt::Formatter;
3use std::ops::Deref;
4use std::path::Path;
5
6use owo_colors::OwoColorize;
7use rootcause::prelude::ResultExt;
8use ytil_git::branch::Branch;
9
10pub fn select() -> rootcause::Result<Option<Branch>> {
21 let repo = ytil_git::repo::discover(Path::new(".")).context("error discovering repo for branch selection")?;
22 let branches = prioritize_branches(
23 ytil_git::branch::get_all_no_redundant(&repo)?,
24 ytil_git::branch::get_previous(&repo).as_deref(),
25 ytil_git::branch::get_user_email(&repo)?.as_deref(),
26 );
27
28 let Some(branch) = crate::minimal_select(branches.into_iter().map(RenderableBranch).collect())? else {
29 return Ok(None);
30 };
31
32 Ok(Some(branch.0))
33}
34
35struct RenderableBranch(pub Branch);
37
38impl Deref for RenderableBranch {
39 type Target = Branch;
40
41 fn deref(&self) -> &Self::Target {
42 &self.0
43 }
44}
45
46impl Display for RenderableBranch {
47 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
48 let styled_date_time = format!("({})", self.committer_date_time());
49 let styled_email = format!("<{}>", self.committer_email());
50 write!(
51 f,
52 "{} {} {}",
53 self.name(),
54 styled_date_time.green(),
55 styled_email.blue().bold(),
56 )
57 }
58}
59
60fn prioritize_branches(branches: Vec<Branch>, previous_branch: Option<&str>, user_email: Option<&str>) -> Vec<Branch> {
61 const MINE_DESIRED_COUNT: usize = 5;
62
63 let branches_len = branches.len();
64 let mut previous = None;
65 let mut mine = Vec::new();
66 let mut rest = Vec::new();
67
68 for branch in branches {
69 if previous.is_none() && previous_branch.is_some_and(|prev| branch.name_no_origin() == prev) {
70 previous = Some(branch);
71 } else if mine.len() < MINE_DESIRED_COUNT && user_email.is_some_and(|email| branch.committer_email() == email) {
72 mine.push(branch);
73 } else {
74 rest.push(branch);
75 }
76 }
77
78 let mut prioritized = Vec::with_capacity(branches_len);
79 prioritized.extend(previous);
80 prioritized.extend(mine);
81 prioritized.extend(rest);
82 prioritized
83}
84
85#[cfg(test)]
86mod tests {
87 use chrono::DateTime;
88 use chrono::Utc;
89 use rstest::rstest;
90 use ytil_git::branch::Branch;
91
92 use super::*;
93
94 #[rstest]
95 #[case(
96 vec![
97 branch("main", "other@example.com", 30),
98 branch("feature-a", "me@example.com", 20),
99 branch("feature-b", "me@example.com", 10),
100 ],
101 Some("feature-b"),
102 Some("me@example.com"),
103 vec![
104 branch("feature-b", "me@example.com", 10),
105 branch("feature-a", "me@example.com", 20),
106 branch("main", "other@example.com", 30),
107 ]
108 )]
109 #[case(
110 vec![
111 remote_branch("origin/feature-a", "me@example.com", 30),
112 branch("main", "other@example.com", 20),
113 ],
114 Some("feature-a"),
115 Some("me@example.com"),
116 vec![
117 remote_branch("origin/feature-a", "me@example.com", 30),
118 branch("main", "other@example.com", 20),
119 ]
120 )]
121 #[case(
122 vec![
123 branch("main", "other@example.com", 30),
124 branch("feature-a", "me@example.com", 20),
125 branch("feature-b", "me@example.com", 10),
126 ],
127 Some("feature-a"),
128 Some("me@example.com"),
129 vec![
130 branch("feature-a", "me@example.com", 20),
131 branch("feature-b", "me@example.com", 10),
132 branch("main", "other@example.com", 30),
133 ]
134 )]
135 fn prioritize_branches_prioritizes_previous_branch_cases(
136 #[case] branches: Vec<Branch>,
137 #[case] previous_branch: Option<&str>,
138 #[case] user_email: Option<&str>,
139 #[case] expected: Vec<Branch>,
140 ) {
141 let ordered = prioritize_branches(branches, previous_branch, user_email);
142
143 pretty_assertions::assert_eq!(ordered, expected);
144 }
145
146 #[test]
147 fn test_prioritize_branches_puts_only_the_wanted_number_of_branches_matching_email_before_rest() {
148 let branches = vec![
149 branch("other-1", "other@example.com", 100),
150 branch("mine-1", "me@example.com", 99),
151 branch("mine-2", "me@example.com", 98),
152 branch("mine-3", "me@example.com", 97),
153 branch("mine-4", "me@example.com", 96),
154 branch("mine-5", "me@example.com", 95),
155 branch("mine-6", "me@example.com", 94),
156 branch("mine-7", "me@example.com", 93),
157 branch("mine-8", "me@example.com", 92),
158 branch("other-2", "other@example.com", 91),
159 ];
160
161 let ordered = prioritize_branches(branches, None, Some("me@example.com"));
162
163 pretty_assertions::assert_eq!(
164 &ordered,
165 &[
166 branch("mine-1", "me@example.com", 99),
167 branch("mine-2", "me@example.com", 98),
168 branch("mine-3", "me@example.com", 97),
169 branch("mine-4", "me@example.com", 96),
170 branch("mine-5", "me@example.com", 95),
171 branch("other-1", "other@example.com", 100),
172 branch("mine-6", "me@example.com", 94),
173 branch("mine-7", "me@example.com", 93),
174 branch("mine-8", "me@example.com", 92),
175 branch("other-2", "other@example.com", 91),
176 ],
177 );
178 }
179
180 #[rstest]
181 #[case(
182 vec![
183 branch("other-1", "other@example.com", 100),
184 branch("mine-1", "me@example.com", 99),
185 branch("other-2", "other@example.com", 98),
186 branch("mine-2", "me@example.com", 97),
187 ],
188 None,
189 Some("me@example.com"),
190 vec![
191 branch("mine-1", "me@example.com", 99),
192 branch("mine-2", "me@example.com", 97),
193 branch("other-1", "other@example.com", 100),
194 branch("other-2", "other@example.com", 98),
195 ]
196 )]
197 #[case(
198 vec![
199 branch("main", "other@example.com", 30),
200 branch("feature-a", "me@example.com", 20),
201 branch("feature-b", "me@example.com", 10),
202 ],
203 Some("feature-b"),
204 None,
205 vec![
206 branch("feature-b", "me@example.com", 10),
207 branch("main", "other@example.com", 30),
208 branch("feature-a", "me@example.com", 20),
209 ]
210 )]
211 #[case(
212 vec![
213 branch("main", "other@example.com", 30),
214 branch("feature-a", "other@example.com", 20),
215 ],
216 Some("missing"),
217 None,
218 vec![
219 branch("main", "other@example.com", 30),
220 branch("feature-a", "other@example.com", 20),
221 ]
222 )]
223 #[case(
224 vec![
225 branch("main", "other@example.com", 30),
226 branch("feature-a", "other@example.com", 20),
227 ],
228 None,
229 None,
230 vec![
231 branch("main", "other@example.com", 30),
232 branch("feature-a", "other@example.com", 20),
233 ]
234 )]
235 fn prioritize_branches_misc_cases(
236 #[case] branches: Vec<Branch>,
237 #[case] previous_branch: Option<&str>,
238 #[case] user_email: Option<&str>,
239 #[case] expected: Vec<Branch>,
240 ) {
241 let ordered = prioritize_branches(branches, previous_branch, user_email);
242
243 pretty_assertions::assert_eq!(ordered, expected);
244 }
245
246 fn branch(name: &str, email: &str, timestamp: i64) -> Branch {
247 Branch::Local {
248 name: name.to_string(),
249 committer_email: email.to_string(),
250 committer_date_time: DateTime::<Utc>::from_timestamp(timestamp, 0).unwrap(),
251 }
252 }
253
254 fn remote_branch(name: &str, email: &str, timestamp: i64) -> Branch {
255 Branch::Remote {
256 name: name.to_string(),
257 committer_email: email.to_string(),
258 committer_date_time: DateTime::<Utc>::from_timestamp(timestamp, 0).unwrap(),
259 }
260 }
261}