1- use crate :: error:: { Error , Result } ;
2- use is_executable:: IsExecutable ;
1+ use crate :: error:: Result ;
32use scopetime:: scope_time;
3+ use std:: fs:: File ;
4+ use std:: path:: PathBuf ;
45use std:: {
56 io:: { Read , Write } ,
67 path:: Path ,
78 process:: Command ,
89} ;
9- use tempfile:: NamedTempFile ;
1010
1111const HOOK_POST_COMMIT : & str = ".git/hooks/post-commit" ;
1212const 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.
1519pub 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