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