1use std::fmt::Display;
4use std::fmt::Formatter;
5use std::ops::Deref;
6use std::path::Path;
7
8use owo_colors::OwoColorize;
9use rootcause::prelude::ResultExt;
10use ytil_git::branch::Branch;
11
12pub fn run() -> rootcause::Result<()> {
13 let Some(branch) = select_branch_with_current_first()? else {
14 return Ok(());
15 };
16
17 println!("{}", branch.name_no_origin());
18 Ok(())
19}
20
21fn select_branch_with_current_first() -> rootcause::Result<Option<Branch>> {
22 let repo = ytil_git::repo::discover(Path::new(".")).context("error discovering repo for branch selection")?;
23 let branches = prioritize_current_branch_first(
24 ytil_git::branch::get_all_no_redundant(&repo)?,
25 ytil_git::branch::get_current()?.as_str(),
26 ytil_git::branch::get_previous(&repo).as_deref(),
27 ytil_git::branch::get_user_email(&repo)?.as_deref(),
28 );
29
30 let Some(branch) = ytil_tui::minimal_select(branches.into_iter().map(RenderableBranch).collect())? else {
31 return Ok(None);
32 };
33
34 Ok(Some(branch.0))
35}
36
37fn prioritize_current_branch_first(
38 branches: Vec<Branch>,
39 current_branch: &str,
40 previous_branch: Option<&str>,
41 user_email: Option<&str>,
42) -> Vec<Branch> {
43 let branches = prioritize_recent_branches(branches, previous_branch, user_email);
44 let mut current = None;
45 let mut rest = Vec::with_capacity(branches.len());
46
47 for branch in branches {
48 if current.is_none() && branch.name_no_origin() == current_branch {
49 current = Some(branch);
50 } else {
51 rest.push(branch);
52 }
53 }
54
55 current.into_iter().chain(rest).collect()
56}
57
58fn prioritize_recent_branches(
59 branches: Vec<Branch>,
60 previous_branch: Option<&str>,
61 user_email: Option<&str>,
62) -> Vec<Branch> {
63 const MINE_DESIRED_COUNT: usize = 5;
64
65 let branches_len = branches.len();
66 let mut previous = None;
67 let mut mine = Vec::new();
68 let mut rest = Vec::new();
69
70 for branch in branches {
71 if previous.is_none() && previous_branch.is_some_and(|prev| branch.name_no_origin() == prev) {
72 previous = Some(branch);
73 } else if mine.len() < MINE_DESIRED_COUNT && user_email.is_some_and(|email| branch.committer_email() == email) {
74 mine.push(branch);
75 } else {
76 rest.push(branch);
77 }
78 }
79
80 let mut prioritized = Vec::with_capacity(branches_len);
81 prioritized.extend(previous);
82 prioritized.extend(mine);
83 prioritized.extend(rest);
84 prioritized
85}
86
87struct RenderableBranch(pub Branch);
88
89impl Deref for RenderableBranch {
90 type Target = Branch;
91
92 fn deref(&self) -> &Self::Target {
93 &self.0
94 }
95}
96
97impl Display for RenderableBranch {
98 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
99 let styled_date_time = format!("({})", self.committer_date_time());
100 let styled_email = format!("<{}>", self.committer_email());
101 write!(
102 f,
103 "{} {} {}",
104 self.name(),
105 styled_date_time.green(),
106 styled_email.blue().bold(),
107 )
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use jiff::Timestamp;
114 use rstest::rstest;
115 use test_that::prelude::*;
116
117 use super::*;
118
119 #[rstest]
120 #[case(
121 vec![branch("main", 30), branch("feature-a", 20), branch("feature-b", 10)],
122 "feature-b",
123 vec![branch("feature-b", 10), branch("main", 30), branch("feature-a", 20)]
124 )]
125 #[case(
126 vec![remote_branch("origin/feature-a", 30), branch("main", 20)],
127 "feature-a",
128 vec![remote_branch("origin/feature-a", 30), branch("main", 20)]
129 )]
130 #[case(
131 vec![branch("main", 30), branch("feature-a", 20)],
132 "missing",
133 vec![branch("main", 30), branch("feature-a", 20)]
134 )]
135 fn test_prioritize_current_branch_first_when_current_branch_varies_orders_expected_branches(
136 #[case] branches: Vec<Branch>,
137 #[case] current_branch: &str,
138 #[case] expected: Vec<Branch>,
139 ) {
140 assert_that!(
141 prioritize_current_branch_first(branches, current_branch, None, None),
142 eq(expected)
143 );
144 }
145
146 #[test]
147 fn test_prioritize_current_branch_first_preserves_gcu_recent_order_after_current() {
148 let branches = vec![
149 branch_with_email("other-1", "other@example.com", 100),
150 branch_with_email("mine-1", "me@example.com", 99),
151 branch_with_email("previous", "other@example.com", 98),
152 branch_with_email("current", "me@example.com", 97),
153 branch_with_email("mine-2", "me@example.com", 96),
154 ];
155
156 assert_that!(
157 prioritize_current_branch_first(branches, "current", Some("previous"), Some("me@example.com")),
158 eq(vec![
159 branch_with_email("current", "me@example.com", 97),
160 branch_with_email("previous", "other@example.com", 98),
161 branch_with_email("mine-1", "me@example.com", 99),
162 branch_with_email("mine-2", "me@example.com", 96),
163 branch_with_email("other-1", "other@example.com", 100),
164 ])
165 );
166 }
167
168 fn branch(name: &str, timestamp: i64) -> Branch {
169 branch_with_email(name, "me@example.com", timestamp)
170 }
171
172 fn branch_with_email(name: &str, email: &str, timestamp: i64) -> Branch {
173 Branch::Local {
174 name: name.to_string(),
175 committer_email: email.to_string(),
176 committer_date_time: Timestamp::from_second(timestamp).unwrap(),
177 }
178 }
179
180 fn remote_branch(name: &str, timestamp: i64) -> Branch {
181 Branch::Remote {
182 name: name.to_string(),
183 committer_email: "me@example.com".to_string(),
184 committer_date_time: Timestamp::from_second(timestamp).unwrap(),
185 }
186 }
187}