git_side/commands/
hook.rs1use 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
17fn 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
23fn 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
38pub 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 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 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 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 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
100pub 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 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 let trimmed = new_content.trim();
139 if trimmed.is_empty() || trimmed == "#!/bin/sh" || trimmed == "#!/bin/bash" {
140 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}