Support prepare commit hook (#1978)

This commit is contained in:
extrawurst 2023-12-16 00:07:54 +01:00 committed by GitHub
parent 7b7c5c4131
commit e7c61ffc89
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 171 additions and 13 deletions

View File

@ -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))

2
Cargo.lock generated
View File

@ -714,7 +714,7 @@ dependencies = [
[[package]]
name = "git2-hooks"
version = "0.3.0"
version = "0.3.1"
dependencies = [
"git2",
"git2-testing",

View File

@ -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

View File

@ -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::*;

View File

@ -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;

View File

@ -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"

View File

@ -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"
)
);
}
}

View File

@ -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()?;