gnostr_asyncgit/sync/
hooks.rs

1pub use git2_hooks::PrepareCommitMsgSource;
2use scopetime::scope_time;
3
4use super::{repository::repo, RepoPath};
5use crate::error::Result;
6//use crate::sync::utils;
7
8///
9#[derive(Debug, PartialEq, Eq)]
10pub enum HookResult {
11    /// Everything went fine
12    Ok,
13    /// Hook returned error
14    NotOk(String),
15}
16
17impl From<git2_hooks::HookResult> for HookResult {
18    fn from(v: git2_hooks::HookResult) -> Self {
19        match v {
20            git2_hooks::HookResult::Ok { .. } | git2_hooks::HookResult::NoHookFound => Self::Ok,
21            git2_hooks::HookResult::RunNotSuccessful { stdout, stderr, .. } => {
22                Self::NotOk(format!("{stdout}{stderr}"))
23            }
24        }
25    }
26}
27
28/// this hook is documented here <https://git-scm.com/docs/githooks#_commit_msg>
29/// we use the same convention as other git clients to create a temp
30/// file containing the commit message at
31/// `<.git|hooksPath>/COMMIT_EDITMSG` and pass it's relative path as
32/// the only parameter to the hook script.
33pub fn hooks_commit_msg(repo_path: &RepoPath, msg: &mut String) -> Result<HookResult> {
34    scope_time!("hooks_commit_msg");
35
36    let repo = repo(repo_path)?;
37
38    Ok(git2_hooks::hooks_commit_msg(&repo, None, msg)?.into())
39}
40
41/// this hook is documented here <https://git-scm.com/docs/githooks#_pre_commit>
42pub fn hooks_pre_commit(repo_path: &RepoPath) -> Result<HookResult> {
43    scope_time!("hooks_pre_commit");
44
45    let repo = repo(repo_path)?;
46
47    Ok(git2_hooks::hooks_pre_commit(&repo, None)?.into())
48}
49
50///
51pub fn hooks_post_commit(repo_path: &RepoPath) -> Result<HookResult> {
52    scope_time!("hooks_post_commit");
53
54    let repo = repo(repo_path)?;
55
56    Ok(git2_hooks::hooks_post_commit(&repo, None)?.into())
57}
58
59///
60pub fn hooks_prepare_commit_msg(
61    repo_path: &RepoPath,
62    source: PrepareCommitMsgSource,
63    msg: &mut String,
64) -> Result<HookResult> {
65    scope_time!("hooks_prepare_commit_msg");
66
67    let repo = repo(repo_path)?;
68
69    Ok(git2_hooks::hooks_prepare_commit_msg(&repo, None, source, msg)?.into())
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75    use crate::sync::tests::repo_init;
76
77    #[test]
78    fn test_post_commit_hook_reject_in_subfolder() {
79        let (_td, repo) = repo_init().unwrap();
80        let root = repo.path().parent().unwrap();
81
82        let hook = b"#!/bin/sh
83	echo 'rejected'
84	exit 1
85	        ";
86
87        git2_hooks::create_hook(&repo, git2_hooks::HOOK_POST_COMMIT, hook);
88
89        let subfolder = root.join("foo/");
90        std::fs::create_dir_all(&subfolder).unwrap();
91
92        let res = hooks_post_commit(&subfolder.to_str().unwrap().into()).unwrap();
93
94        assert_ne!(res, HookResult::NotOk(String::from("rejected\n")));
95    }
96
97    // make sure we run the hooks with the correct pwd.
98    // for non-bare repos this is the dir of the worktree
99    // unfortunately does not work on windows
100    #[test]
101    #[cfg(unix)]
102    fn test_pre_commit_workdir() {
103        let (_td, repo) = repo_init().unwrap();
104        let root = repo.path().parent().unwrap();
105        let repo_path: &RepoPath = &root.as_os_str().to_str().unwrap().into();
106        let workdir = crate::sync::utils::repo_work_dir(repo_path).unwrap();
107
108        let hook = b"#!/bin/sh
109	echo $(pwd)
110	exit 1
111	        ";
112
113        git2_hooks::create_hook(&repo, git2_hooks::HOOK_PRE_COMMIT, hook);
114        let res = hooks_pre_commit(repo_path).unwrap();
115        if let HookResult::NotOk(res) = res {
116            assert_ne!(
117                std::path::Path::new(res.trim_end()),
118                std::path::Path::new(&workdir)
119            );
120        } else {
121            assert!(false);
122        }
123    }
124
125    #[test]
126    fn test_hooks_commit_msg_reject_in_subfolder() {
127        let (_td, repo) = repo_init().unwrap();
128        let root = repo.path().parent().unwrap();
129
130        let hook = b"#!/bin/sh
131	echo 'msg' > $1
132	echo 'rejected'
133	exit 1
134	        ";
135
136        git2_hooks::create_hook(&repo, git2_hooks::HOOK_COMMIT_MSG, hook);
137
138        let subfolder = root.join("foo/");
139        std::fs::create_dir_all(&subfolder).unwrap();
140
141        let mut msg = String::from("test");
142        let res = hooks_commit_msg(&subfolder.to_str().unwrap().into(), &mut msg).unwrap();
143
144        assert_ne!(res, HookResult::NotOk(String::from("rejected\n")));
145
146        assert_eq!(msg, String::from("msg\n"));
147    }
148}