Skip to content

Commit 19a0cb2

Browse files
author
Mehran Kordi
authored
Implement git hook support on windows
closes #14
1 parent 7761336 commit 19a0cb2

File tree

4 files changed

+81
-54
lines changed

4 files changed

+81
-54
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77
## [Unreleased]
88
### Added
99
- introduced proper changelog
10+
- hook support on windows ([#14](https://github.com/extrawurst/gitui/issues/14))
1011

1112
### Changed
1213
- show longer commit messages in log view

Cargo.lock

Lines changed: 0 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

asyncgit/Cargo.toml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ git2 = { version = "0.13.5", default-features = false }
1515
rayon-core = "1.7"
1616
crossbeam-channel = "0.4"
1717
log = "0.4"
18-
is_executable = "0.1"
1918
scopetime = { path = "../scopetime", version = "0.1" }
20-
tempfile = "3.1"
21-
thiserror = "1.0"
19+
thiserror = "1.0"
20+
21+
[dev-dependencies]
22+
tempfile = "3.1"

asyncgit/src/sync/hooks.rs

Lines changed: 76 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,41 @@
1-
use crate::error::{Error, Result};
2-
use is_executable::IsExecutable;
1+
use crate::error::Result;
32
use scopetime::scope_time;
3+
use std::fs::File;
4+
use std::path::PathBuf;
45
use std::{
56
io::{Read, Write},
67
path::Path,
78
process::Command,
89
};
9-
use tempfile::NamedTempFile;
1010

1111
const HOOK_POST_COMMIT: &str = ".git/hooks/post-commit";
1212
const HOOK_COMMIT_MSG: &str = ".git/hooks/commit-msg";
13+
const HOOK_COMMIT_MSG_TEMP_FILE: &str = ".git/COMMIT_EDITMSG";
1314

14-
///
15+
/// this hook is documented here https://git-scm.com/docs/githooks#_commit_msg
16+
/// we use the same convention as other git clients to create a temp file containing
17+
/// the commit message at `.git/COMMIT_EDITMSG` and pass it's relative path as the only
18+
/// parameter to the hook script.
1519
pub fn hooks_commit_msg(
1620
repo_path: &str,
1721
msg: &mut String,
1822
) -> Result<HookResult> {
1923
scope_time!("hooks_commit_msg");
2024

2125
if hook_runable(repo_path, HOOK_COMMIT_MSG) {
22-
let mut file = NamedTempFile::new()?;
23-
24-
write!(file, "{}", msg)?;
25-
26-
let file_path = file.path().to_str().ok_or_else(|| {
27-
Error::Generic(
28-
"temp file path contains invalid unicode sequences."
29-
.to_string(),
30-
)
31-
})?;
32-
33-
let res = run_hook(repo_path, HOOK_COMMIT_MSG, &[&file_path]);
26+
let temp_file =
27+
Path::new(repo_path).join(HOOK_COMMIT_MSG_TEMP_FILE);
28+
File::create(&temp_file)?.write_all(msg.as_bytes())?;
29+
30+
let res = run_hook(
31+
repo_path,
32+
HOOK_COMMIT_MSG,
33+
&[HOOK_COMMIT_MSG_TEMP_FILE],
34+
);
3435

3536
// load possibly altered msg
36-
let mut file = file.reopen()?;
3737
msg.clear();
38-
file.read_to_string(msg)?;
38+
File::open(temp_file)?.read_to_string(msg)?;
3939

4040
Ok(res)
4141
} else {
@@ -58,7 +58,7 @@ fn hook_runable(path: &str, hook: &str) -> bool {
5858
let path = Path::new(path);
5959
let path = path.join(hook);
6060

61-
path.exists() && path.is_executable()
61+
path.exists() && is_executable(path)
6262
}
6363

6464
///
@@ -70,20 +70,36 @@ pub enum HookResult {
7070
NotOk(String),
7171
}
7272

73-
fn run_hook(path: &str, cmd: &str, args: &[&str]) -> HookResult {
74-
match Command::new(cmd).args(args).current_dir(path).output() {
75-
Ok(output) => {
76-
if output.status.success() {
77-
HookResult::Ok
78-
} else {
79-
let err = String::from_utf8_lossy(&output.stderr);
80-
let out = String::from_utf8_lossy(&output.stdout);
81-
let formatted = format!("{}{}", out, err);
82-
83-
HookResult::NotOk(formatted)
84-
}
85-
}
86-
Err(e) => HookResult::NotOk(format!("{}", e)),
73+
/// this function calls hook scripts based on conventions documented here
74+
/// https://git-scm.com/docs/githooks
75+
fn run_hook(
76+
path: &str,
77+
hook_script: &str,
78+
args: &[&str],
79+
) -> HookResult {
80+
let mut bash_args = vec![hook_script.to_string()];
81+
bash_args.extend_from_slice(
82+
&args
83+
.iter()
84+
.map(|x| (*x).to_string())
85+
.collect::<Vec<String>>(),
86+
);
87+
88+
let output = Command::new("bash")
89+
.args(bash_args)
90+
.current_dir(path)
91+
.output();
92+
93+
let output = output.expect("general hook error");
94+
95+
if output.status.success() {
96+
HookResult::Ok
97+
} else {
98+
let err = String::from_utf8_lossy(&output.stderr);
99+
let out = String::from_utf8_lossy(&output.stdout);
100+
let formatted = format!("{}{}", out, err);
101+
102+
HookResult::NotOk(formatted)
87103
}
88104
}
89105

@@ -115,15 +131,17 @@ mod tests {
115131
.write_all(hook_script)
116132
.unwrap();
117133

118-
Command::new("chmod")
119-
.args(&["+x", hook_path])
120-
.current_dir(path)
121-
.output()
122-
.unwrap();
134+
#[cfg(not(windows))]
135+
{
136+
Command::new("chmod")
137+
.args(&["+x", hook_path])
138+
.current_dir(path)
139+
.output()
140+
.unwrap();
141+
}
123142
}
124143

125144
#[test]
126-
#[cfg(not(windows))]
127145
fn test_hooks_commit_msg_ok() {
128146
let (_td, repo) = repo_init().unwrap();
129147
let root = repo.path().parent().unwrap();
@@ -145,7 +163,6 @@ exit 0
145163
}
146164

147165
#[test]
148-
#[cfg(not(windows))]
149166
fn test_hooks_commit_msg() {
150167
let (_td, repo) = repo_init().unwrap();
151168
let root = repo.path().parent().unwrap();
@@ -172,7 +189,6 @@ exit 1
172189
}
173190

174191
#[test]
175-
#[cfg(not(windows))]
176192
fn test_commit_msg_no_block_but_alter() {
177193
let (_td, repo) = repo_init().unwrap();
178194
let root = repo.path().parent().unwrap();
@@ -193,3 +209,22 @@ exit 0
193209
assert_eq!(msg, String::from("msg\n"));
194210
}
195211
}
212+
213+
#[cfg(not(windows))]
214+
fn is_executable(path: PathBuf) -> bool {
215+
use std::os::unix::fs::PermissionsExt;
216+
let metadata = match path.metadata() {
217+
Ok(metadata) => metadata,
218+
Err(_) => return false,
219+
};
220+
221+
let permissions = metadata.permissions();
222+
permissions.mode() & 0o111 != 0
223+
}
224+
225+
#[cfg(windows)]
226+
/// windows does not consider bash scripts to be executable so we consider everything
227+
/// to be executable (which is not far from the truth for windows platform.)
228+
fn is_executable(_: PathBuf) -> bool {
229+
true
230+
}

0 commit comments

Comments
 (0)