Skip to main content

ytil_tui/
git_branch.rs

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
9/// A wrapper around [`Branch`] for display purposes.
10struct 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
34/// Prompts the user to select a branch from all available branches.
35///
36/// The previously checked-out branch (`@{-1}`) is placed first when available.
37/// Then up to seven branches whose latest commit was made with the configured
38/// `user.email` are shown before the remaining recency-ordered list.
39///
40/// # Errors
41/// - If repository discovery fails.
42/// - If [`ytil_git::branch::get_all_no_redundant`] fails.
43/// - If [`crate::minimal_select`] fails.
44pub 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}