refactor: Perform massive refactor

pull/1/head
sgoudham 2 years ago
parent d37764c5a5
commit 95953b87f3
Signed by: hammy
GPG Key ID: 44E818FD5457EEA4

@ -21,6 +21,7 @@ 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' }
[dev-dependencies] [dev-dependencies]
test-case = { version = '2.1.0' } test-case = { version = '2.1.0' }

@ -62,6 +62,6 @@ fn main() {
); );
if let Err(app_error) = git_view.open_upstream_repository() { if let Err(app_error) = git_view.open_upstream_repository() {
clap_panic!(app_error.print()); clap_panic!(app_error.error_str);
} }
} }

@ -1,34 +1,37 @@
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
pub enum AppError { pub enum ErrorType {
CommandFailedToExecute(String), CommandFailed,
CommandError(String), CommandError,
MissingGitRepository(String), MissingGitRepository,
MissingGitRemote(String), MissingGitRemote,
InvalidGitUrl(String), InvalidGitUrl,
InvalidUtf8(String), InvalidUtf8,
IOError,
}
#[derive(Debug, PartialEq)]
pub struct AppError {
pub error_type: ErrorType,
pub error_str: String,
} }
impl From<std::io::Error> for AppError { impl From<std::io::Error> for AppError {
fn from(error: std::io::Error) -> Self { fn from(error: std::io::Error) -> Self {
AppError::CommandFailedToExecute(error.to_string()) AppError::new(ErrorType::IOError, error.to_string())
} }
} }
impl From<std::str::Utf8Error> for AppError { impl From<std::str::Utf8Error> for AppError {
fn from(error: std::str::Utf8Error) -> Self { fn from(error: std::str::Utf8Error) -> Self {
AppError::InvalidUtf8(error.to_string()) AppError::new(ErrorType::InvalidUtf8, error.to_string())
} }
} }
impl AppError { impl AppError {
pub fn print(&self) -> &String { pub fn new(error_type: ErrorType, error_str: String) -> Self {
match self { Self {
AppError::CommandFailedToExecute(str) error_type,
| AppError::MissingGitRepository(str) error_str,
| AppError::MissingGitRemote(str)
| AppError::CommandError(str)
| AppError::InvalidGitUrl(str)
| AppError::InvalidUtf8(str) => str,
} }
} }
} }

