use colored::Colorize;
use git2::{Config, Repository};
use std::env;
use std::path::PathBuf;
use crate::{
bgit_error::{BGitError, BGitErrorWorkflowType, NO_RULE, NO_STEP},
config::global::BGitGlobalConfig,
hook_executor::execute_hook_util,
rules::Rule,
util::find_hook_with_extension,
};
pub mod git_add;
pub mod git_branch;
mod git_checkout;
mod git_clean;
pub mod git_clone;
pub mod git_commit;
pub mod git_config;
mod git_filter_repo;
pub mod git_init;
pub mod git_log;
pub mod git_pull;
pub mod git_push;
pub mod git_restore;
pub mod git_stash;
pub mod git_status;
const PENGUIN_EMOJI: &str = "🐧";
pub(crate) enum HookType {
PreEvent,
PostEvent,
}
pub(crate) trait AtomicEvent<'a> {
fn new(global_config: &'a BGitGlobalConfig) -> Self
where
Self: Sized;
fn get_name(&self) -> &str;
#[allow(unused)]
fn get_action_description(&self) -> &str;
fn add_pre_check_rule(&mut self, rule: Box<dyn Rule + Send + Sync>);
fn get_pre_check_rule(&self) -> &Vec<Box<dyn Rule + Send + Sync>>;
fn raw_execute(&self) -> Result<bool, Box<BGitError>>;
fn pre_execute_hook(&self) -> Result<bool, Box<BGitError>> {
let event_hook_file_name: String = format!("pre_{}", self.get_name());
let bgit_ok = self.execute_hook(&event_hook_file_name, HookType::PreEvent)?;
if !bgit_ok {
return Ok(false);
}
if self.get_name() == "git_commit" {
self.execute_standard_git_hook("pre-commit", HookType::PreEvent)?;
}
Ok(true)
}
fn post_execute_hook(&self) -> Result<bool, Box<BGitError>> {
let post_event_hook_file_name: String = format!("post_{}", self.get_name());
let bgit_ok = self.execute_hook(&post_event_hook_file_name, HookType::PostEvent)?;
if !bgit_ok {
return Ok(false);
}
if self.get_name() == "git_commit" {
self.execute_standard_git_hook("post-commit", HookType::PostEvent)?;
}
Ok(true)
}
fn execute_hook(
&self,
event_hook_file_name: &str,
hook_type: HookType,
) -> Result<bool, Box<BGitError>> {
let cwd = env::current_dir().expect("Failed to get current directory");
let git_repo = Repository::discover(&cwd);
let bgit_hooks_path = match git_repo.is_ok() {
true => {
let git_repo = git_repo.unwrap();
let git_repo_path = git_repo
.path()
.parent()
.expect("Failed to crawl to parent directory of .git folder");
git_repo_path.join(".bgit").join("hooks")
}
false => cwd.join(".bgit").join("hooks"),
};
let event_hook_path = bgit_hooks_path.join(event_hook_file_name);
match find_hook_with_extension(&event_hook_path) {
None => Ok(true),
Some(hook_path) => {
let hook_type_str = match hook_type {
HookType::PreEvent => "pre",
HookType::PostEvent => "post",
};
eprintln!(
"{} Running {}-event hook for {}",
PENGUIN_EMOJI,
hook_type_str,
self.get_name().cyan().bold()
);
execute_hook_util(&hook_path, self.get_name())
}
}
}
fn execute_standard_git_hook(
&self,
hook_name: &str,
hook_type: HookType,
) -> Result<bool, Box<BGitError>> {
if let Some(hooks_dir) = Self::resolve_standard_hooks_dir() {
let hook_path = hooks_dir.join(hook_name);
if hook_path.exists() {
let hook_type_str = match hook_type {
HookType::PreEvent => "pre",
HookType::PostEvent => "post",
};
eprintln!(
"{} Running standard Git {}-hook: {}",
PENGUIN_EMOJI,
hook_type_str,
hook_name.cyan().bold()
);
return execute_hook_util(&hook_path, hook_name);
}
}
Ok(true)
}
fn resolve_standard_hooks_dir() -> Option<PathBuf> {
let cwd = env::current_dir().ok()?;
let repo = Repository::discover(&cwd).ok()?;
if let Ok(cfg) = repo.config()
&& let Ok(val) = cfg.get_string("core.hooksPath")
{
let dir = Self::normalize_hooks_path(Self::repo_root_path(&repo)?, &val);
return Some(dir);
}
if let Ok(global) = Config::open_default()
&& let Ok(val) = global.get_string("core.hooksPath")
{
let dir = Self::normalize_hooks_path(Self::repo_root_path(&repo)?, &val);
return Some(dir);
}
Some(repo.path().join("hooks"))
}
fn repo_root_path(repo: &Repository) -> Option<PathBuf> {
if let Some(workdir) = repo.workdir() {
Some(workdir.to_path_buf())
} else {
repo.path().parent().map(|p| p.to_path_buf())
}
}
fn normalize_hooks_path(repo_root: PathBuf, configured: &str) -> PathBuf {
let expanded = if let Some(rest) = configured.strip_prefix("~/") {
if let Some(home_dir) = home::home_dir() {
home_dir.join(rest)
} else {
PathBuf::from(configured)
}
} else {
PathBuf::from(configured)
};
if expanded.is_absolute() {
expanded
} else {
repo_root.join(expanded)
}
}
fn check_rules(&self) -> Result<bool, Box<BGitError>> {
let rules = self.get_pre_check_rule();
if rules.is_empty() {
return Ok(true);
}
eprintln!(
"{} Running pre-check rules for {}",
PENGUIN_EMOJI,
self.get_name().cyan().bold()
);
for rule in rules.iter() {
let rule_passed = rule.execute()?;
if !rule_passed {
return Err(Box::new(BGitError::new(
"Pre-check Rule failed",
rule.get_description(),
BGitErrorWorkflowType::AtomicEvent,
NO_STEP,
self.get_name(),
rule.get_name(),
)));
}
}
Ok(true)
}
fn execute(&self) -> Result<bool, Box<BGitError>> {
eprintln!("Running event: {}", self.get_name());
let rule_check_status = self.check_rules()?;
if !rule_check_status {
return Ok(false);
}
let event_hook_status = self.pre_execute_hook()?;
if !event_hook_status {
return Err(Box::new(BGitError::new(
"Pre-event hook failed",
"Pre-event hook failed!",
BGitErrorWorkflowType::AtomicEvent,
NO_STEP,
self.get_name(),
NO_RULE,
)));
}
eprintln!(
"{} Running executor for event {}",
PENGUIN_EMOJI,
self.get_name().cyan().bold()
);
let raw_executor_status = self.raw_execute()?;
let post_event_hook_status = self.post_execute_hook()?;
if !post_event_hook_status {
return Err(Box::new(BGitError::new(
"Post-event hook failed",
"Post-event hook failed!",
BGitErrorWorkflowType::AtomicEvent,
NO_STEP,
self.get_name(),
NO_RULE,
)));
}
Ok(raw_executor_status)
}
fn to_bgit_error(&self, message: &str) -> Box<BGitError> {
Box::new(BGitError::new(
"BGitError",
message,
BGitErrorWorkflowType::AtomicEvent,
NO_STEP,
self.get_name(),
NO_RULE,
))
}
}