mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-11-24 13:37:34 +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",
|
"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]]
|
[[package]]
|
||||||
name = "gitbutler"
|
name = "gitbutler"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
@ -1778,6 +1790,7 @@ dependencies = [
|
|||||||
"filetime",
|
"filetime",
|
||||||
"futures",
|
"futures",
|
||||||
"git2",
|
"git2",
|
||||||
|
"git2-hooks",
|
||||||
"governor",
|
"governor",
|
||||||
"itertools 0.12.0",
|
"itertools 0.12.0",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
@ -4596,6 +4609,15 @@ dependencies = [
|
|||||||
"lazy_static",
|
"lazy_static",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shellexpand"
|
||||||
|
version = "3.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "da03fa3b94cc19e3ebfc88c4229c49d8f08cdbd1228870a45f0ffdf84988e14b"
|
||||||
|
dependencies = [
|
||||||
|
"dirs",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "signal-hook"
|
name = "signal-hook"
|
||||||
version = "0.3.17"
|
version = "0.3.17"
|
||||||
|
@ -32,6 +32,7 @@ diffy = "0.3.0"
|
|||||||
filetime = "0.2.22"
|
filetime = "0.2.22"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
git2 = { version = "0.18.1", features = ["vendored-openssl", "vendored-libgit2"] }
|
git2 = { version = "0.18.1", features = ["vendored-openssl", "vendored-libgit2"] }
|
||||||
|
git2-hooks = "0.3"
|
||||||
governor = "0.6.0"
|
governor = "0.6.0"
|
||||||
itertools = "0.12"
|
itertools = "0.12"
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
|
@ -13,6 +13,7 @@ pub enum Code {
|
|||||||
ProjectConflict,
|
ProjectConflict,
|
||||||
ProjectHead,
|
ProjectHead,
|
||||||
Menu,
|
Menu,
|
||||||
|
Hook,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for Code {
|
impl fmt::Display for Code {
|
||||||
@ -27,6 +28,7 @@ impl fmt::Display for Code {
|
|||||||
Code::ProjectGitRemote => write!(f, "errors.projects.git.remote"),
|
Code::ProjectGitRemote => write!(f, "errors.projects.git.remote"),
|
||||||
Code::ProjectHead => write!(f, "errors.projects.head"),
|
Code::ProjectHead => write!(f, "errors.projects.head"),
|
||||||
Code::ProjectConflict => write!(f, "errors.projects.conflict"),
|
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),
|
Io(#[from] std::io::Error),
|
||||||
#[error("network error: {0}")]
|
#[error("network error: {0}")]
|
||||||
Network(git2::Error),
|
Network(git2::Error),
|
||||||
|
#[error("hook error: {0}")]
|
||||||
|
Hooks(#[from] git2_hooks::HooksError),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Other(git2::Error),
|
Other(git2::Error),
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
use std::{path, str};
|
use std::{path, str};
|
||||||
|
|
||||||
use git2::Submodule;
|
use git2::Submodule;
|
||||||
|
use git2_hooks::HookResult;
|
||||||
|
|
||||||
use crate::keys;
|
use crate::keys;
|
||||||
|
|
||||||
@ -424,6 +425,11 @@ impl Repository {
|
|||||||
.map(|iter| iter.map(|reference| reference.map(Into::into).map_err(Into::into)))
|
.map(|iter| iter.map(|reference| reference.map(Into::into).map_err(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> {
|
pub struct CheckoutTreeBuidler<'a> {
|
||||||
|
@ -144,6 +144,8 @@ pub enum CommitError {
|
|||||||
DefaultTargetNotSet(DefaultTargetNotSetError),
|
DefaultTargetNotSet(DefaultTargetNotSetError),
|
||||||
#[error("will not commit conflicted files")]
|
#[error("will not commit conflicted files")]
|
||||||
Conflicted(ProjectConflictError),
|
Conflicted(ProjectConflictError),
|
||||||
|
#[error("commit hook rejected")]
|
||||||
|
CommitHookRejected(String),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Other(#[from] anyhow::Error),
|
Other(#[from] anyhow::Error),
|
||||||
}
|
}
|
||||||
@ -410,6 +412,10 @@ impl From<CommitError> for Error {
|
|||||||
CommitError::BranchNotFound(error) => error.into(),
|
CommitError::BranchNotFound(error) => error.into(),
|
||||||
CommitError::DefaultTargetNotSet(error) => error.into(),
|
CommitError::DefaultTargetNotSet(error) => error.into(),
|
||||||
CommitError::Conflicted(error) => error.into(),
|
CommitError::Conflicted(error) => error.into(),
|
||||||
|
CommitError::CommitHookRejected(error) => Error::UserError {
|
||||||
|
code: crate::error::Code::Hook,
|
||||||
|
message: error,
|
||||||
|
},
|
||||||
CommitError::Other(error) => {
|
CommitError::Other(error) => {
|
||||||
tracing::error!(?error, "commit error");
|
tracing::error!(?error, "commit error");
|
||||||
Error::Unknown
|
Error::Unknown
|
||||||
|
@ -14,6 +14,7 @@ use pretty_assertions::{assert_eq, assert_ne};
|
|||||||
use crate::{
|
use crate::{
|
||||||
gb_repository, git, project_repository, reader, sessions,
|
gb_repository, git, project_repository, reader, sessions,
|
||||||
test_utils::{self, empty_bare_repository, Case, Suite},
|
test_utils::{self, empty_bare_repository, Case, Suite},
|
||||||
|
virtual_branches::errors::CommitError,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
@ -2740,3 +2741,71 @@ fn test_verify_branch_not_integration() -> Result<()> {
|
|||||||
|
|
||||||
Ok(())
|
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 anyhow::{bail, Context, Result};
|
||||||
use diffy::{apply_bytes, Patch};
|
use diffy::{apply_bytes, Patch};
|
||||||
|
use git2_hooks::HookResult;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use slug::slugify;
|
use slug::slugify;
|
||||||
|
|
||||||
@ -2009,6 +2010,15 @@ pub fn commit(
|
|||||||
signing_key: Option<&keys::PrivateKey>,
|
signing_key: Option<&keys::PrivateKey>,
|
||||||
user: Option<&users::User>,
|
user: Option<&users::User>,
|
||||||
) -> Result<git::Oid, errors::CommitError> {
|
) -> 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
|
let default_target = gb_repository
|
||||||
.default_target()
|
.default_target()
|
||||||
.context("failed to get default target")?
|
.context("failed to get default target")?
|
||||||
|
Loading…
Reference in New Issue
Block a user