@@ -2,7 +2,7 @@ use anyhow::{Context, Result};
22use clap:: { Subcommand , ValueEnum } ;
33use std:: process:: Command ;
44
5- use crate :: utils:: confirm;
5+ use crate :: utils:: { confirm, find_repo_root } ;
66
77#[ derive( Subcommand ) ]
88pub enum ConfigCommand {
@@ -13,7 +13,13 @@ pub enum ConfigCommand {
1313 yes : bool ,
1414 #[ arg( long) ]
1515 dry_run : bool ,
16+ #[ arg( long, conflicts_with = "local" ) ]
17+ global : bool ,
18+ #[ arg( long, conflicts_with = "global" ) ]
19+ local : bool ,
1620 } ,
21+ /// Show current git config values
22+ Show ,
1723}
1824
1925#[ derive( ValueEnum , Clone ) ]
@@ -27,15 +33,90 @@ pub enum Preset {
2733}
2834
2935pub fn run ( cmd : ConfigCommand ) -> Result < ( ) > {
30- let ConfigCommand :: Apply {
31- preset,
32- yes,
33- dry_run,
34- } = cmd;
35- match preset {
36- Preset :: Defaults => apply_defaults ( dry_run) ,
37- Preset :: Advanced => apply_advanced ( dry_run) ,
38- Preset :: Delta => apply_delta ( yes, dry_run) ,
36+ match cmd {
37+ ConfigCommand :: Apply {
38+ preset,
39+ yes,
40+ dry_run,
41+ global,
42+ local,
43+ } => {
44+ let scope = determine_scope ( global, local) ;
45+ match preset {
46+ Preset :: Defaults => apply_defaults ( dry_run, scope) ,
47+ Preset :: Advanced => apply_advanced ( dry_run, scope) ,
48+ Preset :: Delta => apply_delta ( yes, dry_run, scope) ,
49+ }
50+ }
51+ ConfigCommand :: Show => show_config ( ) ,
52+ }
53+ }
54+
55+ #[ derive( Clone , Copy ) ]
56+ pub ( crate ) enum ConfigScope {
57+ Global ,
58+ Local ,
59+ }
60+
61+ fn determine_scope ( global : bool , local : bool ) -> ConfigScope {
62+ if global {
63+ ConfigScope :: Global
64+ } else if local || find_repo_root ( ) . is_ok ( ) {
65+ ConfigScope :: Local
66+ } else {
67+ ConfigScope :: Global
68+ }
69+ }
70+
71+ fn scope_flag ( scope : ConfigScope ) -> & ' static str {
72+ match scope {
73+ ConfigScope :: Global => "--global" ,
74+ ConfigScope :: Local => "--local" ,
75+ }
76+ }
77+
78+ fn show_config ( ) -> Result < ( ) > {
79+ println ! ( "Git config (global):" ) ;
80+ show_scope_config ( "--global" ) ;
81+ println ! ( ) ;
82+ println ! ( "Git config (local):" ) ;
83+ show_scope_config ( "--local" ) ;
84+ Ok ( ( ) )
85+ }
86+
87+ fn show_scope_config ( scope : & str ) {
88+ let configs = [
89+ "push.autoSetupRemote" ,
90+ "help.autocorrect" ,
91+ "diff.algorithm" ,
92+ "merge.conflictstyle" ,
93+ "rerere.enabled" ,
94+ "core.pager" ,
95+ ] ;
96+
97+ let mut any = false ;
98+ for key in & configs {
99+ if let Some ( value) = git_config_get ( key, scope) {
100+ println ! ( " {key} = {value}" ) ;
101+ any = true ;
102+ }
103+ }
104+
105+ if !any {
106+ println ! ( " (none)" ) ;
107+ }
108+ }
109+
110+ fn git_config_get ( key : & str , scope : & str ) -> Option < String > {
111+ let output = Command :: new ( "git" )
112+ . args ( [ "config" , scope, "--get" , key] )
113+ . output ( )
114+ . ok ( ) ?;
115+
116+ if output. status . success ( ) {
117+ Some ( String :: from_utf8_lossy ( & output. stdout ) . trim ( ) . to_string ( ) )
118+ } else {
119+ None
39120 }
40121}
41122
@@ -87,10 +168,12 @@ pub(crate) const CONFIG_OPTIONS: &[ConfigOption] = &[
87168] ;
88169
89170/// Apply selected config option keys. Used by the interactive wizard.
90- pub ( crate ) fn apply_config_keys ( keys : & [ & str ] , cargo_available : bool ) -> Result < ( ) > {
171+ pub ( crate ) fn apply_config_keys (
172+ keys : & [ & str ] ,
173+ cargo_available : bool ,
174+ scope : ConfigScope ,
175+ ) -> Result < ( ) > {
91176 for key in keys {
92- // Find the matching option to reuse its value from CONFIG_OPTIONS context,
93- // then dispatch to the appropriate setter.
94177 match * key {
95178 "core.pager" => {
96179 anyhow:: ensure!(
@@ -101,17 +184,16 @@ pub(crate) fn apply_config_keys(keys: &[&str], cargo_available: bool) -> Result<
101184 install_delta ( ) ?;
102185 }
103186 for ( k, v) in DELTA_CONFIGS {
104- git_config_set ( k, v) ?;
187+ git_config_set ( k, v, scope ) ?;
105188 }
106189 }
107190 _ => {
108- // All non-delta options map directly from CONFIG_OPTIONS value
109191 let value = CONFIG_OPTIONS
110192 . iter ( )
111193 . find ( |o| o. key == * key)
112194 . and_then ( |o| o. value )
113195 . ok_or_else ( || anyhow:: anyhow!( "Unknown config key: {key}" ) ) ?;
114- git_config_set ( key, value) ?;
196+ git_config_set ( key, value, scope ) ?;
115197 }
116198 }
117199 }
@@ -138,18 +220,18 @@ const DELTA_CONFIGS: GitConfigs = &[
138220 ( "delta.side-by-side" , "true" ) ,
139221] ;
140222
141- fn apply_defaults ( dry_run : bool ) -> Result < ( ) > {
142- apply_configs ( DEFAULTS , dry_run)
223+ fn apply_defaults ( dry_run : bool , scope : ConfigScope ) -> Result < ( ) > {
224+ apply_configs ( DEFAULTS , dry_run, scope )
143225}
144226
145- fn apply_advanced ( dry_run : bool ) -> Result < ( ) > {
227+ fn apply_advanced ( dry_run : bool , scope : ConfigScope ) -> Result < ( ) > {
146228 println ! (
147229 "Warning: merge.conflictstyle=zdiff3 may cause issues with GitHub Desktop and GUI merge tools."
148230 ) ;
149- apply_configs ( ADVANCED , dry_run)
231+ apply_configs ( ADVANCED , dry_run, scope )
150232}
151233
152- fn apply_delta ( yes : bool , dry_run : bool ) -> Result < ( ) > {
234+ fn apply_delta ( yes : bool , dry_run : bool , scope : ConfigScope ) -> Result < ( ) > {
153235 if !delta_installed ( ) {
154236 if !confirm (
155237 "git-delta is not installed. Install via `cargo install git-delta`?" ,
@@ -166,29 +248,44 @@ fn apply_delta(yes: bool, dry_run: bool) -> Result<()> {
166248 }
167249 println ! (
168250 "Note: delta.side-by-side=true may look wrong in narrow terminals. \
169- Disable with: git config --global delta.side-by-side false"
251+ Disable with: git config {} delta.side-by-side false",
252+ scope_flag( scope)
170253 ) ;
171- apply_configs ( DELTA_CONFIGS , dry_run)
254+ apply_configs ( DELTA_CONFIGS , dry_run, scope )
172255}
173256
174- fn apply_configs ( configs : GitConfigs , dry_run : bool ) -> Result < ( ) > {
257+ fn apply_configs ( configs : GitConfigs , dry_run : bool , scope : ConfigScope ) -> Result < ( ) > {
258+ let flag = scope_flag ( scope) ;
259+ let mut already_set = 0 ;
260+
175261 for ( key, value) in configs {
176- if dry_run {
177- println ! ( "[dry-run] git config --global {key} {value}" ) ;
262+ let current = git_config_get ( key, flag) ;
263+
264+ if current. as_deref ( ) == Some ( value) {
265+ println ! ( "✓ {key} = {value} (already set)" ) ;
266+ already_set += 1 ;
267+ } else if dry_run {
268+ println ! ( "[dry-run] git config {flag} {key} {value}" ) ;
178269 } else {
179- git_config_set ( key, value) ?;
180- println ! ( "Set {key} = {value}" ) ;
270+ git_config_set ( key, value, scope ) ?;
271+ println ! ( "✓ Set {key} = {value}" ) ;
181272 }
182273 }
274+
275+ if already_set == configs. len ( ) {
276+ println ! ( "\n All configs already applied." ) ;
277+ }
278+
183279 Ok ( ( ) )
184280}
185281
186- fn git_config_set ( key : & str , value : & str ) -> Result < ( ) > {
282+ fn git_config_set ( key : & str , value : & str , scope : ConfigScope ) -> Result < ( ) > {
283+ let flag = scope_flag ( scope) ;
187284 let status = Command :: new ( "git" )
188- . args ( [ "config" , "--global" , key, value] )
285+ . args ( [ "config" , flag , key, value] )
189286 . status ( )
190287 . with_context ( || format ! ( "Failed to run git config for '{key}'" ) ) ?;
191- anyhow:: ensure!( status. success( ) , "git config --global {key} {value} failed" ) ;
288+ anyhow:: ensure!( status. success( ) , "git config {flag} {key} {value} failed" ) ;
192289 Ok ( ( ) )
193290}
194291
@@ -224,18 +321,29 @@ mod tests {
224321
225322 #[ test]
226323 fn apply_configs_dry_run_prints_without_running_git ( ) {
227- // dry_run=true must not invoke git; if it did it would fail in CI without a repo
228- let result = apply_configs ( DEFAULTS , true ) ;
324+ let result = apply_configs ( DEFAULTS , true , ConfigScope :: Global ) ;
229325 assert ! ( result. is_ok( ) ) ;
230326 }
231327
232328 #[ test]
233329 fn apply_configs_dry_run_covers_advanced_preset ( ) {
234- assert ! ( apply_configs( ADVANCED , true ) . is_ok( ) ) ;
330+ assert ! ( apply_configs( ADVANCED , true , ConfigScope :: Global ) . is_ok( ) ) ;
235331 }
236332
237333 #[ test]
238334 fn apply_configs_dry_run_covers_delta_preset ( ) {
239- assert ! ( apply_configs( DELTA_CONFIGS , true ) . is_ok( ) ) ;
335+ assert ! ( apply_configs( DELTA_CONFIGS , true , ConfigScope :: Global ) . is_ok( ) ) ;
336+ }
337+
338+ #[ test]
339+ fn determine_scope_defaults_to_global_outside_repo ( ) {
340+ let scope = determine_scope ( false , false ) ;
341+ assert ! ( matches!( scope, ConfigScope :: Global | ConfigScope :: Local ) ) ;
342+ }
343+
344+ #[ test]
345+ fn determine_scope_respects_explicit_flags ( ) {
346+ assert ! ( matches!( determine_scope( true , false ) , ConfigScope :: Global ) ) ;
347+ assert ! ( matches!( determine_scope( false , true ) , ConfigScope :: Local ) ) ;
240348 }
241349}
0 commit comments