git-gamble 2.12.1

blend TDD + TCR to make sure to develop the right thing 😌, baby step by baby step πŸ‘ΆπŸ¦Ά
Documentation
use std::borrow::Cow;
use std::io;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;

use crate::message::Message;
use crate::path::is_executable_path::IsExecutablePath;

use super::gamble_result::GambleError;
use super::gamble_result::GambleResult;

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)
	}
}

/// commands
impl Repository {
	fn shell_command(&self, program: &str, args: &[&str]) -> GambleResult {
		let executed_command =
			&format!("`{program} {}` in {}", args.join(" "), self.path.display());

		if self.dry_run {
			log::info!("Would execute {executed_command}");

			return Ok(());
		}

		let help_message = &format!("Failed to execute {executed_command}");
		let status = Command::new(program)
			.current_dir(self.path())
			.args(args)
			.status()
			.expect(help_message); // TODO: test this

		match status.code() {
			Some(code) => {
				log::info!("{executed_command} exited with {code}");

				if status.success() {
					Ok(())
				} else {
					Err(GambleError {
						messages: vec![],
						code,
					})
				}
			}
			None => Err(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| {
			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::Fatal(help_message))
		})
	}

	fn get_hooks_path(&self) -> Cow<'_, Path> {
		self.query(&["config", "core.hooksPath"]).map_or_else(
			|_| Cow::from(Path::new(".git").join("hooks")),
			|hooks_path_output| Cow::from(PathBuf::default().join(hooks_path_output.trim())),
		)
	}

	pub(crate) fn run_hook(&self, hook_name: &str, args: &[&str]) -> GambleResult {
		let hooks_path = self.get_hooks_path();

		let hook_path = hooks_path.join(hook_name);

		if !hook_path.exists() {
			return Ok(());
		}

		if !hook_path.is_executable() {
			return Ok(());
		}

		if self.no_verify {
			log::info!("Would execute {hook_name} hook"); // TODO: test this

			return Ok(());
		}

		let hook_path_as_string = hook_path.to_str().unwrap(); // TODO: test this
		let hook_with_arguments = [vec![hook_path_as_string], args.to_vec()].concat();
		self.shell_command("sh", &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 execute : git hook run {hook_name} -- {}",
						args.join(" "),
					)))
			})
	}
}

/// queries
impl Repository {
	fn query(&self, args: &[&str]) -> io::Result<String> {
		let query = &format!("`git {}` in {}", args.join(" "), self.path.display());
		let help_message = &format!("Failed to execute {query}");
		let cmd = Command::new("git")
			.current_dir(self.path())
			.args(args)
			.output()
			.expect(help_message); // TODO: test this

		match cmd.status.code() {
			Some(code) => {
				log::info!("query {query} exited with {code}");

				if cmd.status.success() {
					Ok(String::from_utf8(cmd.stdout).unwrap()) // TODO: test this
				} else {
					Err(io::Error::other(help_message.to_string())) // TODO: every error case are ignored
				}
			}
			None => todo!(), // TODO: test this
		}
	}

	fn get_commit_hash_for(&self, revision: &str) -> io::Result<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) -> bool {
		let status = self.query(&["status", "-z"]).expect("Can't get status"); // TODO: test this

		status.is_empty()
	}
}