githops_core/
sync_hooks.rs1use anyhow::Result;
2use colored::Colorize;
3use std::path::Path;
4
5use crate::config::Config;
6use crate::hooks::ALL_HOOKS;
7
8pub const GITHOPS_MARKER: &str = "# GITHOPS_MANAGED";
10
11pub fn sync_to_hooks(config: &Config, hooks_dir: &Path, force: bool) -> Result<(usize, usize)> {
18 std::fs::create_dir_all(hooks_dir)?;
19
20 let mut installed = 0usize;
21 let mut skipped = 0usize;
22
23 for hook_info in ALL_HOOKS {
24 let hook_cfg = match config.hooks.get(hook_info.name) {
25 Some(cfg) => cfg,
26 None => continue,
27 };
28
29 let resolved = hook_cfg.resolved_commands(&config.definitions);
30 let active_count = resolved.iter().filter(|c| !c.test).count();
31
32 if !hook_cfg.enabled || active_count == 0 {
33 continue;
34 }
35
36 let hook_path = hooks_dir.join(hook_info.name);
37 let script = build_hook_script(hook_info.name, active_count);
38
39 if hook_path.exists() {
40 let existing = std::fs::read_to_string(&hook_path).unwrap_or_default();
41 if !existing.contains(GITHOPS_MARKER) {
42 if !force {
43 println!(
44 "{} {} — not managed by githops, skipping (use {} to overwrite)",
45 "skip:".yellow().bold(),
46 hook_info.name,
47 "githops sync --force".cyan()
48 );
49 skipped += 1;
50 continue;
51 }
52 println!(
53 "{} {} — overwriting unmanaged hook",
54 "force:".yellow().bold(),
55 hook_info.name
56 );
57 }
58 }
59
60 std::fs::write(&hook_path, &script)?;
61 make_executable(&hook_path)?;
62 println!("{} {}", "synced:".green().bold(), hook_info.name);
63 installed += 1;
64 }
65
66 for hook_info in ALL_HOOKS {
68 let hook_path = hooks_dir.join(hook_info.name);
69 if !hook_path.exists() {
70 continue;
71 }
72 let existing = std::fs::read_to_string(&hook_path).unwrap_or_default();
73 if !existing.contains(GITHOPS_MARKER) {
74 continue;
75 }
76 let configured = config
77 .hooks
78 .get(hook_info.name)
79 .map(|c| {
80 let resolved = c.resolved_commands(&config.definitions);
81 c.enabled && resolved.iter().any(|cmd| !cmd.test)
82 })
83 .unwrap_or(false);
84 if !configured {
85 std::fs::remove_file(&hook_path)?;
86 println!("{} {}", "removed:".dimmed(), hook_info.name);
87 }
88 }
89
90 Ok((installed, skipped))
91}
92
93fn build_hook_script(hook_name: &str, command_count: usize) -> String {
94 format!(
95 r#"#!/bin/sh
96{marker}
97# Hook: {name}
98# Managed by githops — do not edit manually.
99# Run `githops sync` to regenerate.
100# Commands configured: {count}
101
102exec githops check {name} "$@"
103"#,
104 marker = GITHOPS_MARKER,
105 name = hook_name,
106 count = command_count
107 )
108}
109
110#[cfg(unix)]
111fn make_executable(path: &Path) -> Result<()> {
112 use std::os::unix::fs::PermissionsExt;
113 let mut perms = std::fs::metadata(path)?.permissions();
114 perms.set_mode(perms.mode() | 0o111);
115 std::fs::set_permissions(path, perms)?;
116 Ok(())
117}
118
119#[cfg(not(unix))]
120fn make_executable(_path: &Path) -> Result<()> {
121 Ok(())
122}