use core::fmt; use std::process::{Command, Output}; use crate::error::AppError; #[cfg(test)] use mockall::automock; pub(crate) enum GitCommand<'a> { IsValidRepository, LocalBranch, DefaultRemote, TrackedRemote(&'a str), UpstreamBranch(&'a str), DefaultBranch(&'a str), IsValidRemote(&'a str), CurrentTag, CurrentCommit, } #[derive(Default)] pub struct Git; #[derive(Debug)] pub(crate) enum Domain { GitHub, BitBucket, } #[derive(Debug)] pub(crate) struct Url { pub(crate) protocol: String, pub(crate) domain: Domain, pub(crate) path: String, } pub enum GitOutput { Ok(String), Err(String), } #[cfg_attr(test, automock)] pub trait GitTrait { fn is_valid_repository(&self) -> Result; fn get_local_branch(&self) -> Result; fn get_default_remote(&self) -> Result; fn get_tracked_remote(&self, tracked: &str) -> Result; fn get_upstream_branch(&self, branch: &str) -> Result; fn get_default_branch(&self, remote: &str) -> Result; fn is_valid_remote(&self, remote: &str) -> Result; fn get_current_tag(&self) -> Result; fn get_current_commit(&self) -> Result; } impl GitTrait for Git { fn is_valid_repository(&self) -> Result { execute(command(GitCommand::IsValidRepository)?) } fn get_local_branch(&self) -> Result { execute(command(GitCommand::LocalBranch)?) } fn get_default_remote(&self) -> Result { execute(command(GitCommand::DefaultRemote)?) } fn get_tracked_remote(&self, tracked: &str) -> Result { execute(command(GitCommand::TrackedRemote(tracked))?) } fn get_upstream_branch(&self, branch: &str) -> Result { execute(command(GitCommand::UpstreamBranch(branch))?) } fn get_default_branch(&self, remote: &str) -> Result { execute(command(GitCommand::DefaultBranch(remote))?) } fn is_valid_remote(&self, remote: &str) -> Result { execute(command(GitCommand::IsValidRemote(remote))?) } fn get_current_tag(&self) -> Result { execute(command(GitCommand::CurrentTag)?) } fn get_current_commit(&self) -> Result { execute(command(GitCommand::CurrentCommit)?) } } fn command(git_command: GitCommand) -> Result { match git_command { GitCommand::IsValidRepository => Command::new("git") .arg("rev-parse") .arg("--is-inside-work-tree") .output(), GitCommand::LocalBranch => Command::new("git") .arg("symbolic-ref") .arg("-q") .arg("--short") .arg("HEAD") .output(), GitCommand::DefaultRemote => Command::new("git") .arg("config") .arg("open.default.remote") .output(), GitCommand::TrackedRemote(branch) => Command::new("git") .arg("config") .arg(format!("branch.{}.remote", branch)) .output(), GitCommand::UpstreamBranch(branch) => Command::new("git") .arg("config") .arg(format!("branch.{}.merge", branch)) .output(), GitCommand::DefaultBranch(remote) => Command::new("git") .arg("rev-parse") .arg("--abbrev-ref") .arg(format!("{}/HEAD", remote)) .output(), GitCommand::IsValidRemote(remote) => Command::new("git") .arg("ls-remote") .arg("--get-url") .arg(remote) .output(), GitCommand::CurrentTag => Command::new("git") .arg("describe") .arg("--tags") .arg("--exact-match") .output(), GitCommand::CurrentCommit => Command::new("git").arg("rev-parse").arg("HEAD").output(), } } fn execute(output: Output) -> Result { if output.status.success() { Ok(GitOutput::Ok(trim(&output.stdout)?)) } else { Ok(GitOutput::Err(trim(&output.stderr)?)) } } fn trim(bytes: &[u8]) -> Result { let mut utf_8_string = String::from(std::str::from_utf8(bytes)?.trim()); if utf_8_string.ends_with('\n') { utf_8_string.pop(); if utf_8_string.ends_with('\r') { utf_8_string.pop(); } } Ok(utf_8_string) } impl Url { pub(crate) fn new(protocol: &str, domain: Domain, path: &str) -> Self { Self { protocol: protocol.into(), domain, path: path.into(), } } } impl Domain { pub(crate) fn from_str(s: &str) -> Self { if s == "bitbucket.org" { Domain::BitBucket } else { Domain::GitHub } } } impl PartialEq for Domain { fn eq(&self, other: &Self) -> bool { self.to_string() == other.to_string() } } impl fmt::Display for Domain { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Domain::GitHub => write!(f, "github.com"), Domain::BitBucket => write!(f, "bitbucket.org"), } } }