@@ -43,6 +43,63 @@ pub fn remote_branches(repo: &Path, remote: &str) -> Result<HashSet<String>, Cli
4343 . collect ( ) )
4444}
4545
46+ pub fn default_branch ( repo : & Path , remote : & str ) -> Result < String , CliError > {
47+ choose_default_branch (
48+ local_config_value ( repo, "branp.worktree.defaultBranch" ) ,
49+ live_remote_head_branch ( repo, remote) ,
50+ current_branch ( repo) . ok ( ) . flatten ( ) ,
51+ cached_remote_head_branch ( repo, remote) ,
52+ local_config_value ( repo, "init.defaultBranch" ) ,
53+ )
54+ }
55+
56+ fn choose_default_branch (
57+ explicit_config : Option < String > ,
58+ live_remote_head : Option < String > ,
59+ current_branch : Option < String > ,
60+ cached_remote_head : Option < String > ,
61+ init_default_branch : Option < String > ,
62+ ) -> Result < String , CliError > {
63+ explicit_config
64+ . and_then ( |branch| non_empty_trimmed ( & branch) )
65+ . or_else ( || live_remote_head. and_then ( |branch| non_empty_trimmed ( & branch) ) )
66+ . or_else ( || current_branch. and_then ( |branch| non_empty_trimmed ( & branch) ) )
67+ . or_else ( || cached_remote_head. and_then ( |branch| non_empty_trimmed ( & branch) ) )
68+ . or_else ( || init_default_branch. and_then ( |branch| non_empty_trimmed ( & branch) ) )
69+ . ok_or_else ( || {
70+ CliError :: from (
71+ "could not determine default branch from branp.worktree.defaultBranch, origin HEAD, the current branch, origin/HEAD, or init.defaultBranch" ,
72+ )
73+ } )
74+ }
75+
76+ fn local_config_value ( repo : & Path , key : & str ) -> Option < String > {
77+ output ( repo, & [ "config" , "--local" , "--get" , key] ) . ok ( ) . and_then ( |value| non_empty_trimmed ( & value) )
78+ }
79+
80+ fn live_remote_head_branch ( repo : & Path , remote : & str ) -> Option < String > {
81+ output ( repo, & [ "ls-remote" , "--symref" , remote, "HEAD" ] ) . ok ( ) . and_then ( |output| parse_remote_head ( & output) )
82+ }
83+
84+ fn parse_remote_head ( output : & str ) -> Option < String > {
85+ output
86+ . lines ( )
87+ . find_map ( |line| line. strip_prefix ( "ref: refs/heads/" ) . and_then ( |line| line. strip_suffix ( "\t HEAD" ) ) . map ( str:: to_string) )
88+ . and_then ( |branch| ( !branch. is_empty ( ) ) . then_some ( branch) )
89+ }
90+
91+ fn cached_remote_head_branch ( repo : & Path , remote : & str ) -> Option < String > {
92+ output ( repo, & [ "symbolic-ref" , "--short" , & format ! ( "refs/remotes/{remote}/HEAD" ) ] )
93+ . ok ( )
94+ . and_then ( |value| value. trim ( ) . strip_prefix ( & format ! ( "{remote}/" ) ) . map ( str:: to_string) )
95+ . and_then ( |branch| ( !branch. is_empty ( ) ) . then_some ( branch) )
96+ }
97+
98+ fn non_empty_trimmed ( value : & str ) -> Option < String > {
99+ let value = value. trim ( ) ;
100+ ( !value. is_empty ( ) ) . then ( || value. to_string ( ) )
101+ }
102+
46103pub fn delete_local_branch ( repo : & Path , branch : & str , force : bool ) -> CliResult {
47104 if !local_branch_exists ( repo, branch) ? {
48105 return Ok ( ( ) ) ;
@@ -72,3 +129,47 @@ fn parse_github_url(url: &str) -> Option<GitHubRepo> {
72129
73130 Some ( GitHubRepo { owner : owner. to_string ( ) , name : name. to_string ( ) } )
74131}
132+
133+ #[ cfg( test) ]
134+ mod tests {
135+ use super :: * ;
136+
137+ #[ test]
138+ fn chooses_default_branch_from_best_available_signal ( ) {
139+ assert_eq ! (
140+ choose_default_branch(
141+ Some ( "configured" . to_string( ) ) ,
142+ Some ( "remote" . to_string( ) ) ,
143+ Some ( "base" . to_string( ) ) ,
144+ Some ( "cached" . to_string( ) ) ,
145+ Some ( "init" . to_string( ) ) ,
146+ )
147+ . unwrap( ) ,
148+ "configured"
149+ ) ;
150+ assert_eq ! (
151+ choose_default_branch( None , Some ( "remote" . to_string( ) ) , Some ( "base" . to_string( ) ) , Some ( "cached" . to_string( ) ) , Some ( "init" . to_string( ) ) )
152+ . unwrap( ) ,
153+ "remote"
154+ ) ;
155+ assert_eq ! (
156+ choose_default_branch( None , None , Some ( "base" . to_string( ) ) , Some ( "cached" . to_string( ) ) , Some ( "init" . to_string( ) ) ) . unwrap( ) ,
157+ "base"
158+ ) ;
159+ assert_eq ! ( choose_default_branch( None , None , None , Some ( "cached" . to_string( ) ) , Some ( "init" . to_string( ) ) ) . unwrap( ) , "cached" ) ;
160+ assert_eq ! ( choose_default_branch( None , None , None , None , Some ( "init" . to_string( ) ) ) . unwrap( ) , "init" ) ;
161+ assert ! ( choose_default_branch( None , None , None , None , None ) . is_err( ) ) ;
162+ }
163+
164+ #[ test]
165+ fn current_branch_beats_stale_cached_remote_head ( ) {
166+ assert_eq ! ( choose_default_branch( None , None , Some ( "dev" . to_string( ) ) , Some ( "master" . to_string( ) ) , None ) . unwrap( ) , "dev" ) ;
167+ }
168+
169+ #[ test]
170+ fn parses_live_remote_head ( ) {
171+ let output = "ref: refs/heads/dev\t HEAD\n f7dbd99e3616315c360db815f2a09e03d38ac669\t HEAD\n " ;
172+ assert_eq ! ( parse_remote_head( output) . as_deref( ) , Some ( "dev" ) ) ;
173+ assert_eq ! ( parse_remote_head( "f7dbd99e3616315c360db815f2a09e03d38ac669\t HEAD\n " ) , None ) ;
174+ }
175+ }
0 commit comments