refactor: Add GitTrait to allow for easier testing

pull/1/head
sgoudham 2 years ago
parent 7be0788dc7
commit f830f914c5
Signed by: hammy
GPG Key ID: 44E818FD5457EEA4

@ -21,7 +21,8 @@ path = "src/lib.rs"
clap = { version = '3.1.18', features = ["cargo"] } clap = { version = '3.1.18', features = ["cargo"] }
url = { version = '2.2.2' } url = { version = '2.2.2' }
webbrowser = { version = '0.7.1' } webbrowser = { version = '0.7.1' }
mockall = { version = '0.11.1' } unicode-segmentation = { version = "1.9.0" }
[dev-dependencies] [dev-dependencies]
test-case = { version = '2.1.0' } test-case = { version = '2.1.0' }
mockall = { version = '0.11.1' }

@ -2,6 +2,7 @@ use std::panic::set_hook;
use clap::{command, crate_authors, crate_description, crate_version, Arg, Command, ErrorKind}; use clap::{command, crate_authors, crate_description, crate_version, Arg, Command, ErrorKind};
use git_view::GitView; use git_view::GitView;
use git_view::Git;
macro_rules! clap_panic { macro_rules! clap_panic {
($e:expr) => { ($e:expr) => {
@ -69,7 +70,7 @@ fn main() {
matches.is_present("print"), matches.is_present("print"),
); );
if let Err(app_error) = git_view.view_repository() { if let Err(app_error) = git_view.view_repository(Git) {
clap_panic!(app_error.error_str); clap_panic!(app_error.error_str);
} }
} }

@ -1,18 +1,24 @@
use core::fmt; use core::fmt;
use mockall::automock;
use std::process::{Command, Output}; use std::process::{Command, Output};
use crate::error::AppError; use crate::error::AppError;
#[cfg(test)]
use mockall::automock;
pub(crate) enum Git<'a> { pub(crate) enum GitCommand<'a> {
IsValidRepository, IsValidRepository,
LocalBranch, LocalBranch,
DefaultRemote, DefaultRemote,
TrackedRemote(&'a str), TrackedRemote(&'a str),
UpstreamBranch(&'a str), UpstreamBranch(&'a str),
IsValidRemote(&'a str), IsValidRemote(&'a str),
CurrentTag,
CurrentCommit,
} }
#[derive(Default)]
pub struct Git;
#[derive(Debug)] #[derive(Debug)]
pub(crate) enum Domain { pub(crate) enum Domain {
GitHub, GitHub,
@ -26,72 +32,114 @@ pub(crate) struct Url {
pub(crate) path: String, pub(crate) path: String,
} }
pub(crate) enum GitOutput { pub enum GitOutput {
Ok(String), Ok(String),
Err(String), Err(String),
} }
#[automock] #[cfg_attr(test, automock)]
pub(crate) trait GitCommand { pub trait GitTrait {
fn execute(&self) -> Result<GitOutput, AppError>; fn is_valid_repository(&self) -> Result<GitOutput, AppError>;
fn get_local_branch(&self) -> Result<GitOutput, AppError>;
fn get_default_remote(&self) -> Result<GitOutput, AppError>;
fn get_tracked_remote(&self, tracked: &str) -> Result<GitOutput, AppError>;
fn get_upstream_branch(&self, branch: &str) -> Result<GitOutput, AppError>;
fn is_valid_remote(&self, remote: &str) -> Result<GitOutput, AppError>;
fn get_current_tag(&self) -> Result<GitOutput, AppError>;
fn get_current_commit(&self) -> Result<GitOutput, AppError>;
} }
impl<'a> Git<'a> { impl GitTrait for Git {
fn command(&self) -> Result<Output, std::io::Error> { fn is_valid_repository(&self) -> Result<GitOutput, AppError> {
match *self { execute(command(GitCommand::IsValidRepository)?)
Git::IsValidRepository => Command::new("git")
.arg("rev-parse")
.arg("--is-inside-work-tree")
.output(),
Git::LocalBranch => Command::new("git")
.arg("symbolic-ref")
.arg("-q")
.arg("--short")
.arg("HEAD")
.output(),
Git::DefaultRemote => Command::new("git")
.arg("config")
.arg("open.default.remote")
.output(),
Git::TrackedRemote(branch) => Command::new("git")
.arg("config")
.arg(format!("branch.{}.remote", branch))
.output(),
Git::UpstreamBranch(branch) => Command::new("git")
.arg("config")
.arg(format!("branch.{}.merge", branch))
.output(),
Git::IsValidRemote(remote) => Command::new("git")
.arg("ls-remote")
.arg("--get-url")
.arg(remote)
.output(),
}
} }
fn trim(&self, bytes: &[u8]) -> Result<String, AppError> { fn get_local_branch(&self) -> Result<GitOutput, AppError> {
let mut utf_8_string = String::from(std::str::from_utf8(bytes)?.trim()); execute(command(GitCommand::LocalBranch)?)
}
if utf_8_string.ends_with('\n') { fn get_default_remote(&self) -> Result<GitOutput, AppError> {
utf_8_string.pop(); execute(command(GitCommand::DefaultRemote)?)
if utf_8_string.ends_with('\r') { }
utf_8_string.pop();
} fn get_tracked_remote(&self, tracked: &str) -> Result<GitOutput, AppError> {
} execute(command(GitCommand::TrackedRemote(tracked))?)
}
fn get_upstream_branch(&self, branch: &str) -> Result<GitOutput, AppError> {
execute(command(GitCommand::UpstreamBranch(branch))?)
}
fn is_valid_remote(&self, remote: &str) -> Result<GitOutput, AppError> {
execute(command(GitCommand::IsValidRemote(remote))?)
}
fn get_current_tag(&self) -> Result<GitOutput, AppError> {
execute(command(GitCommand::CurrentTag)?)
}
Ok(utf_8_string) fn get_current_commit(&self) -> Result<GitOutput, AppError> {
execute(command(GitCommand::CurrentCommit)?)
} }
} }
impl<'a> GitCommand for Git<'a> { fn command(git_command: GitCommand) -> Result<Output, std::io::Error> {
fn execute(&self) -> Result<GitOutput, AppError> { match git_command {
let command = self.command()?; GitCommand::IsValidRepository => Command::new("git")
if command.status.success() { .arg("rev-parse")
Ok(GitOutput::Ok(self.trim(&command.stdout)?)) .arg("--is-inside-work-tree")
} else { .output(),
Ok(GitOutput::Err(self.trim(&command.stderr)?)) 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::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<GitOutput, AppError> {
if output.status.success() {
Ok(GitOutput::Ok(trim(&output.stdout)?))
} else {
Ok(GitOutput::Err(trim(&output.stderr)?))
}
}
fn trim(bytes: &[u8]) -> Result<String, AppError> {
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 { impl Url {

@ -4,7 +4,14 @@ mod git;
use std::borrow::Cow; use std::borrow::Cow;
use error::{AppError, ErrorType}; use error::{AppError, ErrorType};
use git::{Domain, Git, GitCommand, GitOutput, Url}; use git::{Domain, GitOutput, GitTrait, Url};
pub use git::Git;
enum Local<'a> {
Branch(Cow<'a, str>),
NotBranch,
}
#[derive(Debug)] #[derive(Debug)]
pub struct GitView<'a> { pub struct GitView<'a> {
@ -32,26 +39,18 @@ impl<'a> GitView<'a> {
} }
} }
pub fn view_repository(&mut self) -> Result<(), AppError> { pub fn view_repository(&mut self, git: impl GitTrait) -> Result<(), AppError> {
// Exit out if we're not inside a git repository // Exit out if we're not inside a git repository
self.is_valid_repository(&Git::IsValidRepository)?; self.is_valid_repository(&git)?;
// Retrieve the current branch // Retrieve the current local ref (branch or not branch)
let branch = self.populate_branch(&Git::LocalBranch)?; let local = self.get_local_ref(&git)?;
// Retrieve the remote // Retrieve the remote
let remote = self.populate_remote(&Git::DefaultRemote, &Git::TrackedRemote(&branch))?; let remote = self.populate_remote(&local, &git)?;
// TODO: Figure out how to default to 'master' or 'main' if branch doesn't exist on remote
//
// Current theory on how to get it working:
// 1. Run "git checkout" to check if there's a remote branch for your current local one (there should be no output if remote branch doesn't exist)
// 2. Then run "git rev-parse --abbrev-ref <remote>/HEAD" and split on the first '/' to get the current default branch
// - Although, I think that this command isn't foolproof, it might be the best option though without trying to use some command line parsers
// Retrieve the remote reference // Retrieve the remote reference
let remote_ref = self.get_remote_reference(&branch, &Git::UpstreamBranch(&branch))?; let remote_ref = self.get_remote_reference(&local, &git)?;
// Retrieve the full git_url // Retrieve the full git_url
// e.g https://github.com/sgoudham/git-view.git // e.g https://github.com/sgoudham/git-view.git
let git_url = self.get_git_url(&remote, &Git::IsValidRemote(&remote))?; let git_url = self.get_git_url(&remote, &git)?;
// Extract protocol, domain and urlpath // Extract protocol, domain and urlpath
let url = self.parse_git_url(&git_url)?; let url = self.parse_git_url(&git_url)?;
// Generate final url to open in the web browser // Generate final url to open in the web browser
@ -67,8 +66,8 @@ impl<'a> GitView<'a> {
Ok(()) Ok(())
} }
fn is_valid_repository(&self, command: &impl GitCommand) -> Result<(), AppError> { fn is_valid_repository(&self, git: &impl GitTrait) -> Result<(), AppError> {
match command.execute()? { match git.is_valid_repository()? {
GitOutput::Ok(_) => Ok(()), GitOutput::Ok(_) => Ok(()),
GitOutput::Err(_) => Err(AppError::new( GitOutput::Err(_) => Err(AppError::new(
ErrorType::MissingGitRepository, ErrorType::MissingGitRepository,
@ -77,16 +76,14 @@ impl<'a> GitView<'a> {
} }
} }
fn populate_branch(&self, command: &impl GitCommand) -> Result<Cow<'_, str>, AppError> { fn get_local_ref(&self, git: &impl GitTrait) -> Result<Local, AppError> {
if self.branch.is_none() { if self.branch.is_none() {
match command.execute()? { match git.get_local_branch()? {
GitOutput::Ok(output) => Ok(Cow::Owned(output)), GitOutput::Ok(output) => Ok(Local::Branch(Cow::Owned(output))),
// Don't error on this, instead make a new enum Branch/NotBranch and match on it GitOutput::Err(_) => Ok(Local::NotBranch),
// later
GitOutput::Err(err) => Err(AppError::new(ErrorType::CommandFailed, err)),
} }
} else { } else {
Ok(Cow::Borrowed(self.branch.as_ref().unwrap())) Ok(Local::Branch(Cow::Borrowed(self.branch.as_ref().unwrap())))
} }
} }
@ -94,20 +91,25 @@ impl<'a> GitView<'a> {
/// User Given Remote -> Default Remote in Config -> Tracked Remote -> 'origin' /// User Given Remote -> Default Remote in Config -> Tracked Remote -> 'origin'
fn populate_remote( fn populate_remote(
&self, &self,
default_remote: &impl GitCommand, local: &Local,
tracked_remote: &impl GitCommand, git: &impl GitTrait,
) -> Result<Cow<'_, str>, AppError> { ) -> Result<Cow<'_, str>, AppError> {
// Priority goes to user given remote // Priority goes to user given remote
if self.remote.is_none() { if self.remote.is_none() {
// Priority then goes to the default remote match local {
match default_remote.execute()? { Local::Branch(branch) => {
GitOutput::Ok(def) => Ok(Cow::Owned(def)), // Priority then goes to the default remote
// Priority then goes to the tracked remote match git.get_default_remote()? {
GitOutput::Err(_) => match tracked_remote.execute()? { GitOutput::Ok(def) => Ok(Cow::Owned(def)),
GitOutput::Ok(tracked) => Ok(Cow::Owned(tracked)), // Priority then goes to the tracked remote
// Default to the 'origin' remote GitOutput::Err(_) => match git.get_tracked_remote(branch)? {
GitOutput::Err(_) => Ok(Cow::Owned("origin".into())), GitOutput::Ok(tracked) => Ok(Cow::Owned(tracked)),
}, // Default to the 'origin' remote
GitOutput::Err(_) => Ok(Cow::Owned("origin".into())),
},
}
}
Local::NotBranch => Ok(Cow::Owned("origin".into())),
} }
} else { } else {
Ok(Cow::Borrowed(self.remote.as_ref().unwrap())) Ok(Cow::Borrowed(self.remote.as_ref().unwrap()))
@ -116,28 +118,49 @@ impl<'a> GitView<'a> {
fn get_remote_reference( fn get_remote_reference(
&self, &self,
branch: &'a str, local: &'a Local,
command: &impl GitCommand, git: &impl GitTrait,
) -> Result<Cow<'a, str>, AppError> { ) -> Result<Cow<'a, str>, AppError> {
match command.execute()? { match local {
GitOutput::Ok(output) => Ok(Cow::Owned( Local::Branch(branch) => {
output.trim_start_matches("refs/heads/").to_string(), match git.get_upstream_branch(branch)? {
)), GitOutput::Ok(output) => Ok(Cow::Owned(
// So as explained above, branch might not exist either output.trim_start_matches("refs/heads/").to_string(),
// Instead of returning branch this early, match on the aforementioned enum above )),
// // If retrieving the upstream_branch fails, that means that there is no valid
// Branch: // upstream branch (surprise surprise)
// Just do what you're doing already and `Cow::Borrowed(branch)` //
// // When there's no valid remote branch, we should default to the repository's
// NotBranch: // default branch, for the vast majority of cases, this will be either "main"
// Check to see if the user is on a tag or a specific commit hash. If all // or "master" but it could be different for whatever INSANE person has changed
// else fails THEN error out // their default to differ from those two terms.
GitOutput::Err(_) => Ok(Cow::Borrowed(branch)), //
// Thankfully, we have a command 'git rev-parse --abbrev-ref <remote>/HEAD'
// to let us retrieve the default branch (we also need to split on the first /
// encountered and take the second split part)
//
// However, it's a bit dodgy so we can't guarantee it will work everytime. If
// the command 'git rev-parse --abbrev-ref <remote>/HEAD' fails, we should just
// default to the local branch and the user will just have to suck it up and
// deal with the 404 error that they will probably get.
GitOutput::Err(_) => Ok(Cow::Borrowed(branch)),
}
}
// Priority is given to the current tag
Local::NotBranch => match git.get_current_tag()? {
GitOutput::Ok(tag) => Ok(Cow::Owned(tag)),
// Priority is then given the current commit
GitOutput::Err(_) => match git.get_current_commit()? {
GitOutput::Ok(commit_hash) => Ok(Cow::Owned(commit_hash)),
// Error out if even the current commit could not be found
GitOutput::Err(err) => Err(AppError::new(ErrorType::CommandFailed, err)),
},
},
} }
} }
fn get_git_url(&self, remote: &str, command: &impl GitCommand) -> Result<String, AppError> { fn get_git_url(&self, remote: &str, git: &impl GitTrait) -> Result<String, AppError> {
match command.execute()? { match git.is_valid_remote(remote)? {
GitOutput::Ok(output) => { GitOutput::Ok(output) => {
if output != remote { if output != remote {
Ok(output) Ok(output)
@ -200,6 +223,12 @@ impl<'a> GitView<'a> {
} }
fn generate_final_url(&self, remote_ref: &str, url: &Url) -> String { fn generate_final_url(&self, remote_ref: &str, url: &Url) -> String {
// if self.commit.unwrap() == "latest" {
// ()
// } else {
// ()
// }
let branch_ref = match &url.domain { let branch_ref = match &url.domain {
Domain::GitHub => { Domain::GitHub => {
if self.is_issue { if self.is_issue {
@ -213,12 +242,6 @@ impl<'a> GitView<'a> {
let mut open_url = format!("{}://{}/{}", url.protocol, url.domain, url.path); let mut open_url = format!("{}://{}/{}", url.protocol, url.domain, url.path);
// if self.commit.unwrap() == "latest" {
// ()
// } else {
// ()
// }
if remote_ref == "master" || remote_ref == "main" { if remote_ref == "master" || remote_ref == "main" {
open_url open_url
} else { } else {
@ -264,7 +287,7 @@ mod lib_tests {
mod is_valid_repository { mod is_valid_repository {
use crate::{ use crate::{
error::ErrorType, error::ErrorType,
git::{GitOutput, MockGitCommand}, git::{GitOutput, MockGitTrait},
lib_tests::instantiate_handler, lib_tests::instantiate_handler,
}; };
@ -272,8 +295,8 @@ mod lib_tests {
fn yes() { fn yes() {
let handler = instantiate_handler(); let handler = instantiate_handler();
let mut mock = MockGitCommand::new(); let mut mock = MockGitTrait::default();
mock.expect_execute() mock.expect_is_valid_repository()
.returning(|| Ok(GitOutput::Ok("Valid".to_owned()))); .returning(|| Ok(GitOutput::Ok("Valid".to_owned())));
let is_valid_repository = handler.is_valid_repository(&mock); let is_valid_repository = handler.is_valid_repository(&mock);
@ -284,8 +307,8 @@ mod lib_tests {
#[test] #[test]
fn no() { fn no() {
let handler = instantiate_handler(); let handler = instantiate_handler();
let mut mock = MockGitCommand::new(); let mut mock = MockGitTrait::default();
mock.expect_execute() mock.expect_is_valid_repository()
.returning(|| Ok(GitOutput::Err("Error".to_owned()))); .returning(|| Ok(GitOutput::Err("Error".to_owned())));
let is_valid_repository = handler.is_valid_repository(&mock); let is_valid_repository = handler.is_valid_repository(&mock);
@ -301,6 +324,18 @@ mod lib_tests {
} }
} }
mod get_local_ref {
#[test]
fn user_given_branch() {}
#[test]
fn is_branch() {}
#[test]
fn is_not_branch() {}
}
mod parse_git_url { mod parse_git_url {
use crate::{error::AppError, lib_tests::instantiate_handler}; use crate::{error::AppError, lib_tests::instantiate_handler};
use test_case::test_case; use test_case::test_case;

Loading…
Cancel
Save