1+ use std:: fmt;
2+ use std:: time:: { Duration , SystemTime } ;
3+
14use clap:: Parser ;
25use color_eyre:: Result ;
36use console:: { Emoji , style} ;
47use eyre:: Context ;
5- use git2:: { Branch , BranchType , PushOptions , Remote , RemoteCallbacks , Repository } ;
8+ use git2:: { Branch , BranchType , Error , PushOptions , Remote , RemoteCallbacks , Repository } ;
69use git2_credentials:: CredentialHandler ;
10+ use human_units:: FormatDuration ;
711use inquire:: error:: InquireError ;
12+ use inquire:: list_option:: ListOption ;
813use inquire:: ui:: { RenderConfig , Styled } ;
914use inquire:: { Confirm , MultiSelect } ;
15+ use verynicetable:: Table ;
1016
1117const EXCLUDES : & [ & str ] = & [ "master" , "main" , "develop" , "development" ] ;
1218
1319#[ derive( Parser ) ]
1420#[ command( author, version, about) ]
1521struct Cli { }
1622
17- fn get_branches ( repo : & Repository , names : Vec < String > ) -> Vec < Branch > {
18- names
19- . into_iter ( )
20- . filter_map ( |n| repo. find_branch ( & n, BranchType :: Local ) . ok ( ) )
21- . collect :: < Vec < Branch > > ( )
23+ struct BranchChoice < ' repo > {
24+ local : Branch < ' repo > ,
25+ upstream : Option < Branch < ' repo > > ,
26+ branch_name : String ,
27+ author_name : Option < String > ,
28+ commit_time : SystemTime ,
29+ }
30+
31+ impl < ' repo > fmt:: Display for BranchChoice < ' repo > {
32+ fn fmt ( & self , f : & mut fmt:: Formatter < ' _ > ) -> fmt:: Result {
33+ let upstream = if self . upstream . is_some ( ) {
34+ " (🔭)"
35+ } else {
36+ ""
37+ } ;
38+ let author = self . author_name . as_deref ( ) . unwrap_or ( "no-name" ) ;
39+ let dur = SystemTime :: now ( )
40+ . duration_since ( self . commit_time )
41+ . unwrap_or_default ( ) ;
42+ let ago = human_units:: Duration ( dur) . format_duration ( ) ;
43+ write ! (
44+ f,
45+ "{}{} 🧒 {} ⏰ {} ago" ,
46+ self . branch_name, upstream, author, ago
47+ )
48+ }
2249}
2350
24- fn show_list_of_branches ( branch_pairs : & Vec < ( Branch , Option < Branch > ) > ) {
25- let lines : Vec < String > = branch_pairs
51+ fn format_final_answers ( opts : & [ ListOption < & BranchChoice > ] ) -> String {
52+ let data : Vec < _ > = opts
2653 . iter ( )
27- . filter_map ( |( lb, rb) | {
28- let local_name = lb. name ( ) . ok ( ) ??;
29- let upstream_name = rb. as_ref ( ) . and_then ( |n| n. name ( ) . ok ( ) ) . flatten ( ) ;
30- let line = match upstream_name {
31- Some ( name) => format ! ( " {local_name} ({name})" ) ,
32- None => format ! ( " {local_name}" ) ,
33- } ;
34- Some ( line)
54+ . map ( |o| {
55+ let c = o. value ;
56+ let remote_name = c
57+ . upstream
58+ . as_ref ( )
59+ . and_then ( |b| b. name ( ) . ok ( ) )
60+ . flatten ( )
61+ . unwrap_or_default ( ) ;
62+ let author = c. author_name . as_deref ( ) . unwrap_or_default ( ) ;
63+ vec ! [ c. branch_name. as_str( ) , author, remote_name]
3564 } )
3665 . collect ( ) ;
37- eprintln ! ( "{}" , lines. join( "\n " ) ) ;
66+ let mut table = Table :: new ( ) ;
67+ table. headers ( & [ "Local" , "Author" , "Remote" ] ) . data ( & data) ;
68+ format ! ( "\n {table}" )
69+ }
70+
71+ fn get_branch_choices ( repo : & Repository ) -> Result < Vec < BranchChoice > , Error > {
72+ let branches = repo. branches ( Some ( BranchType :: Local ) ) ?;
73+ let mut choices: Vec < _ > = branches
74+ . flatten ( )
75+ . filter_map ( |( branch, _t) | {
76+ if branch. is_head ( ) {
77+ return None ;
78+ }
79+ let branch_name = branch. name ( ) . ok ( ) . flatten ( ) ?;
80+ if EXCLUDES . contains ( & branch_name) {
81+ return None ;
82+ }
83+ let branch_name = branch_name. to_string ( ) ;
84+ let upstream = branch. upstream ( ) . ok ( ) ;
85+ let commit = branch. get ( ) . peel_to_commit ( ) . ok ( ) ?;
86+ let secs = u64:: try_from ( commit. time ( ) . seconds ( ) ) . unwrap_or_default ( ) ;
87+ let commit_time = SystemTime :: UNIX_EPOCH . checked_add ( Duration :: from_secs ( secs) ) ?;
88+ let author = commit. author ( ) ;
89+ let author_name = author
90+ . name ( )
91+ . or_else ( || author. email ( ) . and_then ( |s| s. split ( '@' ) . next ( ) ) )
92+ . map ( |s| s. to_string ( ) ) ;
93+ Some ( BranchChoice {
94+ local : branch,
95+ upstream,
96+ branch_name,
97+ author_name,
98+ commit_time,
99+ } )
100+ } )
101+ . collect ( ) ;
102+ choices. sort_unstable_by_key ( |c| c. commit_time ) ;
103+ Ok ( choices)
38104}
39105
40106fn get_local_name < ' a > ( branch : & ' a Branch ) -> Option < & ' a str > {
@@ -55,7 +121,7 @@ fn delete_upstream_branch(
55121 let msg = format ! ( "Failed to delete upstream branch {}" , branch_name) ;
56122 eprintln ! ( "{} {}" , Emoji ( "⚠️" , "!" ) , style( msg) . yellow( ) ) ;
57123 }
58- branch. delete ( ) . ok ( )
124+ branch. delete ( ) . map_err ( |e| eprintln ! ( "{e}" ) ) . ok ( )
59125}
60126
61127fn get_render_config ( ) -> RenderConfig < ' static > {
@@ -71,23 +137,9 @@ fn main() -> Result<()> {
71137 Cli :: parse ( ) ;
72138 inquire:: set_global_render_config ( get_render_config ( ) ) ;
73139 let repo = Repository :: discover ( "." ) . wrap_err ( "Not a Git working folder" ) ?;
74- let branches = repo. branches ( Some ( BranchType :: Local ) ) ?;
75140 let staying_in_branch = repo. head ( ) . ok ( ) . map ( |r| r. is_branch ( ) ) . unwrap_or ( false ) ;
76- let names: Vec < String > = branches
77- . flatten ( )
78- . filter_map ( |( branch, _type) | {
79- if branch. is_head ( ) {
80- return None ;
81- }
82- let n = branch. name ( ) . ok ( ) ??;
83- if EXCLUDES . contains ( & n) {
84- None
85- } else {
86- Some ( n. to_string ( ) )
87- }
88- } )
89- . collect ( ) ;
90- if names. is_empty ( ) {
141+ let branch_choices = get_branch_choices ( & repo) ?;
142+ if branch_choices. is_empty ( ) {
91143 eprintln ! ( "No branches eligible to delete." ) ;
92144 if staying_in_branch {
93145 eprintln ! (
@@ -100,34 +152,33 @@ fn main() -> Result<()> {
100152 }
101153 return Ok ( ( ) ) ;
102154 }
103- let ans_branches = match MultiSelect :: new ( "Select branches to delete" , names) . prompt ( ) {
155+ let ans_branches = match MultiSelect :: new ( "Select branches to delete" , branch_choices)
156+ . with_formatter ( & format_final_answers)
157+ . prompt ( )
158+ {
104159 Ok ( ans) => ans,
105160 Err ( InquireError :: OperationCanceled ) => return Ok ( ( ) ) ,
106161 Err ( e) => return Err ( e. into ( ) ) ,
107162 } ;
108- let ans_up = match Confirm :: new ( "Do you want to delete the upstream branches also" )
163+ let ans_up = match Confirm :: new ( "Do you want to delete the upstream branches also? " )
109164 . with_default ( false )
110165 . prompt ( )
111166 {
112167 Ok ( ans) => ans,
113168 Err ( InquireError :: OperationCanceled ) => return Ok ( ( ) ) ,
114169 Err ( e) => return Err ( e. into ( ) ) ,
115170 } ;
116- let msg = if ans_up {
117- "To delete these branches and their upstream:"
118- } else {
119- "To delete these branches:"
171+ let ans_again = match Confirm :: new ( "Ready to delete?" )
172+ . with_default ( false )
173+ . prompt ( )
174+ {
175+ Ok ( ans) => ans,
176+ Err ( InquireError :: OperationCanceled ) => return Ok ( ( ) ) ,
177+ Err ( e) => return Err ( e. into ( ) ) ,
120178 } ;
121- eprintln ! ( "{}" , style( msg) . blue( ) ) ;
122- let local_branches = get_branches ( & repo, ans_branches) ;
123- let branch_pairs: Vec < ( Branch , Option < Branch > ) > = local_branches
124- . into_iter ( )
125- . map ( |b| {
126- let upstream = b. upstream ( ) . ok ( ) ;
127- ( b, upstream)
128- } )
129- . collect ( ) ;
130- show_list_of_branches ( & branch_pairs) ;
179+ if !ans_again {
180+ return Ok ( ( ) ) ;
181+ }
131182 let mut remote_callback = RemoteCallbacks :: new ( ) ;
132183 let git_config = git2:: Config :: open_default ( ) ?;
133184 let mut credential_handler = CredentialHandler :: new ( git_config) ;
@@ -146,9 +197,15 @@ fn main() -> Result<()> {
146197 let mut origin = repo. find_remote ( "origin" ) . ok ( ) ;
147198 let mut opts = PushOptions :: new ( ) ;
148199 opts. remote_callbacks ( remote_callback) ;
149- for ( mut lb, rb) in branch_pairs {
150- lb. delete ( ) . ok ( ) ;
151- if let Some ( ( orig, branch) ) = origin. as_mut ( ) . zip ( rb) {
200+ for mut c in ans_branches {
201+ c. local
202+ . delete ( )
203+ . map_err ( |e| eprintln ! ( "{e}" ) )
204+ . unwrap_or_default ( ) ;
205+ if !ans_up {
206+ continue ;
207+ }
208+ if let Some ( ( orig, branch) ) = origin. as_mut ( ) . zip ( c. upstream ) {
152209 delete_upstream_branch ( branch, orig, & mut opts) ;
153210 } ;
154211 }
0 commit comments