feat: support pre-push hooks (#2737)

Co-authored-by: extrawurst <776816+extrawurst@users.noreply.github.com>
This commit is contained in:
xlai89 2025-10-31 08:57:27 +01:00 committed by GitHub
parent 2374e00302
commit cb17cfe105
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 90 additions and 5 deletions

View file

@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* increase MSRV from 1.81 to 1.82 [[@cruessler](https://github.com/cruessler)]
### Added
* Support pre-push hook [[@xlai89](https://github.com/xlai89)] ([#1933](https://github.com/extrawurst/gitui/issues/1933))
* Message tab supports pageUp and pageDown [[@xlai89](https://github.com/xlai89)] ([#2623](https://github.com/extrawurst/gitui/issues/2623))
* Files and status tab support pageUp and pageDown [[@fatpandac](https://github.com/fatpandac)] ([#1951](https://github.com/extrawurst/gitui/issues/1951))
* support loading custom syntax highlighting themes from a file [[@acuteenvy](https://github.com/acuteenvy)] ([#2565](https://github.com/gitui-org/gitui/pull/2565))

View file

@ -72,6 +72,15 @@ pub fn hooks_prepare_commit_msg(
.into())
}
/// see `git2_hooks::hooks_pre_push`
pub fn hooks_pre_push(repo_path: &RepoPath) -> Result<HookResult> {
scope_time!("hooks_pre_push");
let repo = repo(repo_path)?;
Ok(git2_hooks::hooks_pre_push(&repo, None)?.into())
}
#[cfg(test)]
mod tests {
use std::{ffi::OsString, io::Write as _, path::Path};

View file

@ -67,7 +67,8 @@ pub use diff::get_diff_commit;
pub use git2::BranchType;
pub use hooks::{
hooks_commit_msg, hooks_post_commit, hooks_pre_commit,
hooks_prepare_commit_msg, HookResult, PrepareCommitMsgSource,
hooks_pre_push, hooks_prepare_commit_msg, HookResult,
PrepareCommitMsgSource,
};
pub use hunks::{reset_hunk, stage_hunk, unstage_hunk};
pub use ignore::add_to_ignore;

View file

@ -44,6 +44,7 @@ pub const HOOK_POST_COMMIT: &str = "post-commit";
pub const HOOK_PRE_COMMIT: &str = "pre-commit";
pub const HOOK_COMMIT_MSG: &str = "commit-msg";
pub const HOOK_PREPARE_COMMIT_MSG: &str = "prepare-commit-msg";
pub const HOOK_PRE_PUSH: &str = "pre-push";
const HOOK_COMMIT_MSG_TEMP_FILE: &str = "COMMIT_EDITMSG";
@ -170,6 +171,20 @@ pub fn hooks_post_commit(
hook.run_hook(&[])
}
/// this hook is documented here <https://git-scm.com/docs/githooks#_pre_push>
pub fn hooks_pre_push(
repo: &Repository,
other_paths: Option<&[&str]>,
) -> Result<HookResult> {
let hook = HookPaths::new(repo, other_paths, HOOK_PRE_PUSH)?;
if !hook.found() {
return Ok(HookResult::NoHookFound);
}
hook.run_hook(&[])
}
pub enum PrepareCommitMsgSource {
Message,
Template,
@ -658,4 +673,37 @@ exit 2
)
);
}
#[test]
fn test_pre_push_sh() {
let (_td, repo) = repo_init();
let hook = b"#!/bin/sh
exit 0
";
create_hook(&repo, HOOK_PRE_PUSH, hook);
let res = hooks_pre_push(&repo, None).unwrap();
assert!(matches!(res, HookResult::Ok { .. }));
}
#[test]
fn test_pre_push_fail_sh() {
let (_td, repo) = repo_init();
let hook = b"#!/bin/sh
echo 'failed'
exit 3
";
create_hook(&repo, HOOK_PRE_PUSH, hook);
let res = hooks_pre_push(&repo, None).unwrap();
let HookResult::RunNotSuccessful { code, stdout, .. } = res
else {
unreachable!()
};
assert_eq!(code.unwrap(), 3);
assert_eq!(&stdout, "failed\n");
}
}

View file

@ -16,9 +16,9 @@ use asyncgit::{
extract_username_password_for_push,
need_username_password_for_push, BasicAuthCredential,
},
get_branch_remote,
get_branch_remote, hooks_pre_push,
remotes::get_default_remote_for_push,
RepoPathRef,
HookResult, RepoPathRef,
},
AsyncGitNotification, AsyncPush, PushRequest, PushType,
RemoteProgress, RemoteProgressState,
@ -144,6 +144,19 @@ impl PushPopup {
remote
};
// run pre push hook - can reject push
if let HookResult::NotOk(e) =
hooks_pre_push(&self.repo.borrow())?
{
log::error!("pre-push hook failed: {e}");
self.queue.push(InternalEvent::ShowErrorMsg(format!(
"pre-push hook failed:\n{e}"
)));
self.pending = false;
self.visible = false;
return Ok(());
}
self.pending = true;
self.progress = None;
self.git_push.request(PushRequest {

View file

@ -16,8 +16,8 @@ use asyncgit::{
extract_username_password, need_username_password,
BasicAuthCredential,
},
get_default_remote, AsyncProgress, PushTagsProgress,
RepoPathRef,
get_default_remote, hooks_pre_push, AsyncProgress,
HookResult, PushTagsProgress, RepoPathRef,
},
AsyncGitNotification, AsyncPushTags, PushTagsRequest,
};
@ -84,6 +84,19 @@ impl PushTagsPopup {
&mut self,
cred: Option<BasicAuthCredential>,
) -> Result<()> {
// run pre push hook - can reject push
if let HookResult::NotOk(e) =
hooks_pre_push(&self.repo.borrow())?
{
log::error!("pre-push hook failed: {e}");
self.queue.push(InternalEvent::ShowErrorMsg(format!(
"pre-push hook failed:\n{e}"
)));
self.pending = false;
self.visible = false;
return Ok(());
}
self.pending = true;
self.progress = None;
self.git_push.request(PushTagsRequest {