1use anyhow::Result;
4use std::path::Path;
5
6#[cfg(unix)]
7use std::os::unix::fs::PermissionsExt;
8
9const PRE_COMMIT_HOOK: &str = r#"#!/bin/sh
10# git-valet: sync valet repo before commit
11if command -v git-valet >/dev/null 2>&1; then
12 git-valet sync --message "chore: auto-sync before commit" 2>/dev/null || true
13fi
14"#;
15
16const PRE_PUSH_HOOK: &str = r#"#!/bin/sh
17# git-valet: sync valet repo before push
18if command -v git-valet >/dev/null 2>&1; then
19 git-valet sync --message "chore: auto-sync before push" 2>/dev/null || true
20fi
21"#;
22
23const POST_MERGE_HOOK: &str = r"#!/bin/sh
24# git-valet: pull valet repo after merge
25if command -v git-valet >/dev/null 2>&1; then
26 git-valet pull 2>/dev/null || true
27fi
28";
29
30const POST_CHECKOUT_HOOK: &str = r"#!/bin/sh
31# git-valet: pull valet repo after checkout
32if command -v git-valet >/dev/null 2>&1; then
33 git-valet pull 2>/dev/null || true
34fi
35";
36
37pub struct HookInstall {
38 pub name: &'static str,
39 pub content: &'static str,
40}
41
42const HOOKS: &[HookInstall] = &[
43 HookInstall { name: "pre-commit", content: PRE_COMMIT_HOOK },
44 HookInstall { name: "pre-push", content: PRE_PUSH_HOOK },
45 HookInstall { name: "post-merge", content: POST_MERGE_HOOK },
46 HookInstall { name: "post-checkout", content: POST_CHECKOUT_HOOK },
47];
48
49const SHADOW_MARKER: &str = "# git-valet:";
50
51pub fn install(git_dir: &Path) -> Result<()> {
53 let hooks_dir = git_dir.join("hooks");
54 std::fs::create_dir_all(&hooks_dir)?;
55
56 for hook in HOOKS {
57 let hook_path = hooks_dir.join(hook.name);
58
59 if hook_path.exists() {
60 let existing = std::fs::read_to_string(&hook_path)?;
61 if existing.contains(SHADOW_MARKER) {
62 continue; }
64 let stripped =
66 hook.content.trim_start_matches("#!/bin/sh\r\n").trim_start_matches("#!/bin/sh\n");
67 let combined = format!("{}\n{stripped}", existing.trim_end());
68 std::fs::write(&hook_path, combined)?;
69 } else {
70 std::fs::write(&hook_path, hook.content)?;
71 }
72
73 #[cfg(unix)]
75 {
76 let mut perms = std::fs::metadata(&hook_path)?.permissions();
77 perms.set_mode(0o755);
78 std::fs::set_permissions(&hook_path, perms)?;
79 }
80 }
81
82 Ok(())
83}
84
85pub fn uninstall(git_dir: &Path) -> Result<()> {
87 let hooks_dir = git_dir.join("hooks");
88
89 for hook in HOOKS {
90 let hook_path = hooks_dir.join(hook.name);
91 if !hook_path.exists() {
92 continue;
93 }
94
95 let content = std::fs::read_to_string(&hook_path)?;
96
97 if !content.contains(SHADOW_MARKER) {
98 continue;
99 }
100
101 let mut filtered = Vec::new();
103 let mut in_valet_block = false;
104 for line in content.lines() {
105 if line.contains(SHADOW_MARKER) {
106 in_valet_block = true;
107 continue;
108 }
109 if in_valet_block {
110 if line.trim() == "fi" {
111 in_valet_block = false;
112 }
113 continue;
114 }
115 filtered.push(line);
116 }
117 let filtered = filtered.join("\n");
118
119 let trimmed = filtered.trim();
120
121 if trimmed.is_empty() || trimmed == "#!/bin/sh" {
122 std::fs::remove_file(&hook_path)?;
124 } else {
125 std::fs::write(&hook_path, format!("{trimmed}\n"))?;
126 }
127 }
128
129 Ok(())
130}