@ -0,0 +1,81 @@
use mockall::automock;
use std::process::{Command, Output};
use crate::error::AppError;
pub(crate) enum Git<'a> {
IsValidRepository,
LocalBranch,
DefaultRemote,
TrackedRemote(&'a str),
UpstreamBranch(&'a str),
IsValidRemote(&'a str),
}
pub(crate) enum GitOutput {
Ok(String),
Err(String),
}
#[automock]
pub(crate) trait GitCommand {
fn execute(&self) -> Result<GitOutput, AppError>;
}
impl<'a> Git<'a> {
fn command(&self) -> Result<Output, std::io::Error> {
match *self {
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> {
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<'a> GitCommand for Git<'a> {
fn execute(&self) -> Result<GitOutput, AppError> {
let command = self.command()?;
if command.status.success() {
Ok(GitOutput::Ok(self.trim(&command.stdout)?))
} else {
Ok(GitOutput::Err(self.trim(&command.stderr)?))
}
}
}

@ -1,8 +1,10 @@
mod error; mod error;
mod git;
use std::process::Command; use std::process::Command;
use error::AppError; use error::{AppError, ErrorType};
use git::{Git, GitCommand, GitOutput};
use url::Url; use url::Url;
#[derive(Debug)] #[derive(Debug)]
@ -30,11 +32,14 @@ impl GitView {
pub fn open_upstream_repository(&mut self) -> Result<(), AppError> { pub fn open_upstream_repository(&mut self) -> Result<(), AppError> {
// Exit out if we're not inside a git repository // Exit out if we're not inside a git repository
self.is_inside_git_repository()?; self.is_valid_repository(&Git::IsValidRepository)?;
// Retrieve the current branch // Retrieve the current branch
self.populate_branch()?; self.populate_branch(&Git::LocalBranch)?;
// Retrieve the remote // Retrieve the remote
self.populate_remote()?; self.remote = Some(self.populate_remote(
&Git::DefaultRemote,
&Git::TrackedRemote(self.branch.as_ref().unwrap()),
)?);
// TODO: Figure out how to default to 'master' or 'main' if branch doesn't exist on remote // TODO: Figure out how to default to 'master' or 'main' if branch doesn't exist on remote
// //
@ -44,158 +49,95 @@ impl GitView {
// - Although, I think that this command isn't foolproof, it might be the best option though without trying to use some command line parsers // - 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()?; let remote_ref =
self.get_remote_reference(&Git::UpstreamBranch(self.branch.as_ref().unwrap()))?;
// 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()?; let git_url = self.get_git_url(&Git::IsValidRemote(self.remote.as_ref().unwrap()))?;
// Extract protocol, domain and urlpath // Extract protocol, domain and urlpath
let (protocol, domain, urlpath) = self.parse_git_url(&git_url)?; let (protocol, domain, urlpath) = self.parse_git_url(&git_url)?;
// Generate final url to open in the web browser
// let final_url = self.generate_final_url(protocol, domain, urlpath);
// Open the URL // Open the URL
webbrowser::open( webbrowser::open(
format!("{}://{}{}/tree/{}", protocol, domain, urlpath, remote_ref).as_str(), format!("{}://{}/{}/tree/{}", protocol, domain, urlpath, remote_ref).as_str(),
)?; )?;
Ok(()) Ok(())
} }
fn is_inside_git_repository(&self) -> Result<(), AppError> { fn is_valid_repository(&self, command: &impl GitCommand) -> Result<(), AppError> {
let output = Command::new("git") match command.execute()? {
.arg("rev-parse") GitOutput::Ok(_) => Ok(()),
.arg("--is-inside-work-tree") GitOutput::Err(_) => Err(AppError::new(
.output()?; ErrorType::MissingGitRepository,
"Looks like you're not in a valid git repository!".to_string(),
if output.status.success() { )),
Ok(())
} else {
Err(AppError::MissingGitRepository(String::from(
"Looks like you're not in a valid git repository!",
)))
} }
} }
fn populate_branch(&mut self) -> Result<(), AppError> { fn populate_branch(&mut self, command: &impl GitCommand) -> Result<(), AppError> {
if self.branch.is_none() { if self.branch.is_none() {
let branch = Command::new("git") match command.execute()? {
.arg("symbolic-ref") GitOutput::Ok(output) => {
.arg("-q") self.branch = Some(output);
.arg("--short")
.arg("HEAD")
.output()?;
if branch.status.success() {
match stdout_to_string(&branch.stdout) {
Ok(str) => self.branch = Some(str),
Err(_) => {
return Err(AppError::InvalidUtf8(String::from(
"Git branch is not valid UTF-8!",
)))
}
}
} else {
return Err(AppError::CommandError(
String::from_utf8_lossy(&branch.stderr).to_string(),
));
}
}
Ok(()) Ok(())
} }
GitOutput::Err(err) => Err(AppError::new(ErrorType::CommandFailed, err)),
fn get_remote_reference(&mut self) -> Result<String, AppError> {
let absolute_upstream_branch = Command::new("git")
.arg("config")
.arg(format!("branch.{}.merge", self.branch.as_ref().unwrap()))
.output()?;
if absolute_upstream_branch.status.success() {
match stdout_to_string(&absolute_upstream_branch.stdout) {
Ok(str) => Ok(str.trim_start_matches("refs/heads/").to_string()),
Err(_) => Err(AppError::InvalidUtf8(String::from(
"Git upstream branch is not valid UTF-8!",
))),
} }
} else { } else {
Ok(self.branch.as_ref().unwrap().to_string()) Ok(())
} }
} }
/// Populates the remote variable within [`GitView`] /// Populates the remote variable within [`GitView`]
/// User Given Remote -> Default Remote in Config -> Tracked Remote -> 'origin' /// User Given Remote -> Default Remote in Config -> Tracked Remote -> 'origin'
fn populate_remote(&mut self) -> Result<(), AppError> { fn populate_remote(
&self,
default_remote: &impl GitCommand,
tracked_remote: &impl GitCommand,
) -> Result<String, 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 // Priority then goes to the default remote
let default_remote = Command::new("git") match default_remote.execute()? {
.arg("config") GitOutput::Ok(def) => Ok(def),
.arg("open.default.remote")
.output()?;
if default_remote.status.success() {
return match stdout_to_string(&default_remote.stdout) {
Ok(str) => {
self.remote = Some(str);
Ok(())
}
Err(_) => Err(AppError::InvalidUtf8(String::from(
"Git default remote is not valid UTF-8!",
))),
};
}
// Priority then goes to the tracked remote // Priority then goes to the tracked remote
let tracked_remote = Command::new("git") GitOutput::Err(_) => match tracked_remote.execute()? {
.arg("config") GitOutput::Ok(tracked) => Ok(tracked),
.arg(format!("branch.{}.remote", self.branch.as_ref().unwrap())) // Default to the 'origin' remote
.output()?; GitOutput::Err(_) => Ok("origin".to_string()),
},
if tracked_remote.status.success() {
return match stdout_to_string(&tracked_remote.stdout) {
Ok(str) => {
self.remote = Some(str);
Ok(())
} }
Err(_) => Err(AppError::InvalidUtf8(String::from( } else {
"Git tracked remote is not valid UTF-8!", Ok(self.remote.as_ref().unwrap().to_string())
))),
};
} }
// Priority then goes to the default 'origin'
self.remote = Some(String::from("origin"));
} }
Ok(()) fn get_remote_reference(&self, command: &impl GitCommand) -> Result<String, AppError> {
match command.execute()? {
GitOutput::Ok(output) => Ok(output.trim_start_matches("refs/heads/").to_string()),
GitOutput::Err(_) => Ok(self.branch.as_ref().unwrap().to_string()),
}
} }
fn get_git_url(&self) -> Result<String, AppError> { fn get_git_url(&self, command: &impl GitCommand) -> Result<String, AppError> {
let is_valid_remote = Command::new("git") match command.execute()? {
.arg("ls-remote") GitOutput::Ok(output) => {
.arg("--get-url") if &output != self.remote.as_ref().unwrap() {
.arg(self.remote.as_ref().unwrap()) Ok(output)
.output()?;
if is_valid_remote.status.success() {
match stdout_to_string(&is_valid_remote.stdout) {
Ok(str) => {
if &str != self.remote.as_ref().unwrap() {
Ok(str)
} else { } else {
Err(AppError::MissingGitRemote(format!( Err(AppError::new(
ErrorType::MissingGitRemote,
format!(
"Looks like your git remote isn't set for '{}'", "Looks like your git remote isn't set for '{}'",
self.remote.as_ref().unwrap(), self.remote.as_ref().unwrap()
))) ),
))
} }
} }
Err(_) => Err(AppError::InvalidUtf8(String::from( GitOutput::Err(err) => Err(AppError::new(ErrorType::CommandFailed, err)),
"Git URL is not valid UTF-8!",
))),
}
} else {
Err(AppError::CommandError(
String::from_utf8_lossy(&is_valid_remote.stderr).to_string(),
))
} }
} }
@ -210,6 +152,7 @@ impl GitView {
fn parse_git_url(&self, git_url: &str) -> Result<(String, String, String), AppError> { fn parse_git_url(&self, git_url: &str) -> Result<(String, String, String), AppError> {
// rust-url cannot parse 'scp-like' urls -> https://github.com/servo/rust-url/issues/220 // rust-url cannot parse 'scp-like' urls -> https://github.com/servo/rust-url/issues/220
// Manually parse the url ourselves // Manually parse the url ourselves
if git_url.contains("://") { if git_url.contains("://") {
match Url::parse(git_url) { match Url::parse(git_url) {
Ok(url) => Ok(( Ok(url) => Ok((
@ -223,10 +166,10 @@ impl GitView {
.trim_end_matches(".git") .trim_end_matches(".git")
.to_string(), .to_string(),
)), )),
Err(_) => Err(AppError::InvalidGitUrl(format!( Err(_) => Err(AppError::new(
"Sorry, couldn't parse git url '{}'", ErrorType::InvalidGitUrl,
git_url format!("Sorry, couldn't parse git url '{}'", git_url),
))), )),
} }
} else { } else {
match git_url.split_once(':') { match git_url.split_once(':') {
@ -234,34 +177,63 @@ impl GitView {
let protocol = "https"; let protocol = "https";
let path = path.trim_end_matches('/').trim_end_matches(".git"); let path = path.trim_end_matches('/').trim_end_matches(".git");
let split_domain = match domain.split_once('@') { let split_domain = match domain.split_once('@') {
Some((_username, dom)) => { Some((_username, dom)) => dom,
dom
}
None => domain, None => domain,
}; };
Ok((protocol.to_string(), split_domain.to_string(), path.to_string())) Ok((
protocol.to_string(),
split_domain.to_string(),
path.to_string(),
))
} }
None => Err(AppError::InvalidGitUrl(format!( None => Err(AppError::new(
"Sorry, couldn't parse git url '{}'", ErrorType::InvalidGitUrl,
git_url format!("Sorry, couldn't parse git url '{}'", git_url),
))), )),
} }
} }
} }
fn generate_final_url(&self, protocol: String, domain: String, urlpath: String) -> String {
todo!();
}
} }
fn stdout_to_string(bytes: &[u8]) -> Result<String, AppError> { #[cfg(test)]
let mut utf_8_string = String::from(std::str::from_utf8(bytes)?.trim()); mod is_valid_repository {
use std::process::{ExitStatus, Output};
use crate::{git::MockGitCommand, GitView};
if utf_8_string.ends_with('\n') { fn instantiate_handler() -> GitView {
utf_8_string.pop(); GitView::new(
if utf_8_string.ends_with('\r') { Some(String::from("main")),
utf_8_string.pop(); Some(String::from("origin")),
Some(String::from("latest")),
false,
)
} }
// #[test]
fn yes() {
let handler = instantiate_handler();
let mut mock = MockGitCommand::new();
let is_valid_repository = handler.is_valid_repository(&mock);
assert!(is_valid_repository.is_ok());
} }
Ok(utf_8_string) // #[test]
fn no() {
let handler = instantiate_handler();
let mut mock = MockGitCommand::new();
mock.expect_execute().never();
let is_valid_repository = handler.is_valid_repository(&mock);
assert!(is_valid_repository.is_err());
}
} }
#[cfg(test)] #[cfg(test)]
@ -313,13 +285,13 @@ mod parse_git_url {
#[test] #[test]
fn invalid_git_url() { fn invalid_git_url() {
let handler = instantiate_handler(); let handler = instantiate_handler();
let git_url_normal = "This isn't a git url"; let git_url_normal = "This isn't a git url";
let error = handler.parse_git_url(git_url_normal); let error = handler.parse_git_url(git_url_normal);
assert!(error.is_err()); assert!(error.is_err());
assert_eq!( assert_eq!(
error.unwrap_err().print(), error.unwrap_err().error_str,
"Sorry, couldn't parse git url 'This isn't a git url'" "Sorry, couldn't parse git url 'This isn't a git url'"
); );
} }

Loading…
Cancel
Save