Skip to main content

git_side/commands/
hook.rs

1use std::fs;
2use std::os::unix::fs::PermissionsExt;
3use std::path::PathBuf;
4
5use colored::Colorize;
6
7use crate::error::{Error, Result};
8use crate::git;
9
10const HOOK_MARKER_START: &str = "# >>> git-side auto >>>";
11const HOOK_MARKER_END: &str = "# <<< git-side auto <<<";
12const HOOK_CONTENT: &str = r"
13# Auto-sync side-tracked files
14git side auto
15";
16
17/// Get the path to a git hook.
18fn hook_path(hook_name: &str) -> Result<PathBuf> {
19    let git_dir = git::git_dir()?;
20    Ok(git_dir.join("hooks").join(hook_name))
21}
22
23/// Check if our hook is already installed.
24fn is_installed(hook_name: &str) -> Result<bool> {
25    let path = hook_path(hook_name)?;
26    if !path.exists() {
27        return Ok(false);
28    }
29
30    let content = fs::read_to_string(&path).map_err(|e| Error::ReadFile {
31        path: path.clone(),
32        source: e,
33    })?;
34
35    Ok(content.contains(HOOK_MARKER_START))
36}
37
38/// Install the git-side hook.
39///
40/// # Errors
41///
42/// Returns an error if the hook is already installed or if file operations fail.
43pub fn install(hook_name: &str) -> Result<()> {
44    if is_installed(hook_name)? {
45        return Err(Error::HookAlreadyInstalled(hook_name.to_string()));
46    }
47
48    let path = hook_path(hook_name)?;
49
50    // Ensure hooks directory exists
51    if let Some(parent) = path.parent() {
52        fs::create_dir_all(parent).map_err(|e| Error::CreateDir {
53            path: parent.to_path_buf(),
54            source: e,
55        })?;
56    }
57
58    // Read existing content or create new
59    let existing = if path.exists() {
60        fs::read_to_string(&path).map_err(|e| Error::ReadFile {
61            path: path.clone(),
62            source: e,
63        })?
64    } else {
65        "#!/bin/sh\n".to_string()
66    };
67
68    // Append our hook
69    let new_content = format!(
70        "{existing}\n{HOOK_MARKER_START}{HOOK_CONTENT}{HOOK_MARKER_END}\n"
71    );
72
73    fs::write(&path, new_content).map_err(|e| Error::WriteFile {
74        path: path.clone(),
75        source: e,
76    })?;
77
78    // Make executable
79    let mut perms = fs::metadata(&path)
80        .map_err(|e| Error::ReadFile {
81            path: path.clone(),
82            source: e,
83        })?
84        .permissions();
85    perms.set_mode(0o755);
86    fs::set_permissions(&path, perms).map_err(|e| Error::WriteFile {
87        path: path.clone(),
88        source: e,
89    })?;
90
91    println!(
92        "{} {} hook installed",
93        "Done.".green().bold(),
94        hook_name.cyan()
95    );
96
97    Ok(())
98}
99
100/// Uninstall the git-side hook.
101///
102/// # Errors
103///
104/// Returns an error if the hook is not installed or if file operations fail.
105pub fn uninstall(hook_name: &str) -> Result<()> {
106    if !is_installed(hook_name)? {
107        return Err(Error::HookNotInstalled(hook_name.to_string()));
108    }
109
110    let path = hook_path(hook_name)?;
111
112    let content = fs::read_to_string(&path).map_err(|e| Error::ReadFile {
113        path: path.clone(),
114        source: e,
115    })?;
116
117    // Remove our section
118    let mut new_lines = Vec::new();
119    let mut in_our_section = false;
120
121    for line in content.lines() {
122        if line.contains(HOOK_MARKER_START) {
123            in_our_section = true;
124            continue;
125        }
126        if line.contains(HOOK_MARKER_END) {
127            in_our_section = false;
128            continue;
129        }
130        if !in_our_section {
131            new_lines.push(line);
132        }
133    }
134
135    let new_content = new_lines.join("\n");
136
137    // Check if only shebang remains
138    let trimmed = new_content.trim();
139    if trimmed.is_empty() || trimmed == "#!/bin/sh" || trimmed == "#!/bin/bash" {
140        // Remove the file entirely
141        fs::remove_file(&path).map_err(|e| Error::WriteFile {
142            path: path.clone(),
143            source: e,
144        })?;
145    } else {
146        fs::write(&path, new_content).map_err(|e| Error::WriteFile {
147            path: path.clone(),
148            source: e,
149        })?;
150    }
151
152    println!(
153        "{} {} hook removed",
154        "Done.".green().bold(),
155        hook_name.cyan()
156    );
157
158    Ok(())
159}