gnostr_asyncgit/sync/
hooks.rs

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