use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
#[cfg(feature = "with_subcommand_hook")]
use crate::git_gamble::subcommand_hook::hook::Hook;
use crate::message::Message;
use super::gamble_result::GambleError;
use super::gamble_result::GambleResult;
use super::git_is_required::git_is_required;
enum CommandError {
GambleError(GambleError),
CommandNotFound(Message),
}
pub(crate) struct Repository {
path: PathBuf,
dry_run: bool,
no_verify: bool,
}
impl Repository {
pub(crate) fn new(path: &Path, dry_run: bool, no_verify: bool) -> Repository {
Repository {
path: path.to_path_buf(),
dry_run,
no_verify,
}
}
fn path(&self) -> &Path {
Path::new(&self.path)
}
fn executed_command(&self, program: &str, args: &[&str]) -> String {
format!("`{program} {}` in {}", args.join(" "), self.path.display())
}
}
impl Repository {
fn shell_command(&self, program: &str, args: &[&str]) -> Result<(), CommandError> {
let executed_command = self.executed_command(program, args);
if self.dry_run {
log::info!("Would execute {executed_command}");
return Ok(());
}
let command = Command::new(program)
.current_dir(self.path())
.args(args)
.status();
command
.map_err(|error| {
CommandError::CommandNotFound(Message::Error(format!(
"Failed to execute {executed_command}: {error}"
)))
})
.and_then(|status| match status.code() {
Some(code) => {
log::info!("{executed_command} exited with {code}");
if status.success() {
Ok(())
} else {
Err(CommandError::GambleError(GambleError {
messages: vec![],
code,
}))
}
}
None => Err(CommandError::GambleError(GambleError {
messages: vec![Message::Error(format!(
"{executed_command} has exited without a normal code"
))],
code: 1,
})),
})
}
pub(crate) fn command(&self, args: &[&str]) -> GambleResult<()> {
self.shell_command("git", args)
.map_err(|error| match error {
CommandError::CommandNotFound(message) => git_is_required().add_message(message),
CommandError::GambleError(gamble_error) => gamble_error,
})
.map_err(|error| {
let executed_command =
&format!("`git {}` in {}", args.join(" "), self.path().display());
let help_message = format!(
"Failed to execute {executed_command} returns code {}",
error.code
);
error.add_message(Message::Error(help_message))
})
}
fn allow_unknown_hook_name(&self) -> GambleResult<Option<&str>> {
self.query(&["hook", "run", "--help"]).map(|output| {
output
.contains("allow-unknown-hook-name")
.then_some("--allow-unknown-hook-name")
})
}
pub(crate) fn run_hook(&self, hook_name: &str, args: &[&str]) -> GambleResult<()> {
if !cfg!(feature = "with_custom_hooks") {
return Ok(());
}
if self.no_verify {
log::info!("Would execute {hook_name} hook");
return Ok(());
}
self.allow_unknown_hook_name()
.and_then(|allow_unknown_hook_name| {
let hook_with_arguments = [
vec!["hook", "run", "--ignore-missing"],
allow_unknown_hook_name
.map(|opt| vec![opt])
.unwrap_or_default(),
vec![hook_name, "--"],
args.to_vec(),
]
.concat();
self.command(&hook_with_arguments).map_err(|error| {
error
.add_message(Message::Error(format!(
"The {hook_name} hook returns code {}, but it should be 0",
error.code
)))
.add_message(Message::Info(format!(
"You can test the hook by executing : git {}",
hook_with_arguments.join(" "),
)))
})
})
}
pub(crate) fn run_test_command(
&self,
test_command: Vec<String>,
) -> Result<std::process::ExitStatus, GambleError> {
let command = Command::new(&test_command[0])
.args(&test_command[1..])
.status();
command.map_err(|error| {
let executed_test_command = test_command.join(" ");
GambleError {
messages: vec![
Message::Error(format!("`{executed_test_command}`: {error}")),
Message::Error(format!(
"Test command `{executed_test_command}` in {} has exited without a normal code",
self.path().display()
)),
],
code: 1,
}
})
}
}
impl Repository {
fn query(&self, args: &[&str]) -> GambleResult<String> {
let command = Command::new("git")
.current_dir(self.path())
.args(args)
.output();
command
.map_err(|error| git_is_required().add_message(Message::Error(format!("{error}"))))
.and_then(|output| {
String::from_utf8(output.stdout).map_err(|error| GambleError {
messages: vec![Message::Error(format!("Can't parse git output : {error}"))],
code: 1,
})
})
.map_err(|error| {
let executed_command = self.executed_command("git", args);
error.add_message(Message::Error(format!(
"Failed to execute {executed_command}"
)))
})
}
fn get_commit_hash_for(&self, revision: &str) -> GambleResult<String> {
self.query(&["rev-parse", revision])
}
pub(crate) fn head_is_failing_ref(&self) -> bool {
let head_commit_hash = self.get_commit_hash_for("HEAD");
let ref_is_failing_commit_hash = self.get_commit_hash_for("gamble-is-failing");
match (head_commit_hash, ref_is_failing_commit_hash) {
(Ok(head_commit_hash), Ok(ref_is_failing_commit_hash)) => {
head_commit_hash == ref_is_failing_commit_hash
}
_ => false,
}
}
pub(crate) fn is_clean(&self) -> GambleResult<bool> {
self.query(&["status", "-z"])
.map(|status| status.is_empty())
}
}
#[cfg(feature = "with_subcommand_hook")]
impl Repository {
pub(crate) fn enable_hook(&self, hook: &Hook) -> GambleResult<()> {
let hook_name = hook.hook_name();
let hooks_map = hook.hooks();
hooks_map.iter().try_for_each(|(hook_event, hook_content)| {
let git_config_section = format!("hook.{}-{}", hook_name, hook_event);
self.add_hook(
format!("{}.{}.sh", hook_event, hook_name).as_str(),
hook_content,
)
.and_then(|hook_path| {
let config_value = hook_path
.to_str()
.expect("Unhandled error ; please report it");
self.config_set(format!("{git_config_section}.command"), config_value)
})
.and_then(|()| self.config_set(format!("{git_config_section}.event"), hook_event))
})
}
pub(crate) fn disable_hook(&self, hook: &Hook) -> GambleResult<()> {
let hook_name = hook.hook_name();
let hooks_map = hook.hooks();
hooks_map.keys().try_for_each(|hook_event| {
let git_config_section = format!("hook.{}-{}", hook_name, hook_event);
self.config_unset(format!("{git_config_section}.event"))
.and_then(|()| self.config_unset(format!("{git_config_section}.command")))
})
}
fn add_hook(&self, hook_file_name: &str, hook_content: &str) -> GambleResult<PathBuf> {
use std::fs;
let hooks_path = self.hooks_path();
let hook_path = hooks_path.join(hook_file_name);
fs::create_dir_all(hooks_path)
.and_then(|()| fs::write(&hook_path, hook_content))
.and_then(|()| {
#[cfg(unix)]
{
use std::fs::set_permissions;
use std::os::unix::fs::PermissionsExt;
let owner_all_permissions_all_executable = PermissionsExt::from_mode(0o755);
set_permissions(&hook_path, owner_all_permissions_all_executable)?
}
Ok(())
})
.map_err(|error| GambleError {
messages: vec![
Message::Error(format!(
"Failed to create the hook file in {}",
hook_path.display()
)),
Message::Error(format!("{error}")),
Message::Info(
"Please make sure you are able to write in that path".to_string(),
),
],
code: 1,
})
.map(|()| hook_path)
}
fn hooks_path(&self) -> PathBuf {
let default_hooks_path = self.path().join(".git/hooks");
self.query(&[
"config",
"get",
"--default",
default_hooks_path.to_str().unwrap(),
"core.hooksPath",
])
.map(|hooks_path| Path::new(&hooks_path.trim()).to_path_buf())
.unwrap_or(default_hooks_path)
}
fn config_set(&self, config_key: String, config_value: &str) -> Result<(), GambleError> {
let comment = "Generated by git-gamble";
self.command(&[
"config",
"set",
"--comment",
comment,
config_key.as_str(),
config_value,
])
}
fn config_unset(&self, config_key: String) -> GambleResult<()> {
match self.command(&["config", "get", config_key.as_str()]) {
Ok(()) => self.command(&["config", "unset", config_key.as_str()]),
Err(_config_is_not_set_do_nothing) => Ok(()),
}
}
}