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