-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathmod.rs
More file actions
179 lines (158 loc) · 4.67 KB
/
Copy pathmod.rs
File metadata and controls
179 lines (158 loc) · 4.67 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
use anyhow::{Context, Result};
use clap::Subcommand;
use std::{fs, path::Path};
use crate::utils::{confirm, find_repo_root};
mod builtins;
#[derive(Subcommand)]
pub enum HooksCommand {
/// Install a hook (built-in or custom command)
Init {
/// Git hook name (e.g. commit-msg, pre-push, pre-commit)
hook: String,
/// Built-in name or shell command to run
target: String,
#[arg(short, long)]
yes: bool,
#[arg(short, long)]
force: bool,
#[arg(long)]
dry_run: bool,
},
/// List installed hooks
List,
/// Remove a hook
Remove {
hook: String,
#[arg(short, long)]
yes: bool,
#[arg(long)]
dry_run: bool,
},
/// Show hook content
Show { hook: String },
}
pub fn run(cmd: HooksCommand) -> Result<()> {
match cmd {
HooksCommand::Init {
hook,
target,
yes,
force,
dry_run,
} => init(&hook, &target, yes, force, dry_run),
HooksCommand::List => list(),
HooksCommand::Remove { hook, yes, dry_run } => remove(&hook, yes, dry_run),
HooksCommand::Show { hook } => show(&hook),
}
}
fn hooks_dir() -> Result<std::path::PathBuf> {
Ok(find_repo_root()?.join(".git").join("hooks"))
}
fn hook_script(target: &str) -> String {
if let Some(script) = builtins::get(target) {
return script.to_owned();
}
format!("#!/bin/sh\nset -e\n{target}\n")
}
const VALID_HOOKS: &[&str] = &[
"applypatch-msg",
"commit-msg",
"fsmonitor-watchman",
"post-update",
"pre-applypatch",
"pre-commit",
"pre-merge-commit",
"pre-push",
"pre-rebase",
"pre-receive",
"prepare-commit-msg",
"push-to-checkout",
"update",
];
fn init(hook: &str, target: &str, yes: bool, force: bool, dry_run: bool) -> Result<()> {
if !VALID_HOOKS.contains(&hook) {
anyhow::bail!(
"'{hook}' is not a valid git hook. Valid hooks: {}",
VALID_HOOKS.join(", ")
);
}
let dir = hooks_dir()?;
let path = dir.join(hook);
if path.exists() && !force {
if !confirm(&format!("Hook '{hook}' already exists. Overwrite?"), yes) {
println!("Aborted.");
return Ok(());
}
if !dry_run {
let backup = dir.join(format!("{hook}.bak"));
fs::copy(&path, &backup).with_context(|| format!("Failed to backup {hook}"))?;
println!("Backed up to {}", backup.display());
}
}
let script = hook_script(target);
if dry_run {
println!("[dry-run] Would write hook '{hook}':\n{script}");
return Ok(());
}
fs::create_dir_all(&dir).context("Failed to create hooks directory")?;
fs::write(&path, &script).with_context(|| format!("Failed to write hook '{hook}'"))?;
set_executable(&path)?;
println!("Installed hook '{hook}'.");
Ok(())
}
fn list() -> Result<()> {
let dir = hooks_dir()?;
let hooks: Vec<_> = fs::read_dir(&dir)
.context("Failed to read hooks directory")?
.filter_map(|e| e.ok())
.filter(|e| {
let name = e.file_name();
let s = name.to_string_lossy();
!s.ends_with(".bak") && !s.ends_with(".sample")
})
.map(|e| e.file_name().to_string_lossy().to_string())
.collect();
if hooks.is_empty() {
println!("No hooks installed.");
} else {
for h in hooks {
println!("{h}");
}
}
Ok(())
}
fn remove(hook: &str, yes: bool, dry_run: bool) -> Result<()> {
let path = hooks_dir()?.join(hook);
anyhow::ensure!(path.exists(), "Hook '{hook}' is not installed");
if !confirm(&format!("Remove hook '{hook}'?"), yes) {
println!("Aborted.");
return Ok(());
}
if dry_run {
println!("[dry-run] Would remove hook '{hook}'.");
return Ok(());
}
fs::remove_file(&path).with_context(|| format!("Failed to remove hook '{hook}'"))?;
println!("Removed hook '{hook}'.");
Ok(())
}
fn show(hook: &str) -> Result<()> {
let path = hooks_dir()?.join(hook);
anyhow::ensure!(path.exists(), "Hook '{hook}' is not installed");
let content =
fs::read_to_string(&path).with_context(|| format!("Failed to read hook '{hook}'"))?;
print!("{content}");
Ok(())
}
#[cfg(unix)]
fn set_executable(path: &Path) -> Result<()> {
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(path)?.permissions();
perms.set_mode(0o755);
fs::set_permissions(path, perms).context("Failed to set executable permission")?;
Ok(())
}
#[cfg(not(unix))]
fn set_executable(_path: &Path) -> Result<()> {
Ok(())
}