forked from gitui-org/gitui
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathhookspath.rs
235 lines (201 loc) · 5.78 KB
/
hookspath.rs
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
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
use git2::Repository;
use crate::{error::Result, HookResult, HooksError};
use std::{
env,
path::{Path, PathBuf},
process::Command,
str::FromStr,
};
pub struct HookPaths {
pub git: PathBuf,
pub hook: PathBuf,
pub pwd: PathBuf,
}
const CONFIG_HOOKS_PATH: &str = "core.hooksPath";
const DEFAULT_HOOKS_PATH: &str = "hooks";
impl HookPaths {
/// `core.hooksPath` always takes precedence.
/// If its defined and there is no hook `hook` this is not considered
/// an error or a reason to search in other paths.
/// If the config is not set we go into search mode and
/// first check standard `.git/hooks` folder and any sub path provided in `other_paths`.
///
/// Note: we try to model as closely as possible what git shell is doing.
pub fn new(
repo: &Repository,
other_paths: Option<&[&str]>,
hook: &str,
) -> Result<Self> {
let pwd = repo
.workdir()
.unwrap_or_else(|| repo.path())
.to_path_buf();
let git_dir = repo.path().to_path_buf();
if let Some(config_path) = Self::config_hook_path(repo)? {
let hooks_path = PathBuf::from(config_path);
let hook = hooks_path.join(hook);
let hook = shellexpand::full(
hook.as_os_str()
.to_str()
.ok_or(HooksError::PathToString)?,
)?;
let hook = PathBuf::from_str(hook.as_ref())
.map_err(|_| HooksError::PathToString)?;
return Ok(Self {
git: git_dir,
hook,
pwd,
});
}
Ok(Self {
git: git_dir,
hook: Self::find_hook(repo, other_paths, hook),
pwd,
})
}
fn config_hook_path(repo: &Repository) -> Result<Option<String>> {
Ok(repo.config()?.get_string(CONFIG_HOOKS_PATH).ok())
}
/// check default hook path first and then followed by `other_paths`.
/// if no hook is found we return the default hook path
fn find_hook(
repo: &Repository,
other_paths: Option<&[&str]>,
hook: &str,
) -> PathBuf {
let mut paths = vec![DEFAULT_HOOKS_PATH.to_string()];
if let Some(others) = other_paths {
paths.extend(
others
.iter()
.map(|p| p.trim_end_matches('/').to_string()),
);
}
for p in paths {
let p = repo.path().to_path_buf().join(p).join(hook);
if p.exists() {
return p;
}
}
repo.path()
.to_path_buf()
.join(DEFAULT_HOOKS_PATH)
.join(hook)
}
/// was a hook file found and is it executable
pub fn found(&self) -> bool {
self.hook.exists() && is_executable(&self.hook)
}
/// this function calls hook scripts based on conventions documented here
/// see <https://git-scm.com/docs/githooks>
pub fn run_hook(&self, args: &[&str]) -> Result<HookResult> {
let hook = self.hook.clone();
#[allow(clippy::unnecessary_debug_formatting)]
let arg_str = format!("{:?} {}", hook, args.join(" "));
// Use -l to avoid "command not found" on Windows.
let bash_args =
vec!["-l".to_string(), "-c".to_string(), arg_str];
log::trace!("run hook '{:?}' in '{:?}'", hook, self.pwd);
let git_shell = find_bash_executable()
.or_else(find_default_unix_shell)
.unwrap_or_else(|| "bash".into());
let output = Command::new(git_shell)
.args(bash_args)
.with_no_window()
.current_dir(&self.pwd)
// This call forces Command to handle the Path environment correctly on windows,
// the specific env set here does not matter
// see https://github.com/rust-lang/rust/issues/37519
.env(
"DUMMY_ENV_TO_FIX_WINDOWS_CMD_RUNS",
"FixPathHandlingOnWindows",
)
.output()?;
if output.status.success() {
Ok(HookResult::Ok { hook })
} else {
let stderr =
String::from_utf8_lossy(&output.stderr).to_string();
let stdout =
String::from_utf8_lossy(&output.stdout).to_string();
Ok(HookResult::RunNotSuccessful {
code: output.status.code(),
stdout,
stderr,
hook,
})
}
}
}
#[cfg(unix)]
fn is_executable(path: &Path) -> bool {
use std::os::unix::fs::PermissionsExt;
let metadata = match path.metadata() {
Ok(metadata) => metadata,
Err(e) => {
log::error!("metadata error: {}", e);
return false;
}
};
let permissions = metadata.permissions();
permissions.mode() & 0o111 != 0
}
#[cfg(windows)]
/// windows does not consider bash scripts to be executable so we consider everything
/// to be executable (which is not far from the truth for windows platform.)
const fn is_executable(_: &Path) -> bool {
true
}
// Find bash.exe, and avoid finding wsl's bash.exe on Windows.
// None for non-Windows.
fn find_bash_executable() -> Option<PathBuf> {
if cfg!(windows) {
Command::new("where.exe")
.arg("git")
.output()
.ok()
.map(|out| {
PathBuf::from(Into::<String>::into(
String::from_utf8_lossy(&out.stdout),
))
})
.as_deref()
.and_then(Path::parent)
.and_then(Path::parent)
.map(|p| p.join("usr/bin/bash.exe"))
.filter(|p| p.exists())
} else {
None
}
}
// Find default shell on Unix-like OS.
fn find_default_unix_shell() -> Option<PathBuf> {
env::var_os("SHELL").map(PathBuf::from)
}
trait CommandExt {
/// The process is a console application that is being run without a
/// console window. Therefore, the console handle for the application is
/// not set.
///
/// This flag is ignored if the application is not a console application,
/// or if it used with either `CREATE_NEW_CONSOLE` or `DETACHED_PROCESS`.
///
/// See: <https://learn.microsoft.com/en-us/windows/win32/procthread/process-creation-flags>
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
fn with_no_window(&mut self) -> &mut Self;
}
impl CommandExt for Command {
/// On Windows, CLI applications that aren't the window's subsystem will
/// create and show a console window that pops up next to the main
/// application window when run. We disable this behavior by setting the
/// `CREATE_NO_WINDOW` flag.
#[inline]
fn with_no_window(&mut self) -> &mut Self {
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
self.creation_flags(Self::CREATE_NO_WINDOW);
}
self
}
}