cascade_cli/cli/commands/
hooks.rs

1use crate::cli::output::Output;
2use crate::config::Settings;
3use crate::errors::{CascadeError, Result};
4use crate::git::find_repository_root;
5use dialoguer::{theme::ColorfulTheme, Confirm};
6use std::env;
7use std::fs;
8use std::path::{Path, PathBuf};
9use std::process::Command;
10
11/// Git repository type detection
12#[derive(Debug, Clone, PartialEq)]
13pub enum RepositoryType {
14    Bitbucket,
15    GitHub,
16    GitLab,
17    AzureDevOps,
18    Unknown,
19}
20
21/// Branch type classification
22#[derive(Debug, Clone, PartialEq)]
23pub enum BranchType {
24    Main,    // main, master, develop
25    Feature, // feature branches
26    Unknown,
27}
28
29/// Installation options for smart hook activation
30#[derive(Debug, Clone)]
31pub struct InstallOptions {
32    pub check_prerequisites: bool,
33    pub feature_branches_only: bool,
34    pub confirm: bool,
35    pub force: bool,
36}
37
38impl Default for InstallOptions {
39    fn default() -> Self {
40        Self {
41            check_prerequisites: true,
42            feature_branches_only: true,
43            confirm: true,
44            force: false,
45        }
46    }
47}
48
49/// Git hooks integration for Cascade CLI
50pub struct HooksManager {
51    repo_path: PathBuf,
52    repo_id: String,
53}
54
55/// Available Git hooks that Cascade can install
56#[derive(Debug, Clone)]
57pub enum HookType {
58    /// Validates commits are added to stacks
59    PostCommit,
60    /// Prevents force pushes and validates stack state
61    PrePush,
62    /// Validates commit messages follow conventions
63    CommitMsg,
64    /// Smart edit mode guidance before commit
65    PreCommit,
66    /// Prepares commit message with stack context
67    PrepareCommitMsg,
68}
69
70impl HookType {
71    fn filename(&self) -> String {
72        let base_name = match self {
73            HookType::PostCommit => "post-commit",
74            HookType::PrePush => "pre-push",
75            HookType::CommitMsg => "commit-msg",
76            HookType::PreCommit => "pre-commit",
77            HookType::PrepareCommitMsg => "prepare-commit-msg",
78        };
79        format!(
80            "{}{}",
81            base_name,
82            crate::utils::platform::git_hook_extension()
83        )
84    }
85
86    fn description(&self) -> &'static str {
87        match self {
88            HookType::PostCommit => "Auto-add new commits to active stack",
89            HookType::PrePush => "Prevent force pushes and validate stack state",
90            HookType::CommitMsg => "Validate commit message format",
91            HookType::PreCommit => "Smart edit mode guidance for better UX",
92            HookType::PrepareCommitMsg => "Add stack context to commit messages",
93        }
94    }
95}
96
97impl HooksManager {
98    pub fn new(repo_path: &Path) -> Result<Self> {
99        // Verify this is a git repository
100        let git_dir = repo_path.join(".git");
101        if !git_dir.exists() {
102            return Err(CascadeError::config(
103                "Not a Git repository. Git hooks require a valid Git repository.".to_string(),
104            ));
105        }
106
107        // Generate a unique repo ID based on remote URL
108        let repo_id = Self::generate_repo_id(repo_path)?;
109
110        Ok(Self {
111            repo_path: repo_path.to_path_buf(),
112            repo_id,
113        })
114    }
115
116    /// Generate a unique repository identifier based on remote URL
117    fn generate_repo_id(repo_path: &Path) -> Result<String> {
118        use std::process::Command;
119
120        let output = Command::new("git")
121            .args(["remote", "get-url", "origin"])
122            .current_dir(repo_path)
123            .output()
124            .map_err(|e| CascadeError::config(format!("Failed to get remote URL: {e}")))?;
125
126        if !output.status.success() {
127            // Fallback to absolute path hash if no remote
128            use sha2::{Digest, Sha256};
129            let canonical_path = repo_path
130                .canonicalize()
131                .unwrap_or_else(|_| repo_path.to_path_buf());
132            let path_str = canonical_path.to_string_lossy();
133            let mut hasher = Sha256::new();
134            hasher.update(path_str.as_bytes());
135            let result = hasher.finalize();
136            let hash = format!("{result:x}");
137            return Ok(format!("local-{}", &hash[..8]));
138        }
139
140        let remote_url = String::from_utf8_lossy(&output.stdout).trim().to_string();
141
142        // Convert URL to safe directory name
143        // e.g., https://github.com/user/repo.git -> github.com-user-repo
144        let safe_name = remote_url
145            .replace("https://", "")
146            .replace("http://", "")
147            .replace("git@", "")
148            .replace("ssh://", "")
149            .replace(".git", "")
150            .replace([':', '/', '\\'], "-")
151            .chars()
152            .filter(|c| c.is_alphanumeric() || *c == '-' || *c == '.' || *c == '_')
153            .collect::<String>();
154
155        Ok(safe_name)
156    }
157
158    /// Get the Cascade-specific hooks directory for this repo
159    fn get_cascade_hooks_dir(&self) -> Result<PathBuf> {
160        let home = dirs::home_dir()
161            .ok_or_else(|| CascadeError::config("Could not find home directory".to_string()))?;
162        let cascade_hooks = home.join(".cascade").join("hooks").join(&self.repo_id);
163        Ok(cascade_hooks)
164    }
165
166    /// Get the Cascade config directory for this repo
167    fn get_cascade_config_dir(&self) -> Result<PathBuf> {
168        let home = dirs::home_dir()
169            .ok_or_else(|| CascadeError::config("Could not find home directory".to_string()))?;
170        let cascade_config = home.join(".cascade").join("config").join(&self.repo_id);
171        Ok(cascade_config)
172    }
173
174    /// Save the current core.hooksPath value for later restoration
175    fn save_original_hooks_path(&self) -> Result<()> {
176        use std::process::Command;
177
178        let output = Command::new("git")
179            .args(["config", "--get", "core.hooksPath"])
180            .current_dir(&self.repo_path)
181            .output()
182            .map_err(|e| CascadeError::config(format!("Failed to check git config: {e}")))?;
183
184        let config_dir = self.get_cascade_config_dir()?;
185        fs::create_dir_all(&config_dir)
186            .map_err(|e| CascadeError::config(format!("Failed to create config directory: {e}")))?;
187
188        let original_path = if output.status.success() {
189            String::from_utf8_lossy(&output.stdout).trim().to_string()
190        } else {
191            // Empty string means it wasn't set
192            String::new()
193        };
194
195        fs::write(config_dir.join("original-hooks-path"), original_path).map_err(|e| {
196            CascadeError::config(format!("Failed to save original hooks path: {e}"))
197        })?;
198
199        Ok(())
200    }
201
202    /// Restore the original core.hooksPath value
203    fn restore_original_hooks_path(&self) -> Result<()> {
204        use std::process::Command;
205
206        let config_dir = self.get_cascade_config_dir()?;
207        let original_path_file = config_dir.join("original-hooks-path");
208
209        if !original_path_file.exists() {
210            // Nothing to restore
211            return Ok(());
212        }
213
214        let original_path = fs::read_to_string(&original_path_file).map_err(|e| {
215            CascadeError::config(format!("Failed to read original hooks path: {e}"))
216        })?;
217
218        if original_path.is_empty() {
219            // It wasn't set originally, so unset it
220            Command::new("git")
221                .args(["config", "--unset", "core.hooksPath"])
222                .current_dir(&self.repo_path)
223                .output()
224                .map_err(|e| {
225                    CascadeError::config(format!("Failed to unset core.hooksPath: {e}"))
226                })?;
227        } else {
228            // Restore the original value
229            Command::new("git")
230                .args(["config", "core.hooksPath", &original_path])
231                .current_dir(&self.repo_path)
232                .output()
233                .map_err(|e| {
234                    CascadeError::config(format!("Failed to restore core.hooksPath: {e}"))
235                })?;
236        }
237
238        // Clean up the saved file
239        fs::remove_file(original_path_file).ok();
240
241        Ok(())
242    }
243
244    /// Get the actual hooks directory path, respecting core.hooksPath configuration
245    #[allow(dead_code)]
246    fn get_hooks_path(repo_path: &Path) -> Result<PathBuf> {
247        use std::process::Command;
248
249        // Try to get core.hooksPath configuration
250        let output = Command::new("git")
251            .args(["config", "--get", "core.hooksPath"])
252            .current_dir(repo_path)
253            .output()
254            .map_err(|e| CascadeError::config(format!("Failed to check git config: {e}")))?;
255
256        let hooks_path = if output.status.success() {
257            let configured_path = String::from_utf8_lossy(&output.stdout).trim().to_string();
258            if configured_path.is_empty() {
259                // Empty value means default
260                repo_path.join(".git").join("hooks")
261            } else if configured_path.starts_with('/') {
262                // Absolute path
263                PathBuf::from(configured_path)
264            } else {
265                // Relative path - relative to repo root
266                repo_path.join(configured_path)
267            }
268        } else {
269            // No core.hooksPath configured, use default
270            repo_path.join(".git").join("hooks")
271        };
272
273        Ok(hooks_path)
274    }
275
276    /// Install all recommended Cascade hooks
277    pub fn install_all(&self) -> Result<()> {
278        self.install_with_options(&InstallOptions::default())
279    }
280
281    /// Install only essential hooks (for setup) - excludes post-commit
282    pub fn install_essential(&self) -> Result<()> {
283        Output::progress("Installing essential Cascade Git hooks");
284
285        let essential_hooks = vec![
286            HookType::PrePush,
287            HookType::CommitMsg,
288            HookType::PrepareCommitMsg,
289            HookType::PreCommit,
290        ];
291
292        for hook in essential_hooks {
293            self.install_hook(&hook)?;
294        }
295
296        Output::success("Essential Cascade hooks installed successfully!");
297        Output::tip(
298            "Note: Post-commit auto-add hook available with 'ca hooks install --all'",
299        );
300        Output::section("Hooks installed");
301        self.list_installed_hooks()?;
302
303        Ok(())
304    }
305
306    /// Install hooks with smart validation options
307    pub fn install_with_options(&self, options: &InstallOptions) -> Result<()> {
308        if options.check_prerequisites && !options.force {
309            self.validate_prerequisites()?;
310        }
311
312        if options.feature_branches_only && !options.force {
313            self.validate_branch_suitability()?;
314        }
315
316        if options.confirm && !options.force {
317            self.confirm_installation()?;
318        }
319
320        Output::progress("Installing all Cascade Git hooks");
321
322        // Install ALL hooks (all 5 HookType variants)
323        let hooks = vec![
324            HookType::PostCommit,
325            HookType::PrePush,
326            HookType::CommitMsg,
327            HookType::PrepareCommitMsg,
328            HookType::PreCommit,
329        ];
330
331        for hook in hooks {
332            self.install_hook(&hook)?;
333        }
334
335        Output::success("All Cascade hooks installed successfully!");
336        Output::section("Hooks installed");
337        self.list_installed_hooks()?;
338
339        Ok(())
340    }
341
342    /// Install a specific hook
343    pub fn install_hook(&self, hook_type: &HookType) -> Result<()> {
344        // Ensure we've saved the original hooks path first
345        self.save_original_hooks_path()?;
346
347        // Create cascade hooks directory
348        let cascade_hooks_dir = self.get_cascade_hooks_dir()?;
349        fs::create_dir_all(&cascade_hooks_dir).map_err(|e| {
350            CascadeError::config(format!("Failed to create cascade hooks directory: {e}"))
351        })?;
352
353        // Generate hook that chains to original
354        let hook_content = self.generate_chaining_hook_script(hook_type)?;
355        let hook_path = cascade_hooks_dir.join(hook_type.filename());
356
357        // Write the hook
358        fs::write(&hook_path, hook_content)
359            .map_err(|e| CascadeError::config(format!("Failed to write hook file: {e}")))?;
360
361        // Make executable (platform-specific)
362        crate::utils::platform::make_executable(&hook_path)
363            .map_err(|e| CascadeError::config(format!("Failed to make hook executable: {e}")))?;
364
365        // Set core.hooksPath to our cascade directory
366        self.set_cascade_hooks_path()?;
367
368        Output::success(format!("Installed {} hook", hook_type.filename()));
369        Ok(())
370    }
371
372    /// Set git's core.hooksPath to our cascade hooks directory
373    fn set_cascade_hooks_path(&self) -> Result<()> {
374        use std::process::Command;
375
376        let cascade_hooks_dir = self.get_cascade_hooks_dir()?;
377        let hooks_path_str = cascade_hooks_dir.to_string_lossy();
378
379        let output = Command::new("git")
380            .args(["config", "core.hooksPath", &hooks_path_str])
381            .current_dir(&self.repo_path)
382            .output()
383            .map_err(|e| CascadeError::config(format!("Failed to set core.hooksPath: {e}")))?;
384
385        if !output.status.success() {
386            return Err(CascadeError::config(format!(
387                "Failed to set core.hooksPath: {}",
388                String::from_utf8_lossy(&output.stderr)
389            )));
390        }
391
392        Ok(())
393    }
394
395    /// Remove all Cascade hooks
396    pub fn uninstall_all(&self) -> Result<()> {
397        Output::progress("Removing Cascade Git hooks");
398
399        // Restore original core.hooksPath
400        self.restore_original_hooks_path()?;
401
402        // Clean up cascade hooks directory
403        let cascade_hooks_dir = self.get_cascade_hooks_dir()?;
404        if cascade_hooks_dir.exists() {
405            fs::remove_dir_all(&cascade_hooks_dir).map_err(|e| {
406                CascadeError::config(format!("Failed to remove cascade hooks directory: {e}"))
407            })?;
408        }
409
410        // Clean up config directory if empty
411        let cascade_config_dir = self.get_cascade_config_dir()?;
412        if cascade_config_dir.exists() {
413            // Try to remove, but ignore if not empty
414            fs::remove_dir(&cascade_config_dir).ok();
415        }
416
417        Output::success("All Cascade hooks removed!");
418        Ok(())
419    }
420
421    /// Remove a specific hook
422    pub fn uninstall_hook(&self, hook_type: &HookType) -> Result<()> {
423        let cascade_hooks_dir = self.get_cascade_hooks_dir()?;
424        let hook_path = cascade_hooks_dir.join(hook_type.filename());
425
426        if hook_path.exists() {
427            fs::remove_file(&hook_path)
428                .map_err(|e| CascadeError::config(format!("Failed to remove hook file: {e}")))?;
429            Output::success(format!("Removed {} hook", hook_type.filename()));
430
431            // If no more hooks in cascade directory, restore original hooks path
432            let remaining_hooks = fs::read_dir(&cascade_hooks_dir)
433                .map_err(|e| CascadeError::config(format!("Failed to read hooks directory: {e}")))?
434                .filter_map(|entry| entry.ok())
435                .filter(|entry| {
436                    entry.path().is_file() && !entry.file_name().to_string_lossy().starts_with('.')
437                })
438                .count();
439
440            if remaining_hooks == 0 {
441                Output::info(
442                    "No more Cascade hooks installed, restoring original hooks configuration",
443                );
444                self.restore_original_hooks_path()?;
445                fs::remove_dir(&cascade_hooks_dir).ok();
446            }
447        } else {
448            Output::info(format!("{} hook not found", hook_type.filename()));
449        }
450
451        Ok(())
452    }
453
454    /// List all installed hooks and their status
455    pub fn list_installed_hooks(&self) -> Result<()> {
456        let hooks = vec![
457            HookType::PostCommit,
458            HookType::PrePush,
459            HookType::CommitMsg,
460            HookType::PrepareCommitMsg,
461            HookType::PreCommit,
462        ];
463
464        Output::section("Git Hooks Status");
465
466        // Check if we're using cascade hooks directory
467        let cascade_hooks_dir = self.get_cascade_hooks_dir()?;
468        let using_cascade_hooks = cascade_hooks_dir.exists()
469            && self.get_current_hooks_path()?
470                == Some(cascade_hooks_dir.to_string_lossy().to_string());
471
472        if using_cascade_hooks {
473            Output::success("✓ Cascade hooks are installed and active");
474            Output::info(format!(
475                "  Hooks directory: {}",
476                cascade_hooks_dir.display()
477            ));
478
479            // Check what original hooks path was saved
480            let config_dir = self.get_cascade_config_dir()?;
481            let original_path_file = config_dir.join("original-hooks-path");
482            if original_path_file.exists() {
483                let original_path = fs::read_to_string(original_path_file).unwrap_or_default();
484                if !original_path.is_empty() {
485                    Output::info(format!("  Chaining to original hooks: {original_path}"));
486                } else {
487                    Output::info("  Chaining to original hooks: .git/hooks");
488                }
489            }
490            println!();
491        } else {
492            Output::warning("⚠️  Cascade hooks are NOT installed in this repository");
493            Output::tip("To install Cascade hooks:");
494            Output::bullet("Run: ca hooks install            (recommended: 4 essential hooks)");
495            Output::bullet("Run: ca hooks install --all      (all 5 hooks + post-commit auto-add)");
496            Output::bullet("Both options preserve existing hooks by chaining to them");
497            println!();
498        }
499
500        for hook in hooks {
501            let cascade_hook_path = cascade_hooks_dir.join(hook.filename());
502
503            if using_cascade_hooks && cascade_hook_path.exists() {
504                Output::success(format!("{}: {} ✓", hook.filename(), hook.description()));
505            } else {
506                // Check default location
507                let default_hook_path = self
508                    .repo_path
509                    .join(".git")
510                    .join("hooks")
511                    .join(hook.filename());
512                if default_hook_path.exists() {
513                    Output::warning(format!(
514                        "{}: {} (In .git/hooks, not managed by Cascade)",
515                        hook.filename(),
516                        hook.description()
517                    ));
518                } else {
519                    Output::error(format!(
520                        "{}: {} (Not installed)",
521                        hook.filename(),
522                        hook.description()
523                    ));
524                }
525            }
526        }
527
528        Ok(())
529    }
530
531    /// Get the current core.hooksPath value
532    fn get_current_hooks_path(&self) -> Result<Option<String>> {
533        use std::process::Command;
534
535        let output = Command::new("git")
536            .args(["config", "--get", "core.hooksPath"])
537            .current_dir(&self.repo_path)
538            .output()
539            .map_err(|e| CascadeError::config(format!("Failed to check git config: {e}")))?;
540
541        if output.status.success() {
542            let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
543            if path.is_empty() {
544                Ok(None)
545            } else {
546                Ok(Some(path))
547            }
548        } else {
549            Ok(None)
550        }
551    }
552
553    /// Generate hook script content
554    pub fn generate_hook_script(&self, hook_type: &HookType) -> Result<String> {
555        let cascade_cli = env::current_exe()
556            .map_err(|e| {
557                CascadeError::config(format!("Failed to get current executable path: {e}"))
558            })?
559            .to_string_lossy()
560            .to_string();
561
562        let script = match hook_type {
563            HookType::PostCommit => self.generate_post_commit_hook(&cascade_cli),
564            HookType::PrePush => self.generate_pre_push_hook(&cascade_cli),
565            HookType::CommitMsg => self.generate_commit_msg_hook(&cascade_cli),
566            HookType::PreCommit => self.generate_pre_commit_hook(&cascade_cli),
567            HookType::PrepareCommitMsg => self.generate_prepare_commit_msg_hook(&cascade_cli),
568        };
569
570        Ok(script)
571    }
572
573    /// Generate hook script that chains to original hooks
574    pub fn generate_chaining_hook_script(&self, hook_type: &HookType) -> Result<String> {
575        let cascade_cli = env::current_exe()
576            .map_err(|e| {
577                CascadeError::config(format!("Failed to get current executable path: {e}"))
578            })?
579            .to_string_lossy()
580            .to_string();
581
582        let config_dir = self.get_cascade_config_dir()?;
583        let hook_name = match hook_type {
584            HookType::PostCommit => "post-commit",
585            HookType::PrePush => "pre-push",
586            HookType::CommitMsg => "commit-msg",
587            HookType::PreCommit => "pre-commit",
588            HookType::PrepareCommitMsg => "prepare-commit-msg",
589        };
590
591        // Generate the cascade-specific hook logic
592        let cascade_logic = match hook_type {
593            HookType::PostCommit => self.generate_post_commit_hook(&cascade_cli),
594            HookType::PrePush => self.generate_pre_push_hook(&cascade_cli),
595            HookType::CommitMsg => self.generate_commit_msg_hook(&cascade_cli),
596            HookType::PreCommit => self.generate_pre_commit_hook(&cascade_cli),
597            HookType::PrepareCommitMsg => self.generate_prepare_commit_msg_hook(&cascade_cli),
598        };
599
600        // Create wrapper that chains to original
601        #[cfg(windows)]
602        return Ok(format!(
603                "@echo off\n\
604                 rem Cascade CLI Hook Wrapper - {}\n\
605                 rem This hook runs Cascade logic first, then chains to original hooks\n\n\
606                 rem Run Cascade logic first\n\
607                 call :cascade_logic %*\n\
608                 set CASCADE_RESULT=%ERRORLEVEL%\n\
609                 if %CASCADE_RESULT% neq 0 exit /b %CASCADE_RESULT%\n\n\
610                 rem Check for original hook\n\
611                 set ORIGINAL_HOOKS_PATH=\n\
612                 if exist \"{}\\original-hooks-path\" (\n\
613                     set /p ORIGINAL_HOOKS_PATH=<\"{}\\original-hooks-path\"\n\
614                 )\n\n\
615                 if \"%ORIGINAL_HOOKS_PATH%\"==\"\" (\n\
616                     rem Default location\n\
617                     for /f \"tokens=*\" %%i in ('git rev-parse --git-dir 2^>nul') do set GIT_DIR=%%i\n\
618                     if exist \"%GIT_DIR%\\hooks\\{}\" (\n\
619                         call \"%GIT_DIR%\\hooks\\{}\" %*\n\
620                         exit /b %ERRORLEVEL%\n\
621                     )\n\
622                 ) else (\n\
623                     rem Custom hooks path\n\
624                     if exist \"%ORIGINAL_HOOKS_PATH%\\{}\" (\n\
625                         call \"%ORIGINAL_HOOKS_PATH%\\{}\" %*\n\
626                         exit /b %ERRORLEVEL%\n\
627                     )\n\
628                 )\n\n\
629                 exit /b 0\n\n\
630                 :cascade_logic\n\
631                 {}\n\
632                 exit /b %ERRORLEVEL%\n",
633                hook_name,
634                config_dir.to_string_lossy(),
635                config_dir.to_string_lossy(),
636                hook_name,
637                hook_name,
638                hook_name,
639                hook_name,
640                cascade_logic
641            ));
642
643        #[cfg(not(windows))]
644        Ok(format!(
645                "#!/bin/sh\n\
646                 # Cascade CLI Hook Wrapper - {}\n\
647                 # This hook runs Cascade logic first, then chains to original hooks\n\n\
648                 set -e\n\n\
649                 # Function to run Cascade logic\n\
650                 cascade_logic() {{\n\
651                 {}\n\
652                 }}\n\n\
653                 # Run Cascade logic first\n\
654                 cascade_logic \"$@\"\n\
655                 CASCADE_RESULT=$?\n\
656                 if [ $CASCADE_RESULT -ne 0 ]; then\n\
657                     exit $CASCADE_RESULT\n\
658                 fi\n\n\
659                 # Check for original hook\n\
660                 ORIGINAL_HOOKS_PATH=\"\"\n\
661                 if [ -f \"{}/original-hooks-path\" ]; then\n\
662                     ORIGINAL_HOOKS_PATH=$(cat \"{}/original-hooks-path\" 2>/dev/null || echo \"\")\n\
663                 fi\n\n\
664                 if [ -z \"$ORIGINAL_HOOKS_PATH\" ]; then\n\
665                     # Default location\n\
666                     GIT_DIR=$(git rev-parse --git-dir 2>/dev/null || echo \".git\")\n\
667                     ORIGINAL_HOOK=\"$GIT_DIR/hooks/{}\"\n\
668                 else\n\
669                     # Custom hooks path\n\
670                     ORIGINAL_HOOK=\"$ORIGINAL_HOOKS_PATH/{}\"\n\
671                 fi\n\n\
672                 # Run original hook if it exists and is executable\n\
673                 if [ -x \"$ORIGINAL_HOOK\" ]; then\n\
674                     \"$ORIGINAL_HOOK\" \"$@\"\n\
675                     exit $?\n\
676                 fi\n\n\
677                 exit 0\n",
678                hook_name,
679                cascade_logic.trim_start_matches("#!/bin/sh\n").trim_start_matches("set -e\n"),
680                config_dir.to_string_lossy(),
681                config_dir.to_string_lossy(),
682                hook_name,
683                hook_name
684        ))
685    }
686
687    fn generate_post_commit_hook(&self, cascade_cli: &str) -> String {
688        #[cfg(windows)]
689        {
690            format!(
691                "@echo off\n\
692                 rem Cascade CLI Hook - Post Commit\n\
693                 rem Automatically adds new commits to the active stack\n\n\
694                 rem Get the commit hash and message\n\
695                 for /f \"tokens=*\" %%i in ('git rev-parse HEAD') do set COMMIT_HASH=%%i\n\
696                 for /f \"tokens=*\" %%i in ('git log --format=%%s -n 1 HEAD') do set COMMIT_MSG=%%i\n\n\
697                 rem Find repository root and check if Cascade is initialized\n\
698                 for /f \"tokens=*\" %%i in ('git rev-parse --show-toplevel 2^>nul') do set REPO_ROOT=%%i\n\
699                 if \"%REPO_ROOT%\"==\"\" set REPO_ROOT=.\n\
700                 if not exist \"%REPO_ROOT%\\.cascade\" (\n\
701                     echo ℹ️ Cascade not initialized, skipping stack management\n\
702                     echo 💡 Run 'ca init' to start using stacked diffs\n\
703                     exit /b 0\n\
704                 )\n\n\
705                 rem Check if there's an active stack\n\
706                 \"{cascade_cli}\" stack list --active >nul 2>&1\n\
707                 if %ERRORLEVEL% neq 0 (\n\
708                     echo ℹ️ No active stack found, commit will not be added to any stack\n\
709                     echo 💡 Use 'ca stack create ^<name^>' to create a stack for this commit\n\
710                     exit /b 0\n\
711                 )\n\n\
712                 rem Add commit to active stack\n\
713                 echo 🪝 Adding commit to active stack...\n\
714                 echo 📝 Commit: %COMMIT_MSG%\n\
715                 \"{cascade_cli}\" stack push --commit \"%COMMIT_HASH%\" --message \"%COMMIT_MSG%\"\n\
716                 if %ERRORLEVEL% equ 0 (\n\
717                     echo ✅ Commit added to stack successfully\n\
718                     echo 💡 Next: 'ca submit' to create PRs when ready\n\
719                 ) else (\n\
720                     echo ⚠️ Failed to add commit to stack\n\
721                     echo 💡 You can manually add it with: ca push --commit %COMMIT_HASH%\n\
722                 )\n"
723            )
724        }
725
726        #[cfg(not(windows))]
727        {
728            format!(
729                "#!/bin/sh\n\
730                 # Cascade CLI Hook - Post Commit\n\
731                 # Automatically adds new commits to the active stack\n\n\
732                 set -e\n\n\
733                 # Get the commit hash and message\n\
734                 COMMIT_HASH=$(git rev-parse HEAD)\n\
735                 COMMIT_MSG=$(git log --format=%s -n 1 HEAD)\n\n\
736                 # Find repository root and check if Cascade is initialized\n\
737                 REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || echo \".\")\n\
738                 if [ ! -d \"$REPO_ROOT/.cascade\" ]; then\n\
739                     echo \"ℹ️ Cascade not initialized, skipping stack management\"\n\
740                     echo \"💡 Run 'ca init' to start using stacked diffs\"\n\
741                     exit 0\n\
742                 fi\n\n\
743                 # Check if there's an active stack\n\
744                 if ! \"{cascade_cli}\" stack list --active > /dev/null 2>&1; then\n\
745                     echo \"ℹ️ No active stack found, commit will not be added to any stack\"\n\
746                     echo \"💡 Use 'ca stack create <name>' to create a stack for this commit\"\n\
747                     exit 0\n\
748                 fi\n\n\
749                 # Add commit to active stack (using specific commit targeting)\n\
750                 echo \"🪝 Adding commit to active stack...\"\n\
751                 echo \"📝 Commit: $COMMIT_MSG\"\n\
752                 if \"{cascade_cli}\" stack push --commit \"$COMMIT_HASH\" --message \"$COMMIT_MSG\"; then\n\
753                     echo \"✅ Commit added to stack successfully\"\n\
754                     echo \"💡 Next: 'ca submit' to create PRs when ready\"\n\
755                 else\n\
756                     echo \"⚠️ Failed to add commit to stack\"\n\
757                     echo \"💡 You can manually add it with: ca push --commit $COMMIT_HASH\"\n\
758                 fi\n"
759            )
760        }
761    }
762
763    fn generate_pre_push_hook(&self, cascade_cli: &str) -> String {
764        #[cfg(windows)]
765        {
766            format!(
767                "@echo off\n\
768                 rem Cascade CLI Hook - Pre Push\n\
769                 rem Prevents force pushes and validates stack state\n\n\
770                 rem Check for force push\n\
771                 echo %* | findstr /C:\"--force\" /C:\"--force-with-lease\" /C:\"-f\" >nul\n\
772                 if %ERRORLEVEL% equ 0 (\n\
773                     echo ❌ Force push detected!\n\
774                     echo 🌊 Cascade CLI uses stacked diffs - force pushes can break stack integrity\n\
775                     echo.\n\
776                     echo 💡 Instead of force pushing, try these streamlined commands:\n\
777                     echo    • ca sync      - Sync with remote changes ^(handles rebasing^)\n\
778                     echo    • ca push      - Push all unpushed commits ^(new default^)\n\
779                     echo    • ca submit    - Submit all entries for review ^(new default^)\n\
780                     echo    • ca autoland  - Auto-merge when approved + builds pass\n\
781                     echo.\n\
782                     echo 🚨 If you really need to force push, run:\n\
783                     echo    git push --force-with-lease [remote] [branch]\n\
784                     echo    ^(But consider if this will affect other stack entries^)\n\
785                     exit /b 1\n\
786                 )\n\n\
787                 rem Find repository root and check if Cascade is initialized\n\
788                 for /f \"tokens=*\" %%i in ('git rev-parse --show-toplevel 2^>nul') do set REPO_ROOT=%%i\n\
789                 if \"%REPO_ROOT%\"==\"\" set REPO_ROOT=.\n\
790                 if not exist \"%REPO_ROOT%\\.cascade\" (\n\
791                     echo ℹ️ Cascade not initialized, allowing push\n\
792                     exit /b 0\n\
793                 )\n\n\
794                 rem Validate stack state\n\
795                 echo 🪝 Validating stack state before push...\n\
796                 \"{cascade_cli}\" stack validate\n\
797                 if %ERRORLEVEL% equ 0 (\n\
798                     echo ✅ Stack validation passed\n\
799                 ) else (\n\
800                     echo ❌ Stack validation failed\n\
801                     echo 💡 Fix validation errors before pushing:\n\
802                     echo    • ca doctor       - Check overall health\n\
803                     echo    • ca status       - Check current stack status\n\
804                     echo    • ca sync         - Sync with remote and rebase if needed\n\
805                     exit /b 1\n\
806                 )\n\n\
807                 echo ✅ Pre-push validation complete\n"
808            )
809        }
810
811        #[cfg(not(windows))]
812        {
813            format!(
814                "#!/bin/sh\n\
815                 # Cascade CLI Hook - Pre Push\n\
816                 # Prevents force pushes and validates stack state\n\n\
817                 set -e\n\n\
818                 # Check for force push\n\
819                 if echo \"$*\" | grep -q -- \"--force\\|--force-with-lease\\|-f\"; then\n\
820                     echo \"❌ Force push detected!\"\n\
821                     echo \"🌊 Cascade CLI uses stacked diffs - force pushes can break stack integrity\"\n\
822                     echo \"\"\n\
823                     echo \"💡 Instead of force pushing, try these streamlined commands:\"\n\
824                     echo \"   • ca sync      - Sync with remote changes (handles rebasing)\"\n\
825                     echo \"   • ca push      - Push all unpushed commits (new default)\"\n\
826                     echo \"   • ca submit    - Submit all entries for review (new default)\"\n\
827                     echo \"   • ca autoland  - Auto-merge when approved + builds pass\"\n\
828                     echo \"\"\n\
829                     echo \"🚨 If you really need to force push, run:\"\n\
830                     echo \"   git push --force-with-lease [remote] [branch]\"\n\
831                     echo \"   (But consider if this will affect other stack entries)\"\n\
832                     exit 1\n\
833                 fi\n\n\
834                 # Find repository root and check if Cascade is initialized\n\
835                 REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || echo \".\")\n\
836                 if [ ! -d \"$REPO_ROOT/.cascade\" ]; then\n\
837                     echo \"ℹ️ Cascade not initialized, allowing push\"\n\
838                     exit 0\n\
839                 fi\n\n\
840                 # Validate stack state\n\
841                 echo \"🪝 Validating stack state before push...\"\n\
842                 if \"{cascade_cli}\" stack validate; then\n\
843                     echo \"✅ Stack validation passed\"\n\
844                 else\n\
845                     echo \"❌ Stack validation failed\"\n\
846                     echo \"💡 Fix validation errors before pushing:\"\n\
847                     echo \"   • ca doctor       - Check overall health\"\n\
848                     echo \"   • ca status       - Check current stack status\"\n\
849                     echo \"   • ca sync         - Sync with remote and rebase if needed\"\n\
850                     exit 1\n\
851                 fi\n\n\
852                 echo \"✅ Pre-push validation complete\"\n"
853            )
854        }
855    }
856
857    fn generate_commit_msg_hook(&self, _cascade_cli: &str) -> String {
858        #[cfg(windows)]
859        {
860            r#"@echo off
861rem Cascade CLI Hook - Commit Message
862rem Validates commit message format
863
864set COMMIT_MSG_FILE=%1
865if "%COMMIT_MSG_FILE%"=="" (
866    echo ❌ No commit message file provided
867    exit /b 1
868)
869
870rem Read commit message (Windows batch is limited, but this covers basic cases)
871for /f "delims=" %%i in ('type "%COMMIT_MSG_FILE%"') do set COMMIT_MSG=%%i
872
873rem Skip validation for merge commits, fixup commits, etc.
874echo %COMMIT_MSG% | findstr /B /C:"Merge" /C:"Revert" /C:"fixup!" /C:"squash!" >nul
875if %ERRORLEVEL% equ 0 exit /b 0
876
877rem Find repository root and check if Cascade is initialized
878for /f "tokens=*" %%i in ('git rev-parse --show-toplevel 2^>nul') do set REPO_ROOT=%%i
879if "%REPO_ROOT%"=="" set REPO_ROOT=.
880if not exist "%REPO_ROOT%\.cascade" exit /b 0
881
882rem Basic commit message validation
883echo %COMMIT_MSG% | findstr /R "^..........*" >nul
884if %ERRORLEVEL% neq 0 (
885    echo ❌ Commit message too short (minimum 10 characters)
886    echo 💡 Write a descriptive commit message for better stack management
887    exit /b 1
888)
889
890rem Check for very long messages (approximate check in batch)
891echo %COMMIT_MSG% | findstr /R "^..................................................................................*" >nul
892if %ERRORLEVEL% equ 0 (
893    echo ⚠️ Warning: Commit message longer than 72 characters
894    echo 💡 Consider keeping the first line short for better readability
895)
896
897rem Check for conventional commit format (optional)
898echo %COMMIT_MSG% | findstr /R "^(feat|fix|docs|style|refactor|test|chore|perf|ci|build)" >nul
899if %ERRORLEVEL% neq 0 (
900    echo 💡 Consider using conventional commit format:
901    echo    feat: add new feature
902    echo    fix: resolve bug
903    echo    docs: update documentation
904    echo    etc.
905)
906
907echo ✅ Commit message validation passed
908"#.to_string()
909        }
910
911        #[cfg(not(windows))]
912        {
913            r#"#!/bin/sh
914# Cascade CLI Hook - Commit Message
915# Validates commit message format
916
917set -e
918
919COMMIT_MSG_FILE="$1"
920COMMIT_MSG=$(cat "$COMMIT_MSG_FILE")
921
922# Skip validation for merge commits, fixup commits, etc.
923if echo "$COMMIT_MSG" | grep -E "^(Merge|Revert|fixup!|squash!)" > /dev/null; then
924    exit 0
925fi
926
927# Find repository root and check if Cascade is initialized
928REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || echo ".")
929if [ ! -d "$REPO_ROOT/.cascade" ]; then
930    exit 0
931fi
932
933# Basic commit message validation
934if [ ${#COMMIT_MSG} -lt 10 ]; then
935    echo "❌ Commit message too short (minimum 10 characters)"
936    echo "💡 Write a descriptive commit message for better stack management"
937    exit 1
938fi
939
940if [ ${#COMMIT_MSG} -gt 72 ]; then
941    echo "⚠️ Warning: Commit message longer than 72 characters"
942    echo "💡 Consider keeping the first line short for better readability"
943fi
944
945# Check for conventional commit format (optional)
946if ! echo "$COMMIT_MSG" | grep -E "^(feat|fix|docs|style|refactor|test|chore|perf|ci|build)(\(.+\))?: .+" > /dev/null; then
947    echo "💡 Consider using conventional commit format:"
948    echo "   feat: add new feature"
949    echo "   fix: resolve bug"
950    echo "   docs: update documentation"
951    echo "   etc."
952fi
953
954echo "✅ Commit message validation passed"
955"#.to_string()
956        }
957    }
958
959    #[allow(clippy::uninlined_format_args)]
960    fn generate_pre_commit_hook(&self, cascade_cli: &str) -> String {
961        #[cfg(windows)]
962        {
963            format!(
964                "@echo off\n\
965                 rem Cascade CLI Hook - Pre Commit\n\
966                 rem Smart edit mode guidance for better UX\n\n\
967                 rem Check if Cascade is initialized\n\
968                 for /f \\\"tokens=*\\\" %%i in ('git rev-parse --show-toplevel 2^>nul') do set REPO_ROOT=%%i\n\
969                 if \\\"%REPO_ROOT%\\\"==\\\"\\\" set REPO_ROOT=.\n\
970                 if not exist \\\"%REPO_ROOT%\\.cascade\\\" exit /b 0\n\n\
971                 rem Check if we're in edit mode\n\
972                 \\\"{0}\\\" entry status --quiet >nul 2>&1\n\
973                 if %ERRORLEVEL% equ 0 (\n\
974                     echo ⚠️ You're in EDIT MODE for a stack entry!\n\
975                     echo.\n\
976                     echo Choose your action:\n\
977                     echo   🔄 [A]mend: Modify the current entry\n\
978                     echo   ➕ [N]ew:   Create new entry on top ^(current behavior^)\n\
979                     echo   ❌ [C]ancel: Stop and think about it\n\
980                     echo.\n\
981                     set /p choice=\\\"Your choice (A/n/c): \\\"\n\
982                     \n\
983                     if /i \\\"%choice%\\\"==\\\"A\\\" (\n\
984                         echo ✅ Running: git commit --amend\n\
985                         git commit --amend\n\
986                         if %ERRORLEVEL% equ 0 (\n\
987                             echo 💡 Entry updated! Run 'ca sync' to update PRs\n\
988                         )\n\
989                         exit /b %ERRORLEVEL%\n\
990                     ) else if /i \\\"%choice%\\\"==\\\"C\\\" (\n\
991                         echo ❌ Commit cancelled\n\
992                         exit /b 1\n\
993                     ) else (\n\
994                         echo ➕ Creating new stack entry...\n\
995                         rem Let the commit proceed normally\n\
996                         exit /b 0\n\
997                     )\n\
998                 )\n\n\
999                 rem Not in edit mode, proceed normally\n\
1000                 exit /b 0\n",
1001                cascade_cli
1002            )
1003        }
1004
1005        #[cfg(not(windows))]
1006        {
1007            format!(
1008                "#!/bin/sh\n\
1009                 # Cascade CLI Hook - Pre Commit\n\
1010                 # Smart edit mode guidance for better UX\n\n\
1011                 set -e\n\n\
1012                 # Check if Cascade is initialized\n\
1013                 REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || echo \\\".\\\")\n\
1014                 if [ ! -d \\\"$REPO_ROOT/.cascade\\\" ]; then\n\
1015                     exit 0\n\
1016                 fi\n\n\
1017                 # Check if we're in edit mode\n\
1018                 if \\\"{0}\\\" entry status --quiet >/dev/null 2>&1; then\n\
1019                     echo \\\"⚠️ You're in EDIT MODE for a stack entry!\\\"\n\
1020                     echo \\\"\\\"\n\
1021                     echo \\\"Choose your action:\\\"\n\
1022                     echo \\\"  🔄 [A]mend: Modify the current entry\\\"\n\
1023                     echo \\\"  ➕ [N]ew:   Create new entry on top (current behavior)\\\"\n\
1024                     echo \\\"  ❌ [C]ancel: Stop and think about it\\\"\n\
1025                     echo \\\"\\\"\n\
1026                     \n\
1027                     # Read user choice with default to 'new'\n\
1028                     read -p \\\"Your choice (A/n/c): \\\" choice\n\
1029                     \n\
1030                     case \\\"$choice\\\" in\n\
1031                         [Aa])\n\
1032                             echo \\\"✅ Running: git commit --amend\\\"\n\
1033                             # Temporarily disable hooks to avoid recursion\n\
1034                             git -c core.hooksPath=/dev/null commit --amend\n\
1035                             if [ $? -eq 0 ]; then\n\
1036                                 echo \\\"💡 Entry updated! Run 'ca sync' to update PRs\\\"\n\
1037                             fi\n\
1038                             exit $?\n\
1039                             ;;\n\
1040                         [Cc])\n\
1041                             echo \\\"❌ Commit cancelled\\\"\n\
1042                             exit 1\n\
1043                             ;;\n\
1044                         *)\n\
1045                             echo \\\"➕ Creating new stack entry...\\\"\n\
1046                             # Let the commit proceed normally\n\
1047                             exit 0\n\
1048                             ;;\n\
1049                     esac\n\
1050                 fi\n\n\
1051                 # Not in edit mode, proceed normally\n\
1052                 exit 0\n",
1053                cascade_cli
1054            )
1055        }
1056    }
1057
1058    fn generate_prepare_commit_msg_hook(&self, cascade_cli: &str) -> String {
1059        #[cfg(windows)]
1060        {
1061            format!(
1062                "@echo off\n\
1063                 rem Cascade CLI Hook - Prepare Commit Message\n\
1064                 rem Adds stack context to commit messages\n\n\
1065                 set COMMIT_MSG_FILE=%1\n\
1066                 set COMMIT_SOURCE=%2\n\
1067                 set COMMIT_SHA=%3\n\n\
1068                 rem Only modify message if it's a regular commit (not merge, template, etc.)\n\
1069                 if not \"%COMMIT_SOURCE%\"==\"\" if not \"%COMMIT_SOURCE%\"==\"message\" exit /b 0\n\n\
1070                 rem Find repository root and check if Cascade is initialized\n\
1071                 for /f \"tokens=*\" %%i in ('git rev-parse --show-toplevel 2^>nul') do set REPO_ROOT=%%i\n\
1072                 if \"%REPO_ROOT%\"==\"\" set REPO_ROOT=.\n\
1073                 if not exist \"%REPO_ROOT%\\.cascade\" exit /b 0\n\n\
1074                 rem Check if in edit mode first\n\
1075                 for /f \"tokens=*\" %%i in ('\"{cascade_cli}\" entry status --quiet 2^>nul') do set EDIT_STATUS=%%i\n\
1076                 if \"%EDIT_STATUS%\"==\"\" set EDIT_STATUS=inactive\n\n\
1077                 if not \"%EDIT_STATUS%\"==\"inactive\" (\n\
1078                     rem In edit mode - provide smart guidance\n\
1079                     set /p CURRENT_MSG=<%COMMIT_MSG_FILE%\n\n\
1080                     rem Skip if message already has edit guidance\n\
1081                     echo !CURRENT_MSG! | findstr \"[EDIT MODE]\" >nul\n\
1082                     if %ERRORLEVEL% equ 0 exit /b 0\n\n\
1083                     rem Add edit mode guidance to commit message\n\
1084                     echo.\n\
1085                     echo # [EDIT MODE] You're editing a stack entry\n\
1086                     echo #\n\
1087                     echo # Choose your action:\n\
1088                     echo #   🔄 AMEND: To modify the current entry, use:\n\
1089                     echo #       git commit --amend\n\
1090                     echo #\n\
1091                     echo #   ➕ NEW: To create a new entry on top, use:\n\
1092                     echo #       git commit    ^(this command^)\n\
1093                     echo #\n\
1094                     echo # 💡 After committing, run 'ca sync' to update PRs\n\
1095                     echo.\n\
1096                     type \"%COMMIT_MSG_FILE%\"\n\
1097                 ) > \"%COMMIT_MSG_FILE%.tmp\" && (\n\
1098                     move \"%COMMIT_MSG_FILE%.tmp\" \"%COMMIT_MSG_FILE%\"\n\
1099                 ) else (\n\
1100                     rem Regular stack mode - check for active stack\n\
1101                     for /f \"tokens=*\" %%i in ('\"{cascade_cli}\" stack list --active --format=name 2^>nul') do set ACTIVE_STACK=%%i\n\n\
1102                     if not \"%ACTIVE_STACK%\"==\"\" (\n\
1103                         rem Get current commit message\n\
1104                         set /p CURRENT_MSG=<%COMMIT_MSG_FILE%\n\n\
1105                         rem Skip if message already has stack context\n\
1106                         echo !CURRENT_MSG! | findstr \"[stack:\" >nul\n\
1107                         if %ERRORLEVEL% equ 0 exit /b 0\n\n\
1108                         rem Add stack context to commit message\n\
1109                         echo.\n\
1110                         echo # Stack: %ACTIVE_STACK%\n\
1111                         echo # This commit will be added to the active stack automatically.\n\
1112                         echo # Use 'ca stack status' to see the current stack state.\n\
1113                         type \"%COMMIT_MSG_FILE%\"\n\
1114                     ) > \"%COMMIT_MSG_FILE%.tmp\"\n\
1115                     move \"%COMMIT_MSG_FILE%.tmp\" \"%COMMIT_MSG_FILE%\"\n\
1116                 )\n"
1117            )
1118        }
1119
1120        #[cfg(not(windows))]
1121        {
1122            format!(
1123                "#!/bin/sh\n\
1124                 # Cascade CLI Hook - Prepare Commit Message\n\
1125                 # Adds stack context to commit messages\n\n\
1126                 set -e\n\n\
1127                 COMMIT_MSG_FILE=\"$1\"\n\
1128                 COMMIT_SOURCE=\"$2\"\n\
1129                 COMMIT_SHA=\"$3\"\n\n\
1130                 # Only modify message if it's a regular commit (not merge, template, etc.)\n\
1131                 if [ \"$COMMIT_SOURCE\" != \"\" ] && [ \"$COMMIT_SOURCE\" != \"message\" ]; then\n\
1132                     exit 0\n\
1133                 fi\n\n\
1134                 # Find repository root and check if Cascade is initialized\n\
1135                 REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || echo \".\")\n\
1136                 if [ ! -d \"$REPO_ROOT/.cascade\" ]; then\n\
1137                     exit 0\n\
1138                 fi\n\n\
1139                 # Check if in edit mode first\n\
1140                 EDIT_STATUS=$(\"{cascade_cli}\" entry status --quiet 2>/dev/null || echo \"inactive\")\n\
1141                 \n\
1142                 if [ \"$EDIT_STATUS\" != \"inactive\" ]; then\n\
1143                     # In edit mode - provide smart guidance\n\
1144                     CURRENT_MSG=$(cat \"$COMMIT_MSG_FILE\")\n\
1145                     \n\
1146                     # Skip if message already has edit guidance\n\
1147                     if echo \"$CURRENT_MSG\" | grep -q \"\\[EDIT MODE\\]\"; then\n\
1148                         exit 0\n\
1149                     fi\n\
1150                     \n\
1151                     echo \"\n\
1152                 # [EDIT MODE] You're editing a stack entry\n\
1153                 #\n\
1154                 # Choose your action:\n\
1155                 #   🔄 AMEND: To modify the current entry, use:\n\
1156                 #       git commit --amend\n\
1157                 #\n\
1158                 #   ➕ NEW: To create a new entry on top, use:\n\
1159                 #       git commit    (this command)\n\
1160                 #\n\
1161                 # 💡 After committing, run 'ca sync' to update PRs\n\
1162                 \n\
1163                 $CURRENT_MSG\" > \"$COMMIT_MSG_FILE\"\n\
1164                 else\n\
1165                     # Regular stack mode - check for active stack\n\
1166                     ACTIVE_STACK=$(\"{cascade_cli}\" stack list --active --format=name 2>/dev/null || echo \"\")\n\
1167                     \n\
1168                     if [ -n \"$ACTIVE_STACK\" ]; then\n\
1169                         # Get current commit message\n\
1170                         CURRENT_MSG=$(cat \"$COMMIT_MSG_FILE\")\n\
1171                         \n\
1172                         # Skip if message already has stack context\n\
1173                         if echo \"$CURRENT_MSG\" | grep -q \"\\[stack:\"; then\n\
1174                             exit 0\n\
1175                         fi\n\
1176                         \n\
1177                         # Add stack context to commit message\n\
1178                         echo \"\n\
1179                     # Stack: $ACTIVE_STACK\n\
1180                     # This commit will be added to the active stack automatically.\n\
1181                     # Use 'ca stack status' to see the current stack state.\n\
1182                     $CURRENT_MSG\" > \"$COMMIT_MSG_FILE\"\n\
1183                     fi\n\
1184                 fi\n"
1185            )
1186        }
1187    }
1188
1189    /// Detect repository type from remote URLs
1190    pub fn detect_repository_type(&self) -> Result<RepositoryType> {
1191        let output = Command::new("git")
1192            .args(["remote", "get-url", "origin"])
1193            .current_dir(&self.repo_path)
1194            .output()
1195            .map_err(|e| CascadeError::config(format!("Failed to get remote URL: {e}")))?;
1196
1197        if !output.status.success() {
1198            return Ok(RepositoryType::Unknown);
1199        }
1200
1201        let remote_url = String::from_utf8_lossy(&output.stdout)
1202            .trim()
1203            .to_lowercase();
1204
1205        if remote_url.contains("github.com") {
1206            Ok(RepositoryType::GitHub)
1207        } else if remote_url.contains("gitlab.com") || remote_url.contains("gitlab") {
1208            Ok(RepositoryType::GitLab)
1209        } else if remote_url.contains("dev.azure.com") || remote_url.contains("visualstudio.com") {
1210            Ok(RepositoryType::AzureDevOps)
1211        } else if remote_url.contains("bitbucket") {
1212            Ok(RepositoryType::Bitbucket)
1213        } else {
1214            Ok(RepositoryType::Unknown)
1215        }
1216    }
1217
1218    /// Detect current branch type
1219    pub fn detect_branch_type(&self) -> Result<BranchType> {
1220        let output = Command::new("git")
1221            .args(["branch", "--show-current"])
1222            .current_dir(&self.repo_path)
1223            .output()
1224            .map_err(|e| CascadeError::config(format!("Failed to get current branch: {e}")))?;
1225
1226        if !output.status.success() {
1227            return Ok(BranchType::Unknown);
1228        }
1229
1230        let branch_name = String::from_utf8_lossy(&output.stdout)
1231            .trim()
1232            .to_lowercase();
1233
1234        if branch_name == "main" || branch_name == "master" || branch_name == "develop" {
1235            Ok(BranchType::Main)
1236        } else if !branch_name.is_empty() {
1237            Ok(BranchType::Feature)
1238        } else {
1239            Ok(BranchType::Unknown)
1240        }
1241    }
1242
1243    /// Validate prerequisites for hook installation
1244    pub fn validate_prerequisites(&self) -> Result<()> {
1245        Output::check_start("Checking prerequisites for Cascade hooks");
1246
1247        // 1. Check repository type
1248        let repo_type = self.detect_repository_type()?;
1249        match repo_type {
1250            RepositoryType::Bitbucket => {
1251                Output::success("Bitbucket repository detected");
1252                Output::tip("Hooks will work great with 'ca submit' and 'ca autoland' for Bitbucket integration");
1253            }
1254            RepositoryType::GitHub => {
1255                Output::success("GitHub repository detected");
1256                Output::tip("Consider setting up GitHub Actions for CI/CD integration");
1257            }
1258            RepositoryType::GitLab => {
1259                Output::success("GitLab repository detected");
1260                Output::tip("GitLab CI integration works well with Cascade stacks");
1261            }
1262            RepositoryType::AzureDevOps => {
1263                Output::success("Azure DevOps repository detected");
1264                Output::tip("Azure Pipelines can be configured to work with Cascade workflows");
1265            }
1266            RepositoryType::Unknown => {
1267                Output::info(
1268                    "Unknown repository type - hooks will still work for local Git operations",
1269                );
1270            }
1271        }
1272
1273        // 2. Check Cascade configuration
1274        let config_dir = crate::config::get_repo_config_dir(&self.repo_path)?;
1275        let config_path = config_dir.join("config.json");
1276        if !config_path.exists() {
1277            return Err(CascadeError::config(
1278                "🚫 Cascade not initialized!\n\n\
1279                Please run 'ca init' or 'ca setup' first to configure Cascade CLI.\n\
1280                Hooks require proper Bitbucket Server configuration.\n\n\
1281                Use --force to install anyway (not recommended)."
1282                    .to_string(),
1283            ));
1284        }
1285
1286        // 3. Validate Bitbucket configuration
1287        let config = Settings::load_from_file(&config_path)?;
1288
1289        if config.bitbucket.url == "https://bitbucket.example.com"
1290            || config.bitbucket.url.contains("example.com")
1291        {
1292            return Err(CascadeError::config(
1293                "🚫 Invalid Bitbucket configuration!\n\n\
1294                Your Bitbucket URL appears to be a placeholder.\n\
1295                Please run 'ca setup' to configure a real Bitbucket Server.\n\n\
1296                Use --force to install anyway (not recommended)."
1297                    .to_string(),
1298            ));
1299        }
1300
1301        if config.bitbucket.project == "PROJECT" || config.bitbucket.repo == "repo" {
1302            return Err(CascadeError::config(
1303                "🚫 Incomplete Bitbucket configuration!\n\n\
1304                Your project/repository settings appear to be placeholders.\n\
1305                Please run 'ca setup' to complete configuration.\n\n\
1306                Use --force to install anyway (not recommended)."
1307                    .to_string(),
1308            ));
1309        }
1310
1311        Output::success("Prerequisites validation passed");
1312        Ok(())
1313    }
1314
1315    /// Validate branch suitability for hooks
1316    pub fn validate_branch_suitability(&self) -> Result<()> {
1317        let branch_type = self.detect_branch_type()?;
1318
1319        match branch_type {
1320            BranchType::Main => {
1321                return Err(CascadeError::config(
1322                    "🚫 Currently on main/master branch!\n\n\
1323                    Cascade hooks are designed for feature branch development.\n\
1324                    Working directly on main/master with stacked diffs can:\n\
1325                    • Complicate the commit history\n\
1326                    • Interfere with team collaboration\n\
1327                    • Break CI/CD workflows\n\n\
1328                    💡 Recommended workflow:\n\
1329                    1. Create a feature branch: git checkout -b feature/my-feature\n\
1330                    2. Install hooks: ca hooks install\n\
1331                    3. Develop with stacked commits (auto-added with hooks)\n\
1332                    4. Push & submit: ca push && ca submit (all by default)\n\
1333                    5. Auto-land when ready: ca autoland\n\n\
1334                    Use --force to install anyway (not recommended)."
1335                        .to_string(),
1336                ));
1337            }
1338            BranchType::Feature => {
1339                Output::success("Feature branch detected - suitable for stacked development");
1340            }
1341            BranchType::Unknown => {
1342                Output::warning("Unknown branch type - proceeding with caution");
1343            }
1344        }
1345
1346        Ok(())
1347    }
1348
1349    /// Confirm installation with user
1350    pub fn confirm_installation(&self) -> Result<()> {
1351        Output::section("Hook Installation Summary");
1352
1353        let hooks = vec![
1354            HookType::PostCommit,
1355            HookType::PrePush,
1356            HookType::CommitMsg,
1357            HookType::PrepareCommitMsg,
1358        ];
1359
1360        for hook in &hooks {
1361            Output::sub_item(format!("{}: {}", hook.filename(), hook.description()));
1362        }
1363
1364        println!();
1365        Output::section("These hooks will automatically");
1366        Output::bullet("Add commits to your active stack");
1367        Output::bullet("Validate commit messages");
1368        Output::bullet("Prevent force pushes that break stack integrity");
1369        Output::bullet("Add stack context to commit messages");
1370
1371        println!();
1372        Output::section("With hooks + new defaults, your workflow becomes");
1373        Output::sub_item("git commit       → Auto-added to stack");
1374        Output::sub_item("ca push          → Pushes all by default");
1375        Output::sub_item("ca submit        → Submits all by default");
1376        Output::sub_item("ca autoland      → Auto-merges when ready");
1377
1378        // Interactive confirmation to proceed with installation
1379        let should_install = Confirm::with_theme(&ColorfulTheme::default())
1380            .with_prompt("Install Cascade hooks?")
1381            .default(true)
1382            .interact()
1383            .map_err(|e| CascadeError::config(format!("Failed to get user confirmation: {e}")))?;
1384
1385        if should_install {
1386            Output::success("Proceeding with installation");
1387            Ok(())
1388        } else {
1389            Err(CascadeError::config(
1390                "Installation cancelled by user".to_string(),
1391            ))
1392        }
1393    }
1394}
1395
1396/// Run hooks management commands
1397pub async fn install() -> Result<()> {
1398    install_with_options(false, false, false, false).await
1399}
1400
1401pub async fn install_essential() -> Result<()> {
1402    let current_dir = env::current_dir()
1403        .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
1404
1405    let repo_root = find_repository_root(&current_dir)
1406        .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
1407
1408    let hooks_manager = HooksManager::new(&repo_root)?;
1409    hooks_manager.install_essential()
1410}
1411
1412pub async fn install_with_options(
1413    skip_checks: bool,
1414    allow_main_branch: bool,
1415    yes: bool,
1416    force: bool,
1417) -> Result<()> {
1418    let current_dir = env::current_dir()
1419        .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
1420
1421    let repo_root = find_repository_root(&current_dir)
1422        .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
1423
1424    let hooks_manager = HooksManager::new(&repo_root)?;
1425
1426    let options = InstallOptions {
1427        check_prerequisites: !skip_checks,
1428        feature_branches_only: !allow_main_branch,
1429        confirm: !yes,
1430        force,
1431    };
1432
1433    hooks_manager.install_with_options(&options)
1434}
1435
1436pub async fn uninstall() -> Result<()> {
1437    let current_dir = env::current_dir()
1438        .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
1439
1440    let repo_root = find_repository_root(&current_dir)
1441        .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
1442
1443    let hooks_manager = HooksManager::new(&repo_root)?;
1444    hooks_manager.uninstall_all()
1445}
1446
1447pub async fn status() -> Result<()> {
1448    let current_dir = env::current_dir()
1449        .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
1450
1451    let repo_root = find_repository_root(&current_dir)
1452        .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
1453
1454    let hooks_manager = HooksManager::new(&repo_root)?;
1455    hooks_manager.list_installed_hooks()
1456}
1457
1458pub async fn install_hook(hook_name: &str) -> Result<()> {
1459    install_hook_with_options(hook_name, false, false).await
1460}
1461
1462pub async fn install_hook_with_options(
1463    hook_name: &str,
1464    skip_checks: bool,
1465    force: bool,
1466) -> Result<()> {
1467    let current_dir = env::current_dir()
1468        .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
1469
1470    let repo_root = find_repository_root(&current_dir)
1471        .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
1472
1473    let hooks_manager = HooksManager::new(&repo_root)?;
1474
1475    let hook_type = match hook_name {
1476        "post-commit" => HookType::PostCommit,
1477        "pre-push" => HookType::PrePush,
1478        "commit-msg" => HookType::CommitMsg,
1479        "pre-commit" => HookType::PreCommit,
1480        "prepare-commit-msg" => HookType::PrepareCommitMsg,
1481        _ => {
1482            return Err(CascadeError::config(format!(
1483                "Unknown hook type: {hook_name}"
1484            )))
1485        }
1486    };
1487
1488    // Run basic validation if not skipped
1489    if !skip_checks && !force {
1490        hooks_manager.validate_prerequisites()?;
1491    }
1492
1493    hooks_manager.install_hook(&hook_type)
1494}
1495
1496pub async fn uninstall_hook(hook_name: &str) -> Result<()> {
1497    let current_dir = env::current_dir()
1498        .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
1499
1500    let repo_root = find_repository_root(&current_dir)
1501        .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
1502
1503    let hooks_manager = HooksManager::new(&repo_root)?;
1504
1505    let hook_type = match hook_name {
1506        "post-commit" => HookType::PostCommit,
1507        "pre-push" => HookType::PrePush,
1508        "commit-msg" => HookType::CommitMsg,
1509        "pre-commit" => HookType::PreCommit,
1510        "prepare-commit-msg" => HookType::PrepareCommitMsg,
1511        _ => {
1512            return Err(CascadeError::config(format!(
1513                "Unknown hook type: {hook_name}"
1514            )))
1515        }
1516    };
1517
1518    hooks_manager.uninstall_hook(&hook_type)
1519}
1520
1521#[cfg(test)]
1522mod tests {
1523    use super::*;
1524    use std::process::Command;
1525    use tempfile::TempDir;
1526
1527    fn create_test_repo() -> (TempDir, std::path::PathBuf) {
1528        let temp_dir = TempDir::new().unwrap();
1529        let repo_path = temp_dir.path().to_path_buf();
1530
1531        // Initialize git repository
1532        Command::new("git")
1533            .args(["init"])
1534            .current_dir(&repo_path)
1535            .output()
1536            .unwrap();
1537        Command::new("git")
1538            .args(["config", "user.name", "Test"])
1539            .current_dir(&repo_path)
1540            .output()
1541            .unwrap();
1542        Command::new("git")
1543            .args(["config", "user.email", "test@test.com"])
1544            .current_dir(&repo_path)
1545            .output()
1546            .unwrap();
1547
1548        // Create initial commit
1549        std::fs::write(repo_path.join("README.md"), "# Test").unwrap();
1550        Command::new("git")
1551            .args(["add", "."])
1552            .current_dir(&repo_path)
1553            .output()
1554            .unwrap();
1555        Command::new("git")
1556            .args(["commit", "-m", "Initial"])
1557            .current_dir(&repo_path)
1558            .output()
1559            .unwrap();
1560
1561        // Initialize cascade
1562        crate::config::initialize_repo(&repo_path, Some("https://test.bitbucket.com".to_string()))
1563            .unwrap();
1564
1565        (temp_dir, repo_path)
1566    }
1567
1568    #[test]
1569    fn test_hooks_manager_creation() {
1570        let (_temp_dir, repo_path) = create_test_repo();
1571        let _manager = HooksManager::new(&repo_path).unwrap();
1572
1573        assert_eq!(_manager.repo_path, repo_path);
1574        // Should create a HooksManager successfully
1575        assert!(!_manager.repo_id.is_empty());
1576    }
1577
1578    #[test]
1579    fn test_hooks_manager_custom_hooks_path() {
1580        let (_temp_dir, repo_path) = create_test_repo();
1581
1582        // Set custom hooks path
1583        Command::new("git")
1584            .args(["config", "core.hooksPath", "custom-hooks"])
1585            .current_dir(&repo_path)
1586            .output()
1587            .unwrap();
1588
1589        // Create the custom hooks directory
1590        let custom_hooks_dir = repo_path.join("custom-hooks");
1591        std::fs::create_dir_all(&custom_hooks_dir).unwrap();
1592
1593        let _manager = HooksManager::new(&repo_path).unwrap();
1594
1595        assert_eq!(_manager.repo_path, repo_path);
1596        // Should create a HooksManager successfully
1597        assert!(!_manager.repo_id.is_empty());
1598    }
1599
1600    #[test]
1601    fn test_hook_chaining_with_existing_hooks() {
1602        let (_temp_dir, repo_path) = create_test_repo();
1603        let manager = HooksManager::new(&repo_path).unwrap();
1604
1605        let hook_type = HookType::PreCommit;
1606        let hook_path = repo_path.join(".git/hooks").join(hook_type.filename());
1607
1608        // Create an existing project hook
1609        let existing_hook_content = "#!/bin/bash\n# Project pre-commit hook\n./scripts/lint.sh\n";
1610        std::fs::write(&hook_path, existing_hook_content).unwrap();
1611        crate::utils::platform::make_executable(&hook_path).unwrap();
1612
1613        // Install cascade hook (uses core.hooksPath, doesn't modify original)
1614        let result = manager.install_hook(&hook_type);
1615        assert!(result.is_ok());
1616
1617        // Original hook should remain unchanged
1618        let original_content = std::fs::read_to_string(&hook_path).unwrap();
1619        assert!(original_content.contains("# Project pre-commit hook"));
1620        assert!(original_content.contains("./scripts/lint.sh"));
1621
1622        // Cascade hook should exist in cascade directory
1623        let cascade_hooks_dir = manager.get_cascade_hooks_dir().unwrap();
1624        let cascade_hook_path = cascade_hooks_dir.join(hook_type.filename());
1625        assert!(cascade_hook_path.exists());
1626
1627        // Test uninstall removes cascade hooks but leaves original
1628        let uninstall_result = manager.uninstall_hook(&hook_type);
1629        assert!(uninstall_result.is_ok());
1630
1631        // Original hook should still exist and be unchanged
1632        let after_uninstall = std::fs::read_to_string(&hook_path).unwrap();
1633        assert!(after_uninstall.contains("# Project pre-commit hook"));
1634        assert!(after_uninstall.contains("./scripts/lint.sh"));
1635
1636        // Cascade hook should be removed
1637        assert!(!cascade_hook_path.exists());
1638    }
1639
1640    #[test]
1641    fn test_hook_installation() {
1642        let (_temp_dir, repo_path) = create_test_repo();
1643        let manager = HooksManager::new(&repo_path).unwrap();
1644
1645        // Test installing post-commit hook
1646        let hook_type = HookType::PostCommit;
1647        let result = manager.install_hook(&hook_type);
1648        assert!(result.is_ok());
1649
1650        // Verify hook file exists in cascade hooks directory
1651        let hook_filename = hook_type.filename();
1652        let cascade_hooks_dir = manager.get_cascade_hooks_dir().unwrap();
1653        let hook_path = cascade_hooks_dir.join(&hook_filename);
1654        assert!(hook_path.exists());
1655
1656        // Verify hook is executable (platform-specific)
1657        #[cfg(unix)]
1658        {
1659            use std::os::unix::fs::PermissionsExt;
1660            let metadata = std::fs::metadata(&hook_path).unwrap();
1661            let permissions = metadata.permissions();
1662            assert!(permissions.mode() & 0o111 != 0); // Check executable bit
1663        }
1664
1665        #[cfg(windows)]
1666        {
1667            // On Windows, verify it has .bat extension and file exists
1668            assert!(hook_filename.ends_with(".bat"));
1669            assert!(hook_path.exists());
1670        }
1671    }
1672
1673    #[test]
1674    fn test_hook_detection() {
1675        let (_temp_dir, repo_path) = create_test_repo();
1676        let _manager = HooksManager::new(&repo_path).unwrap();
1677
1678        // Check if hook files exist with platform-appropriate filenames
1679        let post_commit_path = repo_path
1680            .join(".git/hooks")
1681            .join(HookType::PostCommit.filename());
1682        let pre_push_path = repo_path
1683            .join(".git/hooks")
1684            .join(HookType::PrePush.filename());
1685        let commit_msg_path = repo_path
1686            .join(".git/hooks")
1687            .join(HookType::CommitMsg.filename());
1688
1689        // Initially no hooks should be installed
1690        assert!(!post_commit_path.exists());
1691        assert!(!pre_push_path.exists());
1692        assert!(!commit_msg_path.exists());
1693    }
1694
1695    #[test]
1696    fn test_hook_validation() {
1697        let (_temp_dir, repo_path) = create_test_repo();
1698        let manager = HooksManager::new(&repo_path).unwrap();
1699
1700        // Test validation - may fail in CI due to missing dependencies
1701        let validation = manager.validate_prerequisites();
1702        // In CI environment, validation might fail due to missing configuration
1703        // Just ensure it doesn't panic
1704        let _ = validation; // Don't assert ok/err, just ensure no panic
1705
1706        // Test branch validation - should work regardless of environment
1707        let branch_validation = manager.validate_branch_suitability();
1708        // Branch validation should work in most cases, but be tolerant
1709        let _ = branch_validation; // Don't assert ok/err, just ensure no panic
1710    }
1711
1712    #[test]
1713    fn test_hook_uninstallation() {
1714        let (_temp_dir, repo_path) = create_test_repo();
1715        let manager = HooksManager::new(&repo_path).unwrap();
1716
1717        // Install then uninstall hook
1718        let hook_type = HookType::PostCommit;
1719        manager.install_hook(&hook_type).unwrap();
1720
1721        let cascade_hooks_dir = manager.get_cascade_hooks_dir().unwrap();
1722        let hook_path = cascade_hooks_dir.join(hook_type.filename());
1723        assert!(hook_path.exists());
1724
1725        let result = manager.uninstall_hook(&hook_type);
1726        assert!(result.is_ok());
1727        assert!(!hook_path.exists());
1728    }
1729
1730    #[test]
1731    fn test_hook_content_generation() {
1732        let (_temp_dir, repo_path) = create_test_repo();
1733        let manager = HooksManager::new(&repo_path).unwrap();
1734
1735        // Use a known binary name for testing
1736        let binary_name = "cascade-cli";
1737
1738        // Test post-commit hook generation
1739        let post_commit_content = manager.generate_post_commit_hook(binary_name);
1740        #[cfg(windows)]
1741        {
1742            assert!(post_commit_content.contains("@echo off"));
1743            assert!(post_commit_content.contains("rem Cascade CLI Hook"));
1744        }
1745        #[cfg(not(windows))]
1746        {
1747            assert!(post_commit_content.contains("#!/bin/sh"));
1748            assert!(post_commit_content.contains("# Cascade CLI Hook"));
1749        }
1750        assert!(post_commit_content.contains(binary_name));
1751
1752        // Test pre-push hook generation
1753        let pre_push_content = manager.generate_pre_push_hook(binary_name);
1754        #[cfg(windows)]
1755        {
1756            assert!(pre_push_content.contains("@echo off"));
1757            assert!(pre_push_content.contains("rem Cascade CLI Hook"));
1758        }
1759        #[cfg(not(windows))]
1760        {
1761            assert!(pre_push_content.contains("#!/bin/sh"));
1762            assert!(pre_push_content.contains("# Cascade CLI Hook"));
1763        }
1764        assert!(pre_push_content.contains(binary_name));
1765
1766        // Test commit-msg hook generation (doesn't use binary, just validates)
1767        let commit_msg_content = manager.generate_commit_msg_hook(binary_name);
1768        #[cfg(windows)]
1769        {
1770            assert!(commit_msg_content.contains("@echo off"));
1771            assert!(commit_msg_content.contains("rem Cascade CLI Hook"));
1772        }
1773        #[cfg(not(windows))]
1774        {
1775            assert!(commit_msg_content.contains("#!/bin/sh"));
1776            assert!(commit_msg_content.contains("# Cascade CLI Hook"));
1777        }
1778
1779        // Test prepare-commit-msg hook generation (does use binary)
1780        let prepare_commit_content = manager.generate_prepare_commit_msg_hook(binary_name);
1781        #[cfg(windows)]
1782        {
1783            assert!(prepare_commit_content.contains("@echo off"));
1784            assert!(prepare_commit_content.contains("rem Cascade CLI Hook"));
1785        }
1786        #[cfg(not(windows))]
1787        {
1788            assert!(prepare_commit_content.contains("#!/bin/sh"));
1789            assert!(prepare_commit_content.contains("# Cascade CLI Hook"));
1790        }
1791        assert!(prepare_commit_content.contains(binary_name));
1792    }
1793
1794    #[test]
1795    fn test_hook_status_reporting() {
1796        let (_temp_dir, repo_path) = create_test_repo();
1797        let manager = HooksManager::new(&repo_path).unwrap();
1798
1799        // Check repository type detection - should work with our test setup
1800        let repo_type = manager.detect_repository_type().unwrap();
1801        // In CI environment, this might be Unknown if remote detection fails
1802        assert!(matches!(
1803            repo_type,
1804            RepositoryType::Bitbucket | RepositoryType::Unknown
1805        ));
1806
1807        // Check branch type detection
1808        let branch_type = manager.detect_branch_type().unwrap();
1809        // Should be on main/master branch, but allow for different default branch names
1810        assert!(matches!(
1811            branch_type,
1812            BranchType::Main | BranchType::Unknown
1813        ));
1814    }
1815
1816    #[test]
1817    fn test_force_installation() {
1818        let (_temp_dir, repo_path) = create_test_repo();
1819        let manager = HooksManager::new(&repo_path).unwrap();
1820
1821        // Create a fake existing hook with platform-appropriate content
1822        let hook_filename = HookType::PostCommit.filename();
1823        let hook_path = repo_path.join(".git/hooks").join(&hook_filename);
1824
1825        #[cfg(windows)]
1826        let existing_content = "@echo off\necho existing hook";
1827        #[cfg(not(windows))]
1828        let existing_content = "#!/bin/sh\necho 'existing hook'";
1829
1830        std::fs::write(&hook_path, existing_content).unwrap();
1831
1832        // Install cascade hook (uses core.hooksPath, doesn't modify original)
1833        let hook_type = HookType::PostCommit;
1834        let result = manager.install_hook(&hook_type);
1835        assert!(result.is_ok());
1836
1837        // Verify cascade hook exists in cascade directory
1838        let cascade_hooks_dir = manager.get_cascade_hooks_dir().unwrap();
1839        let cascade_hook_path = cascade_hooks_dir.join(&hook_filename);
1840        assert!(cascade_hook_path.exists());
1841
1842        // Original hook should remain unchanged
1843        let original_content = std::fs::read_to_string(&hook_path).unwrap();
1844        assert!(original_content.contains("existing hook"));
1845
1846        // Cascade hook should contain cascade logic
1847        let cascade_content = std::fs::read_to_string(&cascade_hook_path).unwrap();
1848        assert!(cascade_content.contains("cascade-cli") || cascade_content.contains("ca"));
1849    }
1850}