Skip to content

Commit e7c61ff

Browse files
authored
Support prepare commit hook (#1978)
1 parent 7b7c5c4 commit e7c61ff

File tree

8 files changed

+171
-13
lines changed

8 files changed

+171
-13
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Added
1111
* `theme.ron` now supports customizing line break symbol ([#1894](https://github.com/extrawurst/gitui/issues/1894))
1212
* add confirmation for dialog for undo commit [[@TeFiLeDo](https://github.com/TeFiLeDo)] ([#1912](https://github.com/extrawurst/gitui/issues/1912))
13+
* support `prepare-commit-msg` hook ([#1873](https://github.com/extrawurst/gitui/issues/1873))
1314

1415
### Changed
1516
* do not allow tag when `tag.gpgsign` enabled [[@TeFiLeDo](https://github.com/TeFiLeDo)] ([#1915](https://github.com/extrawurst/gitui/pull/1915))

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343

4444
- Fast and intuitive **keyboard only** control
4545
- Context based help (**no need to memorize** tons of hot-keys)
46-
- Inspect, commit, and amend changes (incl. hooks: *pre-commit*,*commit-msg*,*post-commit*)
46+
- Inspect, commit, and amend changes (incl. hooks: *pre-commit*,*commit-msg*,*post-commit*,*prepare-commit-msg*)
4747
- Stage, unstage, revert and reset files, hunks and lines
4848
- Stashing (save, pop, apply, drop, and inspect)
4949
- Push / Fetch to / from remote

asyncgit/src/sync/hooks.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use super::{repository::repo, RepoPath};
22
use crate::error::Result;
3+
pub use git2_hooks::PrepareCommitMsgSource;
34
use scopetime::scope_time;
45

56
///
@@ -59,6 +60,22 @@ pub fn hooks_post_commit(repo_path: &RepoPath) -> Result<HookResult> {
5960
Ok(git2_hooks::hooks_post_commit(&repo, None)?.into())
6061
}
6162

63+
///
64+
pub fn hooks_prepare_commit_msg(
65+
repo_path: &RepoPath,
66+
source: PrepareCommitMsgSource,
67+
msg: &mut String,
68+
) -> Result<HookResult> {
69+
scope_time!("hooks_prepare_commit_msg");
70+
71+
let repo = repo(repo_path)?;
72+
73+
Ok(git2_hooks::hooks_prepare_commit_msg(
74+
&repo, None, source, msg,
75+
)?
76+
.into())
77+
}
78+
6279
#[cfg(test)]
6380
mod tests {
6481
use super::*;

asyncgit/src/sync/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@ pub use config::{
6565
pub use diff::get_diff_commit;
6666
pub use git2::BranchType;
6767
pub use hooks::{
68-
hooks_commit_msg, hooks_post_commit, hooks_pre_commit, HookResult,
68+
hooks_commit_msg, hooks_post_commit, hooks_pre_commit,
69+
hooks_prepare_commit_msg, HookResult, PrepareCommitMsgSource,
6970
};
7071
pub use hunks::{reset_hunk, stage_hunk, unstage_hunk};
7172
pub use ignore::add_to_ignore;

git2-hooks/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "git2-hooks"
3-
version = "0.3.0"
3+
version = "0.3.1"
44
authors = ["extrawurst <[email protected]>"]
55
edition = "2021"
66
description = "adds git hooks support based on git2-rs"

git2-hooks/src/lib.rs

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ use git2::Repository;
2727
pub const HOOK_POST_COMMIT: &str = "post-commit";
2828
pub const HOOK_PRE_COMMIT: &str = "pre-commit";
2929
pub const HOOK_COMMIT_MSG: &str = "commit-msg";
30+
pub const HOOK_PREPARE_COMMIT_MSG: &str = "prepare-commit-msg";
3031

3132
const HOOK_COMMIT_MSG_TEMP_FILE: &str = "COMMIT_EDITMSG";
3233

@@ -152,6 +153,65 @@ pub fn hooks_post_commit(
152153
hook.run_hook(&[])
153154
}
154155

156+
///
157+
pub enum PrepareCommitMsgSource {
158+
Message,
159+
Template,
160+
Merge,
161+
Squash,
162+
Commit(git2::Oid),
163+
}
164+
165+
/// this hook is documented here <https://git-scm.com/docs/githooks#_prepare_commit_msg>
166+
pub fn hooks_prepare_commit_msg(
167+
repo: &Repository,
168+
other_paths: Option<&[&str]>,
169+
source: PrepareCommitMsgSource,
170+
msg: &mut String,
171+
) -> Result<HookResult> {
172+
let hook =
173+
HookPaths::new(repo, other_paths, HOOK_PREPARE_COMMIT_MSG)?;
174+
175+
if !hook.found() {
176+
return Ok(HookResult::NoHookFound);
177+
}
178+
179+
let temp_file = hook.git.join(HOOK_COMMIT_MSG_TEMP_FILE);
180+
File::create(&temp_file)?.write_all(msg.as_bytes())?;
181+
182+
let temp_file_path = temp_file.as_os_str().to_string_lossy();
183+
184+
let vec = vec![
185+
temp_file_path.as_ref(),
186+
match source {
187+
PrepareCommitMsgSource::Message => "message",
188+
PrepareCommitMsgSource::Template => "template",
189+
PrepareCommitMsgSource::Merge => "merge",
190+
PrepareCommitMsgSource::Squash => "squash",
191+
PrepareCommitMsgSource::Commit(_) => "commit",
192+
},
193+
];
194+
let mut args = vec;
195+
196+
let id = if let PrepareCommitMsgSource::Commit(id) = &source {
197+
Some(id.to_string())
198+
} else {
199+
None
200+
};
201+
202+
if let Some(id) = &id {
203+
args.push(id);
204+
}
205+
206+
let res = hook.run_hook(args.as_slice())?;
207+
208+
// load possibly altered msg
209+
msg.clear();
210+
File::open(temp_file)?.read_to_string(msg)?;
211+
212+
Ok(res)
213+
}
214+
155215
#[cfg(test)]
156216
mod tests {
157217
use super::*;
@@ -480,4 +540,65 @@ exit 0
480540

481541
assert_eq!(hook.pwd, git_root.parent().unwrap());
482542
}
543+
544+
#[test]
545+
fn test_hooks_prep_commit_msg_success() {
546+
let (_td, repo) = repo_init();
547+
548+
let hook = b"#!/bin/sh
549+
echo msg:$2 > $1
550+
exit 0
551+
";
552+
553+
create_hook(&repo, HOOK_PREPARE_COMMIT_MSG, hook);
554+
555+
let mut msg = String::from("test");
556+
let res = hooks_prepare_commit_msg(
557+
&repo,
558+
None,
559+
PrepareCommitMsgSource::Message,
560+
&mut msg,
561+
)
562+
.unwrap();
563+
564+
assert!(matches!(res, HookResult::Ok { .. }));
565+
assert_eq!(msg, String::from("msg:message\n"));
566+
}
567+
568+
#[test]
569+
fn test_hooks_prep_commit_msg_reject() {
570+
let (_td, repo) = repo_init();
571+
572+
let hook = b"#!/bin/sh
573+
echo $2,$3 > $1
574+
echo 'rejected'
575+
exit 2
576+
";
577+
578+
create_hook(&repo, HOOK_PREPARE_COMMIT_MSG, hook);
579+
580+
let mut msg = String::from("test");
581+
let res = hooks_prepare_commit_msg(
582+
&repo,
583+
None,
584+
PrepareCommitMsgSource::Commit(git2::Oid::zero()),
585+
&mut msg,
586+
)
587+
.unwrap();
588+
589+
let HookResult::RunNotSuccessful { code, stdout, .. } = res
590+
else {
591+
unreachable!()
592+
};
593+
594+
assert_eq!(code.unwrap(), 2);
595+
assert_eq!(&stdout, "rejected\n");
596+
597+
assert_eq!(
598+
msg,
599+
String::from(
600+
"commit,0000000000000000000000000000000000000000\n"
601+
)
602+
);
603+
}
483604
}

src/components/commit.rs

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ use anyhow::{bail, Ok, Result};
1414
use asyncgit::{
1515
cached, message_prettify,
1616
sync::{
17-
self, get_config_string, CommitId, HookResult, RepoPathRef,
18-
RepoState,
17+
self, get_config_string, CommitId, HookResult,
18+
PrepareCommitMsgSource, RepoPathRef, RepoState,
1919
},
2020
StatusItem, StatusItemType,
2121
};
@@ -366,7 +366,7 @@ impl CommitComponent {
366366

367367
let repo_state = sync::repo_state(&self.repo.borrow())?;
368368

369-
self.mode = if repo_state != RepoState::Clean
369+
let (mode, msg_source) = if repo_state != RepoState::Clean
370370
&& reword.is_some()
371371
{
372372
bail!("cannot reword while repo is not in a clean state");
@@ -381,7 +381,7 @@ impl CommitComponent {
381381
.combine(),
382382
);
383383
self.input.set_title(strings::commit_reword_title());
384-
Mode::Reword(reword_id)
384+
(Mode::Reword(reword_id), PrepareCommitMsgSource::Message)
385385
} else {
386386
match repo_state {
387387
RepoState::Merge => {
@@ -392,15 +392,15 @@ impl CommitComponent {
392392
self.input.set_text(sync::merge_msg(
393393
&self.repo.borrow(),
394394
)?);
395-
Mode::Merge(ids)
395+
(Mode::Merge(ids), PrepareCommitMsgSource::Merge)
396396
}
397397
RepoState::Revert => {
398398
self.input
399399
.set_title(strings::commit_title_revert());
400400
self.input.set_text(sync::merge_msg(
401401
&self.repo.borrow(),
402402
)?);
403-
Mode::Revert
403+
(Mode::Revert, PrepareCommitMsgSource::Message)
404404
}
405405

406406
_ => {
@@ -430,17 +430,35 @@ impl CommitComponent {
430430
.ok()
431431
});
432432

433-
if self.is_empty() {
433+
let msg_source = if self.is_empty() {
434434
if let Some(s) = &self.commit_template {
435435
self.input.set_text(s.clone());
436+
PrepareCommitMsgSource::Template
437+
} else {
438+
PrepareCommitMsgSource::Message
436439
}
437-
}
440+
} else {
441+
PrepareCommitMsgSource::Message
442+
};
438443
self.input.set_title(strings::commit_title());
439-
Mode::Normal
444+
445+
(Mode::Normal, msg_source)
440446
}
441447
}
442448
};
443449

450+
self.mode = mode;
451+
452+
let mut msg = self.input.get_text().to_string();
453+
if let HookResult::NotOk(e) = sync::hooks_prepare_commit_msg(
454+
&self.repo.borrow(),
455+
msg_source,
456+
&mut msg,
457+
)? {
458+
log::error!("prepare-commit-msg hook rejection: {e}",);
459+
}
460+
self.input.set_text(msg);
461+
444462
self.commit_msg_history_idx = 0;
445463
self.input.show()?;
446464

0 commit comments

Comments
 (0)