summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorextrawurst <776816+extrawurst@users.noreply.github.com>2023-12-16 00:07:54 +0100
committerGitHub <noreply@github.com>2023-12-16 00:07:54 +0100
commite7c61ffc89f987fa3f6a1dbb6e7c6f6a83a0b321 (patch)
treef3a86f9022cc823c1f38b9747782ab170f55d14e
parent7b7c5c41315c0c6e061bfd62374c54749984b206 (diff)
Support prepare commit hook (#1978)
-rw-r--r--CHANGELOG.md1
-rw-r--r--Cargo.lock2
-rw-r--r--README.md2
-rw-r--r--asyncgit/src/sync/hooks.rs17
-rw-r--r--asyncgit/src/sync/mod.rs3
-rw-r--r--git2-hooks/Cargo.toml2
-rw-r--r--git2-hooks/src/lib.rs121
-rw-r--r--src/components/commit.rs36
8 files changed, 171 insertions, 13 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 61e2589b..af52455f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
* `theme.ron` now supports customizing line break symbol ([#1894](https://github.com/extrawurst/gitui/issues/1894))
* add confirmation for dialog for undo commit [[@TeFiLeDo](https://github.com/TeFiLeDo)] ([#1912](https://github.com/extrawurst/gitui/issues/1912))
+* support `prepare-commit-msg` hook ([#1873](https://github.com/extrawurst/gitui/issues/1873))
### Changed
* do not allow tag when `tag.gpgsign` enabled [[@TeFiLeDo](https://github.com/TeFiLeDo)] ([#1915](https://github.com/extrawurst/gitui/pull/1915))
diff --git a/Cargo.lock b/Cargo.lock
index 1792def0..30cdfc0f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -714,7 +714,7 @@ dependencies = [
[[package]]
name = "git2-hooks"
-version = "0.3.0"
+version = "0.3.1"
dependencies = [
"git2",
"git2-testing",
diff --git a/README.md b/README.md
index 0ae563ed..3b4b9c9e 100644
--- a/README.md
+++ b/README.md
@@ -43,7 +43,7 @@
- Fast and intuitive **keyboard only** control
- Context based help (**no need to memorize** tons of hot-keys)
-- Inspect, commit, and amend changes (incl. hooks: *pre-commit*,*commit-msg*,*post-commit*)
+- Inspect, commit, and amend changes (incl. hooks: *pre-commit*,*commit-msg*,*post-commit*,*prepare-commit-msg*)
- Stage, unstage, revert and reset files, hunks and lines
- Stashing (save, pop, apply, drop, and inspect)
- Push / Fetch to / from remote
diff --git a/asyncgit/src/sync/hooks.rs b/asyncgit/src/sync/hooks.rs
index f480da07..b6928918 100644
--- a/asyncgit/src/sync/hooks.rs
+++ b/asyncgit/src/sync/hooks.rs
@@ -1,5 +1,6 @@
use super::{repository::repo, RepoPath};
use crate::error::Result;
+pub use git2_hooks::PrepareCommitMsgSource;
use scopetime::scope_time;
///
@@ -59,6 +60,22 @@ pub fn hooks_post_commit(repo_path: &RepoPath) -> Result<HookResult> {
Ok(git2_hooks::hooks_post_commit(&repo, None)?.into())
}
+///
+pub fn hooks_prepare_commit_msg(
+ repo_path: &RepoPath,
+ source: PrepareCommitMsgSource,
+ msg: &mut String,
+) -> Result<HookResult> {
+ scope_time!("hooks_prepare_commit_msg");
+
+ let repo = repo(repo_path)?;
+
+ Ok(git2_hooks::hooks_prepare_commit_msg(
+ &repo, None, source, msg,
+ )?
+ .into())
+}
+
#[cfg(test)]
mod tests {
use super::*;
diff --git a/asyncgit/src/sync/mod.rs b/asyncgit/src/sync/mod.rs
index 909bea6f..83683a9e 100644
--- a/asyncgit/src/sync/mod.rs
+++ b/asyncgit/src/sync/mod.rs
@@ -65,7 +65,8 @@ pub use config::{
pub use diff::get_diff_commit;
pub use git2::BranchType;
pub use hooks::{
- hooks_commit_msg, hooks_post_commit, hooks_pre_commit, HookResult,
+ hooks_commit_msg, hooks_post_commit, hooks_pre_commit,
+ 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/Cargo.toml b/git2-hooks/Cargo.toml
index c85f4cc9..1c6c63e5 100644
--- a/git2-hooks/Cargo.toml
+++ b/git2-hooks/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "git2-hooks"
-version = "0.3.0"
+version = "0.3.1"
authors = ["extrawurst <mail@rusticorn.com>"]
edition = "2021"
description = "adds git hooks support based on git2-rs"
diff --git a/git2-hooks/src/lib.rs b/git2-hooks/src/lib.rs
index 0cf5ac8d..ea7842cc 100644
--- a/git2-hooks/src/lib.rs
+++ b/git2-hooks/src/lib.rs
@@ -27,6 +27,7 @@ use git2::Repository;
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";
const HOOK_COMMIT_MSG_TEMP_FILE: &str = "COMMIT_EDITMSG";
@@ -152,6 +153,65 @@ pub fn hooks_post_commit(
hook.run_hook(&[])
}
+///
+pub enum PrepareCommitMsgSource {
+ Message,
+ Template,
+ Merge,
+ Squash,
+ Commit(git2::Oid),
+}
+
+/// this hook is documented here <https://git-scm.com/docs/githooks#_prepare_commit_msg>
+pub fn hooks_prepare_commit_msg(
+ repo: &Repository,
+ other_paths: Option<&[&str]>,
+ source: PrepareCommitMsgSource,
+ msg: &mut String,
+) -> Result<HookResult> {
+ let hook =
+ HookPaths::new(repo, other_paths, HOOK_PREPARE_COMMIT_MSG)?;
+
+ if !hook.found() {
+ return Ok(HookResult::NoHookFound);
+ }
+
+ let temp_file = hook.git.join(HOOK_COMMIT_MSG_TEMP_FILE);
+ File::create(&temp_file)?.write_all(msg.as_bytes())?;
+
+ let temp_file_path = temp_file.as_os_str().to_string_lossy();
+
+ let vec = vec![
+ temp_file_path.as_ref(),
+ match source {
+ PrepareCommitMsgSource::Message => "message",
+ PrepareCommitMsgSource::Template => "template",
+ PrepareCommitMsgSource::Merge => "merge",
+ PrepareCommitMsgSource::Squash => "squash",
+ PrepareCommitMsgSource::Commit(_) => "commit",
+ },
+ ];
+ let mut args = vec;
+
+ let id = if let PrepareCommitMsgSource::Commit(id) = &source {
+ Some(id.to_string())
+ } else {
+ None
+ };
+
+ if let Some(id) = &id {
+ args.push(id);
+ }
+
+ let res = hook.run_hook(args.as_slice())?;
+
+ // load possibly altered msg
+ msg.clear();
+ File::open(temp_file)?.read_to_string(msg)?;
+
+ Ok(res)
+}
+
#[cfg(test)]
mod tests {
use super::*;
@@ -480,4 +540,65 @@ exit 0
assert_eq!(hook.pwd, git_root.parent().unwrap());
}
+
+ #[test]
+ fn test_hooks_prep_commit_msg_success() {
+ let (_td, repo) = repo_init();
+
+ let hook = b"#!/bin/sh
+echo msg:$2 > $1
+exit 0
+ ";
+
+ create_hook(&repo, HOOK_PREPARE_COMMIT_MSG, hook);
+
+ let mut msg = String::from("test");
+ let res = hooks_prepare_commit_msg(
+ &repo,
+ None,
+ PrepareCommitMsgSource::Message,
+ &mut msg,
+ )
+ .unwrap();
+
+ assert!(matches!(res, HookResult::Ok { .. }));
+ assert_eq!(msg, String::from("msg:message\n"));
+ }
+
+ #[test]
+ fn test_hooks_prep_commit_msg_reject() {
+ let (_td, repo) = repo_init();
+
+ let hook = b"#!/bin/sh
+echo $2,$3 > $1
+echo 'rejected'
+exit 2
+ ";
+
+ create_hook(&repo, HOOK_PREPARE_COMMIT_MSG, hook);
+
+ let mut msg = String::from("test");
+ let res = hooks_prepare_commit_msg(
+ &repo,
+ None,
+ PrepareCommitMsgSource::Commit(git2::Oid::zero()),
+ &mut msg,
+ )
+ .unwrap();
+
+ let HookResult::RunNotSuccessful { code, stdout, .. } = res
+ else {
+ unreachable!()
+ };
+
+ assert_eq!(code.unwrap(), 2);
+ assert_eq!(&stdout, "rejected\n");
+
+ assert_eq!(
+ msg,
+ String::from(
+ "commit,0000000000000000000000000000000000000000\n"
+ )
+ );
+ }
}
diff --git a/src/components/commit.rs b/src/components/commit.rs
index ba32394a..bd762878 100644
--- a/src/components/commit.rs
+++ b/src/components/commit.rs
@@ -14,8 +14,8 @@ use anyhow::{bail, Ok, Result};
use asyncgit::{
cached, message_prettify,
sync::{
- self, get_config_string, CommitId, HookResult, RepoPathRef,
- RepoState,
+ self, get_config_string, CommitId, HookResult,
+ PrepareCommitMsgSource, RepoPathRef, RepoState,
},
StatusItem, StatusItemType,
};
@@ -366,7 +366,7 @@ impl CommitComponent {
let repo_state = sync::repo_state(&self.repo.borrow())?;
- self.mode = if repo_state != RepoState::Clean
+ let (mode, msg_source) = if repo_state != RepoState::Clean
&& reword.is_some()
{
bail!("cannot reword while repo is not in a clean state");
@@ -381,7 +381,7 @@ impl CommitComponent {
.combine(),
);
self.input.set_title(strings::commit_reword_title());
- Mode::Reword(reword_id)
+ (Mode::Reword(reword_id), PrepareCommitMsgSource::Message)
} else {
match repo_state {
RepoState::Merge => {
@@ -392,7 +392,7 @@ impl CommitComponent {
self.input.set_text(sync::merge_msg(
&self.repo.borrow(),
)?);
- Mode::Merge(ids)
+ (Mode::Merge(ids), PrepareCommitMsgSource::Merge)
}
RepoState::Revert => {
self.input
@@ -400,7 +400,7 @@ impl CommitComponent {
self.input.set_text(sync::merge_msg(
&self.repo.borrow(),
)?);
- Mode::Revert
+ (Mode::Revert, PrepareCommitMsgSource::Message)
}
_ => {
@@ -430,17 +430,35 @@ impl CommitComponent {
.ok()
});
- if self.is_empty() {
+ let msg_source = if self.is_empty() {
if let Some(s) = &self.commit_template {
self.input.set_text(s.clone());
+ PrepareCommitMsgSource::Template
+ } else {
+ PrepareCommitMsgSource::Message
}
- }
+ } else {
+ PrepareCommitMsgSource::Message
+ };
self.input.set_title(strings::commit_title());
- Mode::Normal
+
+ (Mode::Normal, msg_source)
}
}
};
+ self.mode = mode;
+
+ let mut msg = self.input.get_text().to_string();
+ if let HookResult::NotOk(e) = sync::hooks_prepare_commit_msg(
+ &self.repo.borrow(),
+ msg_source,
+ &mut msg,
+ )? {
+ log::error!("prepare-commit-msg hook rejection: {e}",);
+ }
+ self.input.set_text(msg);
+
self.commit_msg_history_idx = 0;
self.input.show()?;