11use anyhow:: Result ;
22use inquire:: { MultiSelect , Text } ;
3+ use std:: { collections:: HashSet , fs} ;
34
4- use crate :: { attributes, config, git, hooks, ignore} ;
5+ use crate :: { attributes, config, git, hooks, ignore, utils :: find_repo_root } ;
56
67const BANNER : & str = r#"
78 ███ █████ █████ ███ █████
@@ -35,14 +36,36 @@ pub fn run() -> Result<()> {
3536
3637 // ── Hooks ────────────────────────────────────────────────────────────────
3738 let builtins = hooks:: available_builtins ( ) ;
39+ let installed_hooks = get_installed_hooks ( ) ;
40+
3841 let mut hook_items: Vec < String > = builtins
3942 . iter ( )
40- . map ( |b| format ! ( "{:<25} ({}) — {}" , b. name, b. hook, b. description) )
43+ . map ( |b| {
44+ let base = format ! ( "{:<25} ({}) — {}" , b. name, b. hook, b. description) ;
45+ if installed_hooks. contains ( b. name ) {
46+ format ! ( "{} [✓ installed]" , base)
47+ } else {
48+ base
49+ }
50+ } )
4151 . collect ( ) ;
4252 hook_items. push ( "Add custom hook..." . to_string ( ) ) ;
4353
54+ let preselected: Vec < usize > = builtins
55+ . iter ( )
56+ . enumerate ( )
57+ . filter ( |( _, b) | installed_hooks. contains ( b. name ) )
58+ . map ( |( i, _) | i)
59+ . collect ( ) ;
60+
61+ let default_selection = if preselected. is_empty ( ) {
62+ vec ! [ 0usize ]
63+ } else {
64+ preselected
65+ } ;
66+
4467 let hook_selections = MultiSelect :: new ( "Hooks" , hook_items. clone ( ) )
45- . with_default ( & [ 0usize ] ) // conventional-commits preselected
68+ . with_default ( & default_selection )
4669 . with_help_message ( "↑↓ move space select enter confirm esc skip" )
4770 . prompt_skippable ( ) ?
4871 . unwrap_or_default ( ) ;
@@ -65,6 +88,12 @@ pub fn run() -> Result<()> {
6588 }
6689 }
6790
91+ let hooks_to_remove: Vec < & str > = installed_hooks
92+ . iter ( )
93+ . filter ( |h| !selected_builtins. contains ( & h. as_str ( ) ) )
94+ . map ( |s| s. as_str ( ) )
95+ . collect ( ) ;
96+
6897 // ── .gitignore ───────────────────────────────────────────────────────────
6998 println ! ( ) ;
7099 let all_templates = load_ignore_templates ( ) ;
@@ -97,33 +126,51 @@ pub fn run() -> Result<()> {
97126
98127 // ── Git config ───────────────────────────────────────────────────────────
99128 println ! ( ) ;
129+ let configured_keys = get_configured_keys ( ) ;
130+
100131 let config_options: Vec < & config:: ConfigOption > = config:: CONFIG_OPTIONS
101132 . iter ( )
102133 . filter ( |o| o. key != "core.pager" || cargo_available)
103134 . collect ( ) ;
104135
105- let config_labels: Vec < & str > = config_options. iter ( ) . map ( |o| o. label ) . collect ( ) ;
136+ let config_labels: Vec < String > = config_options
137+ . iter ( )
138+ . map ( |o| {
139+ if configured_keys. contains ( o. key ) {
140+ format ! ( "{} [✓ already set]" , o. label)
141+ } else {
142+ o. label . to_string ( )
143+ }
144+ } )
145+ . collect ( ) ;
146+
147+ let config_labels_refs: Vec < & str > = config_labels. iter ( ) . map ( |s| s. as_str ( ) ) . collect ( ) ;
106148
107- // pre-select recommended ones
108149 let defaults: Vec < usize > = config_options
109150 . iter ( )
110151 . enumerate ( )
111152 . filter ( |( _, o) | o. recommended )
112153 . map ( |( i, _) | i)
113154 . collect ( ) ;
114155
115- let config_selections = MultiSelect :: new ( "Git config" , config_labels . clone ( ) )
156+ let config_selections = MultiSelect :: new ( "Git config" , config_labels_refs . clone ( ) )
116157 . with_default ( & defaults)
117158 . with_help_message ( "↑↓ move space select enter confirm esc skip" )
118159 . prompt_skippable ( ) ?
119160 . unwrap_or_default ( ) ;
120161
121162 let selected_config_keys: Vec < & str > = resolve_keys (
122163 & config_selections,
123- & config_labels ,
164+ & config_labels_refs ,
124165 & config_options. iter ( ) . map ( |o| o. key ) . collect :: < Vec < _ > > ( ) ,
125166 ) ;
126167
168+ let configs_to_remove: Vec < & str > = config_options
169+ . iter ( )
170+ . filter ( |o| configured_keys. contains ( o. key ) && !selected_config_keys. contains ( & o. key ) )
171+ . map ( |o| o. key )
172+ . collect ( ) ;
173+
127174 // ── Summary & confirm ────────────────────────────────────────────────────
128175 let nothing = selected_builtins. is_empty ( )
129176 && custom_hooks. is_empty ( )
@@ -175,6 +222,13 @@ pub fn run() -> Result<()> {
175222 hooks:: install_custom ( hook, cmd, false ) ?;
176223 println ! ( " ◇ hook '{hook}' installed ✓" ) ;
177224 }
225+ for hook in & hooks_to_remove {
226+ if let Some ( builtin) = hooks:: available_builtins ( ) . iter ( ) . find ( |b| b. name == * hook) {
227+ if hooks:: remove_hook ( builtin. hook , true ) . is_ok ( ) {
228+ println ! ( " ◇ hook '{hook}' removed ✓" ) ;
229+ }
230+ }
231+ }
178232 if !selected_templates. is_empty ( ) {
179233 let joined = selected_templates. join ( "," ) ;
180234 ignore:: add_templates ( & joined, false ) ?;
@@ -192,6 +246,12 @@ pub fn run() -> Result<()> {
192246 ) ?;
193247 println ! ( " ◇ git config applied ✓" ) ;
194248 }
249+ for key in & configs_to_remove {
250+ if config:: remove_config_key ( key, config:: ConfigScope :: Local ) . is_err ( ) {
251+ let _ = config:: remove_config_key ( key, config:: ConfigScope :: Global ) ;
252+ }
253+ println ! ( " ◇ git config '{key}' removed ✓" ) ;
254+ }
195255
196256 println ! ( "\n Done\n " ) ;
197257 Ok ( ( ) )
@@ -201,6 +261,64 @@ fn load_ignore_templates() -> Vec<String> {
201261 ignore:: fetch_template_list ( ) . unwrap_or_default ( )
202262}
203263
264+ fn get_installed_hooks ( ) -> HashSet < String > {
265+ let mut installed = HashSet :: new ( ) ;
266+ if let Ok ( root) = find_repo_root ( ) {
267+ let hooks_dir = root. join ( ".git" ) . join ( "hooks" ) ;
268+ if hooks_dir. exists ( ) {
269+ if let Ok ( entries) = fs:: read_dir ( & hooks_dir) {
270+ for entry in entries. filter_map ( |e| e. ok ( ) ) {
271+ let name = entry. file_name ( ) . to_string_lossy ( ) . to_string ( ) ;
272+ if !name. ends_with ( ".bak" ) && !name. ends_with ( ".sample" ) {
273+ let content = fs:: read_to_string ( entry. path ( ) ) . unwrap_or_default ( ) ;
274+ let builtin_match = hooks:: available_builtins ( ) . iter ( ) . find ( |b| {
275+ b. hook == name && content. contains ( & b. script [ ..80 . min ( b. script . len ( ) ) ] )
276+ } ) ;
277+ if let Some ( b) = builtin_match {
278+ installed. insert ( b. name . to_string ( ) ) ;
279+ }
280+ }
281+ }
282+ }
283+ }
284+ }
285+ installed
286+ }
287+
288+ fn get_configured_keys ( ) -> HashSet < String > {
289+ let mut configured = HashSet :: new ( ) ;
290+ for option in config:: CONFIG_OPTIONS {
291+ if option. key == "core.pager" {
292+ continue ;
293+ }
294+ if let Some ( value) = option. value {
295+ if let Ok ( output) = std:: process:: Command :: new ( "git" )
296+ . args ( [ "config" , "--local" , "--get" , option. key ] )
297+ . output ( )
298+ {
299+ if output. status . success ( ) {
300+ let current = String :: from_utf8_lossy ( & output. stdout ) . trim ( ) . to_string ( ) ;
301+ if current == value {
302+ configured. insert ( option. key . to_string ( ) ) ;
303+ }
304+ }
305+ }
306+ if let Ok ( output) = std:: process:: Command :: new ( "git" )
307+ . args ( [ "config" , "--global" , "--get" , option. key ] )
308+ . output ( )
309+ {
310+ if output. status . success ( ) {
311+ let current = String :: from_utf8_lossy ( & output. stdout ) . trim ( ) . to_string ( ) ;
312+ if current == value {
313+ configured. insert ( option. key . to_string ( ) ) ;
314+ }
315+ }
316+ }
317+ }
318+ }
319+ configured
320+ }
321+
204322/// Maps selected display labels back to their corresponding keys.
205323fn resolve_keys < ' a > (
206324 selections : & [ impl AsRef < str > ] ,
0 commit comments