1
- use crate :: error:: { Error , Result } ;
2
- use is_executable:: IsExecutable ;
1
+ use crate :: error:: Result ;
3
2
use scopetime:: scope_time;
3
+ use std:: fs:: File ;
4
+ use std:: path:: PathBuf ;
4
5
use std:: {
5
6
io:: { Read , Write } ,
6
7
path:: Path ,
7
8
process:: Command ,
8
9
} ;
9
- use tempfile:: NamedTempFile ;
10
10
11
11
const HOOK_POST_COMMIT : & str = ".git/hooks/post-commit" ;
12
12
const HOOK_COMMIT_MSG : & str = ".git/hooks/commit-msg" ;
13
+ const HOOK_COMMIT_MSG_TEMP_FILE : & str = ".git/COMMIT_EDITMSG" ;
13
14
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.
15
19
pub fn hooks_commit_msg (
16
20
repo_path : & str ,
17
21
msg : & mut String ,
18
22
) -> Result < HookResult > {
19
23
scope_time ! ( "hooks_commit_msg" ) ;
20
24
21
25
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
+ ) ;
34
35
35
36
// load possibly altered msg
36
- let mut file = file. reopen ( ) ?;
37
37
msg. clear ( ) ;
38
- file . read_to_string ( msg) ?;
38
+ File :: open ( temp_file ) ? . read_to_string ( msg) ?;
39
39
40
40
Ok ( res)
41
41
} else {
@@ -58,7 +58,7 @@ fn hook_runable(path: &str, hook: &str) -> bool {
58
58
let path = Path :: new ( path) ;
59
59
let path = path. join ( hook) ;
60
60
61
- path. exists ( ) && path . is_executable ( )
61
+ path. exists ( ) && is_executable ( path )
62
62
}
63
63
64
64
///
@@ -70,20 +70,36 @@ pub enum HookResult {
70
70
NotOk ( String ) ,
71
71
}
72
72
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)
87
103
}
88
104
}
89
105
@@ -115,15 +131,17 @@ mod tests {
115
131
. write_all ( hook_script)
116
132
. unwrap ( ) ;
117
133
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
+ }
123
142
}
124
143
125
144
#[ test]
126
- #[ cfg( not( windows) ) ]
127
145
fn test_hooks_commit_msg_ok ( ) {
128
146
let ( _td, repo) = repo_init ( ) . unwrap ( ) ;
129
147
let root = repo. path ( ) . parent ( ) . unwrap ( ) ;
@@ -145,7 +163,6 @@ exit 0
145
163
}
146
164
147
165
#[ test]
148
- #[ cfg( not( windows) ) ]
149
166
fn test_hooks_commit_msg ( ) {
150
167
let ( _td, repo) = repo_init ( ) . unwrap ( ) ;
151
168
let root = repo. path ( ) . parent ( ) . unwrap ( ) ;
@@ -172,7 +189,6 @@ exit 1
172
189
}
173
190
174
191
#[ test]
175
- #[ cfg( not( windows) ) ]
176
192
fn test_commit_msg_no_block_but_alter ( ) {
177
193
let ( _td, repo) = repo_init ( ) . unwrap ( ) ;
178
194
let root = repo. path ( ) . parent ( ) . unwrap ( ) ;
@@ -193,3 +209,22 @@ exit 0
193
209
assert_eq ! ( msg, String :: from( "msg\n " ) ) ;
194
210
}
195
211
}
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