mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-12-25 02:26:14 +03:00
GB-700: support user triggered pre commit hook (#1960)
* run pre commit hook * unittest * support custom search path: `.husky`
This commit is contained in:
parent
043e0de45b
commit
8d917494ed
22
Cargo.lock
generated
22
Cargo.lock
generated
@ -1763,6 +1763,18 @@ dependencies = [
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "git2-hooks"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fa76beea92a16bbdf05842f0c6d9611bbb7eef764b34dde223359077463b800"
|
||||
dependencies = [
|
||||
"git2",
|
||||
"log",
|
||||
"shellexpand",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gitbutler"
|
||||
version = "0.0.0"
|
||||
@ -1778,6 +1790,7 @@ dependencies = [
|
||||
"filetime",
|
||||
"futures",
|
||||
"git2",
|
||||
"git2-hooks",
|
||||
"governor",
|
||||
"itertools 0.12.0",
|
||||
"lazy_static",
|
||||
@ -4596,6 +4609,15 @@ dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shellexpand"
|
||||
version = "3.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da03fa3b94cc19e3ebfc88c4229c49d8f08cdbd1228870a45f0ffdf84988e14b"
|
||||
dependencies = [
|
||||
"dirs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook"
|
||||
version = "0.3.17"
|
||||
|
@ -32,6 +32,7 @@ diffy = "0.3.0"
|
||||
filetime = "0.2.22"
|
||||
futures = "0.3"
|
||||
git2 = { version = "0.18.1", features = ["vendored-openssl", "vendored-libgit2"] }
|
||||
git2-hooks = "0.3"
|
||||
governor = "0.6.0"
|
||||
itertools = "0.12"
|
||||
lazy_static = "1.4.0"
|
||||
|
@ -13,6 +13,7 @@ pub enum Code {
|
||||
ProjectConflict,
|
||||
ProjectHead,
|
||||
Menu,
|
||||
Hook,
|
||||
}
|
||||
|
||||
impl fmt::Display for Code {
|
||||
@ -27,6 +28,7 @@ impl fmt::Display for Code {
|
||||
Code::ProjectGitRemote => write!(f, "errors.projects.git.remote"),
|
||||
Code::ProjectHead => write!(f, "errors.projects.head"),
|
||||
Code::ProjectConflict => write!(f, "errors.projects.conflict"),
|
||||
Code::Hook => write!(f, "errors.hook"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,8 @@ pub enum Error {
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("network error: {0}")]
|
||||
Network(git2::Error),
|
||||
#[error("hook error: {0}")]
|
||||
Hooks(#[from] git2_hooks::HooksError),
|
||||
#[error(transparent)]
|
||||
Other(git2::Error),
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
use std::{path, str};
|
||||
|
||||
use git2::Submodule;
|
||||
use git2_hooks::HookResult;
|
||||
|
||||
use crate::keys;
|
||||
|
||||
@ -424,6 +425,11 @@ impl Repository {
|
||||
.map(|iter| iter.map(|reference| reference.map(Into::into).map_err(Into::into)))
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn run_hook_pre_commit(&self) -> Result<HookResult> {
|
||||
let res = git2_hooks::hooks_pre_commit(&self.0, Some(&["../.husky"]))?;
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CheckoutTreeBuidler<'a> {
|
||||
|
@ -144,6 +144,8 @@ pub enum CommitError {
|
||||
DefaultTargetNotSet(DefaultTargetNotSetError),
|
||||
#[error("will not commit conflicted files")]
|
||||
Conflicted(ProjectConflictError),
|
||||
#[error("commit hook rejected")]
|
||||
CommitHookRejected(String),
|
||||
#[error(transparent)]
|
||||
Other(#[from] anyhow::Error),
|
||||
}
|
||||
@ -410,6 +412,10 @@ impl From<CommitError> for Error {
|
||||
CommitError::BranchNotFound(error) => error.into(),
|
||||
CommitError::DefaultTargetNotSet(error) => error.into(),
|
||||
CommitError::Conflicted(error) => error.into(),
|
||||
CommitError::CommitHookRejected(error) => Error::UserError {
|
||||
code: crate::error::Code::Hook,
|
||||
message: error,
|
||||
},
|
||||
CommitError::Other(error) => {
|
||||
tracing::error!(?error, "commit error");
|
||||
Error::Unknown
|
||||
|
@ -14,6 +14,7 @@ use pretty_assertions::{assert_eq, assert_ne};
|
||||
use crate::{
|
||||
gb_repository, git, project_repository, reader, sessions,
|
||||
test_utils::{self, empty_bare_repository, Case, Suite},
|
||||
virtual_branches::errors::CommitError,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
@ -2740,3 +2741,71 @@ fn test_verify_branch_not_integration() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pre_commit_hook_rejection() -> Result<()> {
|
||||
let suite = Suite::default();
|
||||
let Case {
|
||||
project,
|
||||
gb_repository,
|
||||
project_repository,
|
||||
..
|
||||
} = suite.new_case_with_files(HashMap::from([
|
||||
(
|
||||
path::PathBuf::from("test.txt"),
|
||||
"line1\nline2\nline3\nline4\n",
|
||||
),
|
||||
(
|
||||
path::PathBuf::from("test2.txt"),
|
||||
"line5\nline6\nline7\nline8\n",
|
||||
),
|
||||
]));
|
||||
|
||||
set_test_target(&gb_repository, &project_repository)?;
|
||||
|
||||
let branch1_id = create_virtual_branch(
|
||||
&gb_repository,
|
||||
&project_repository,
|
||||
&BranchCreateRequest::default(),
|
||||
)
|
||||
.expect("failed to create virtual branch")
|
||||
.id;
|
||||
|
||||
std::fs::write(
|
||||
std::path::Path::new(&project.path).join("test.txt"),
|
||||
"line0\nline1\nline2\nline3\nline4\n",
|
||||
)?;
|
||||
|
||||
let hook = b"#!/bin/sh
|
||||
echo 'rejected'
|
||||
exit 1
|
||||
";
|
||||
|
||||
git2_hooks::create_hook(
|
||||
(&project_repository.git_repository).into(),
|
||||
git2_hooks::HOOK_PRE_COMMIT,
|
||||
hook,
|
||||
);
|
||||
|
||||
let res = commit(
|
||||
&gb_repository,
|
||||
&project_repository,
|
||||
&branch1_id,
|
||||
"test commit",
|
||||
None,
|
||||
Some(suite.keys.get_or_create()?).as_ref(),
|
||||
None,
|
||||
);
|
||||
|
||||
let error = res.unwrap_err();
|
||||
|
||||
assert!(matches!(error, CommitError::CommitHookRejected(_)));
|
||||
|
||||
let CommitError::CommitHookRejected(output) = error else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
assert_eq!(&output, "rejected\n");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ use std::{
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use diffy::{apply_bytes, Patch};
|
||||
use git2_hooks::HookResult;
|
||||
use serde::Serialize;
|
||||
use slug::slugify;
|
||||
|
||||
@ -2009,6 +2010,15 @@ pub fn commit(
|
||||
signing_key: Option<&keys::PrivateKey>,
|
||||
user: Option<&users::User>,
|
||||
) -> Result<git::Oid, errors::CommitError> {
|
||||
let hook_result = project_repository
|
||||
.git_repository
|
||||
.run_hook_pre_commit()
|
||||
.context("failed to run hook")?;
|
||||
|
||||
if let HookResult::RunNotSuccessful { stdout, .. } = hook_result {
|
||||
return Err(errors::CommitError::CommitHookRejected(stdout));
|
||||
}
|
||||
|
||||
let default_target = gb_repository
|
||||
.default_target()
|
||||
.context("failed to get default target")?
|
||||
|
Loading…
Reference in New Issue
Block a user