From a377ee9e7430a4af6c33e88932b0c119aa0b3e46 Mon Sep 17 00:00:00 2001 From: xlai89 Date: Fri, 10 Oct 2025 16:46:15 +0200 Subject: [PATCH] feat: support pre-push hooks --- CHANGELOG.md | 1 + asyncgit/src/sync/hooks.rs | 9 +++++++ asyncgit/src/sync/mod.rs | 3 ++- git2-hooks/src/lib.rs | 48 ++++++++++++++++++++++++++++++++++++++ src/popups/push.rs | 17 ++++++++++++-- src/popups/push_tags.rs | 17 ++++++++++++-- 6 files changed, 90 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e27a1c447f..b388b07559 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * execute git-hooks directly if possible (on *nix) else use sh instead of bash (without reading SHELL variable) [[@Joshix](https://github.com/Joshix-1)] ([#2483](https://github.com/extrawurst/gitui/pull/2483)) ### Added +* Support pre-push hook [[@xlai89](https://github.com/xlai89)] ([#1933](https://github.com/extrawurst/gitui/issues/1933)) * 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)) * Select syntax highlighting theme out of the defaults from syntect [[@vasilismanol](https://github.com/vasilismanol)] ([#1931](https://github.com/extrawurst/gitui/issues/1931)) diff --git a/asyncgit/src/sync/hooks.rs b/asyncgit/src/sync/hooks.rs index 111b426259..fe1a91912d 100644 --- a/asyncgit/src/sync/hooks.rs +++ b/asyncgit/src/sync/hooks.rs @@ -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 { + 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}; diff --git a/asyncgit/src/sync/mod.rs b/asyncgit/src/sync/mod.rs index 54ec5f1e03..c5c7901cc2 100644 --- a/asyncgit/src/sync/mod.rs +++ b/asyncgit/src/sync/mod.rs @@ -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; diff --git a/git2-hooks/src/lib.rs b/git2-hooks/src/lib.rs index 4c949a1e46..acabae1389 100644 --- a/git2-hooks/src/lib.rs +++ b/git2-hooks/src/lib.rs @@ -43,6 +43,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"; @@ -169,6 +170,20 @@ pub fn hooks_post_commit( hook.run_hook(&[]) } +/// this hook is documented here +pub fn hooks_pre_push( + repo: &Repository, + other_paths: Option<&[&str]>, +) -> Result { + 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, @@ -657,4 +672,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"); + } } diff --git a/src/popups/push.rs b/src/popups/push.rs index b7dff07123..f900ce8613 100644 --- a/src/popups/push.rs +++ b/src/popups/push.rs @@ -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 { diff --git a/src/popups/push_tags.rs b/src/popups/push_tags.rs index 420df0b860..30df245b16 100644 --- a/src/popups/push_tags.rs +++ b/src/popups/push_tags.rs @@ -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, ) -> 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 {