feat: Parse URL & (naively) open the url in the browser

pull/1/head
sgoudham 3 years ago
parent ce4138712e
commit 26d9e5bd09
Signed by: hammy
GPG Key ID: 44E818FD5457EEA4

@ -1,4 +1,9 @@
use std::{fmt::format, process::Command}; mod error;
use std::process::Command;
use error::AppError;
use url::Url;
#[derive(Debug)] #[derive(Debug)]
pub struct GitView { pub struct GitView {
@ -23,97 +28,104 @@ impl GitView {
} }
} }
pub fn open_upstream_repository(&mut self) -> Result<(), String> { 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_inside_git_repository()?;
// Retrieve the current branch // Retrieve the current branch
self.populate_branch()?; self.populate_branch()?;
// Retrieve the remote // Retrieve the remote
self.populate_remote()?; self.populate_remote()?;
// Retrieve the upstream branch
let upstream_branch = self.get_upstream_branch()?; // TODO: Figure out how to default to 'master' or 'main' if branch doesn't exist on remote
// Retrieve the remote reference
let remote_ref = self.get_remote_reference()?;
// 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()?;
// Extract protocol, domain and urlpath // Extract protocol, domain and urlpath
let (protocol, domain, urlpath) = self.extract_args_from_git_url(&git_url); let (protocol, domain, urlpath) = self.parse_git_url(&git_url)?;
webbrowser::open(&git_url).expect("Sorry, I couldn't find the default browser!"); // Open the URL
webbrowser::open(
format!("{}://{}{}/tree/{}", protocol, domain, urlpath, remote_ref).as_str(),
)?;
Ok(()) Ok(())
} }
fn is_inside_git_repository(&self) -> Result<(), String> { fn is_inside_git_repository(&self) -> Result<(), AppError> {
let output = Command::new("git") let output = Command::new("git")
.arg("rev-parse") .arg("rev-parse")
.arg("--is-inside-work-tree") .arg("--is-inside-work-tree")
.output() .output()?;
.expect("`git rev-parse --is-inside-work-tree`");
if output.status.success() { if output.status.success() {
Ok(()) Ok(())
} else { } else {
Err(String::from( Err(AppError::MissingGitRepository(String::from(
"Looks like you're not in a valid git repository!", "Looks like you're not in a valid git repository!",
)) )))
} }
} }
fn populate_branch(&mut self) -> Result<(), String> { fn populate_branch(&mut self) -> Result<(), AppError> {
if self.branch.is_none() { if self.branch.is_none() {
let branch = Command::new("git") let branch = Command::new("git")
.arg("symbolic-ref") .arg("symbolic-ref")
.arg("-q") .arg("-q")
.arg("--short") .arg("--short")
.arg("HEAD") .arg("HEAD")
.output() .output()?;
.expect("`git symbolic-ref -q --short HEAD`");
if branch.status.success() { if branch.status.success() {
match stdout_to_string(&branch.stdout) { match stdout_to_string(&branch.stdout) {
Ok(str) => self.branch = Some(str), Ok(str) => self.branch = Some(str),
Err(_) => return Err(String::from("Git branch is not valid UTF-8!")), Err(_) => {
return Err(AppError::InvalidUtf8(String::from(
"Git branch is not valid UTF-8!",
)))
}
} }
} else { } else {
return Err(String::from_utf8_lossy(&branch.stderr).to_string()); return Err(AppError::CommandError(
String::from_utf8_lossy(&branch.stderr).to_string(),
));
} }
} }
Ok(()) Ok(())
} }
fn get_upstream_branch(&mut self) -> Result<String, String> { fn get_remote_reference(&mut self) -> Result<String, AppError> {
let absolute_upstream_branch = Command::new("git") let absolute_upstream_branch = Command::new("git")
.arg("config") .arg("config")
.arg(format!("branch.{}.merge", self.branch.as_ref().unwrap())) .arg(format!("branch.{}.merge", self.branch.as_ref().unwrap()))
.output() .output()?;
.expect("`git config branch.<branch>.merge`");
if absolute_upstream_branch.status.success() { if absolute_upstream_branch.status.success() {
match stdout_to_string(&absolute_upstream_branch.stdout) { match stdout_to_string(&absolute_upstream_branch.stdout) {
Ok(str) => Ok(str.trim_start_matches("refs/heads/").to_string()), Ok(str) => Ok(str.trim_start_matches("refs/heads/").to_string()),
Err(_) => Err(String::from("Git upstream branch is not valid UTF-8!")), Err(_) => Err(AppError::InvalidUtf8(String::from(
"Git upstream branch is not valid UTF-8!",
))),
} }
} else { } else {
Err(format!( Ok(self.branch.as_ref().unwrap().to_string())
"Looks like the branch '{}' doesn't exist upstream!",
self.branch.as_ref().unwrap()
))
} }
} }
/// 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<(), String> { fn populate_remote(&mut self) -> Result<(), 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") let default_remote = Command::new("git")
.arg("config") .arg("config")
.arg("open.default.remote") .arg("open.default.remote")
.output() .output()?;
.expect("`git config open.default.remote`");
if default_remote.status.success() { if default_remote.status.success() {
return match stdout_to_string(&default_remote.stdout) { return match stdout_to_string(&default_remote.stdout) {
@ -121,7 +133,9 @@ impl GitView {
self.remote = Some(str); self.remote = Some(str);
Ok(()) Ok(())
} }
Err(_) => Err(String::from("Git default remote is not valid UTF-8!")), Err(_) => Err(AppError::InvalidUtf8(String::from(
"Git default remote is not valid UTF-8!",
))),
}; };
} }
@ -129,8 +143,7 @@ impl GitView {
let tracked_remote = Command::new("git") let tracked_remote = Command::new("git")
.arg("config") .arg("config")
.arg(format!("branch.{}.remote", self.branch.as_ref().unwrap())) .arg(format!("branch.{}.remote", self.branch.as_ref().unwrap()))
.output() .output()?;
.expect("`git config branch.<branch>.remote`");
if tracked_remote.status.success() { if tracked_remote.status.success() {
return match stdout_to_string(&tracked_remote.stdout) { return match stdout_to_string(&tracked_remote.stdout) {
@ -138,7 +151,9 @@ impl GitView {
self.remote = Some(str); self.remote = Some(str);
Ok(()) Ok(())
} }
Err(_) => Err(String::from("Git tracked remote is not valid UTF-8!")), Err(_) => Err(AppError::InvalidUtf8(String::from(
"Git tracked remote is not valid UTF-8!",
))),
}; };
} }
@ -149,13 +164,12 @@ impl GitView {
Ok(()) Ok(())
} }
fn get_git_url(&self) -> Result<String, String> { fn get_git_url(&self) -> Result<String, AppError> {
let is_valid_remote = Command::new("git") let is_valid_remote = Command::new("git")
.arg("ls-remote") .arg("ls-remote")
.arg("--get-url") .arg("--get-url")
.arg(self.remote.as_ref().unwrap()) .arg(self.remote.as_ref().unwrap())
.output() .output()?;
.expect("`git ls-remote --get-url <remote>`");
if is_valid_remote.status.success() { if is_valid_remote.status.success() {
match stdout_to_string(&is_valid_remote.stdout) { match stdout_to_string(&is_valid_remote.stdout) {
@ -163,61 +177,52 @@ impl GitView {
if &str != self.remote.as_ref().unwrap() { if &str != self.remote.as_ref().unwrap() {
Ok(str) Ok(str)
} else { } else {
Err(format!( Err(AppError::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(String::from("Git URL is not valid UTF-8!")), Err(_) => Err(AppError::InvalidUtf8(String::from(
"Git URL is not valid UTF-8!",
))),
} }
} else { } else {
Err(String::from_utf8_lossy(&is_valid_remote.stderr).to_string()) Err(AppError::CommandError(
String::from_utf8_lossy(&is_valid_remote.stderr).to_string(),
))
} }
} }
fn extract_args_from_git_url(&self, git_url: &str) -> (String, String, String) {
let mut protocol = String::from("https");
let mut domain = String::new();
let mut url_path = String::new();
/* /*
* To enter this if block, the 'git_url' must be in the following formats: * Potential formats:
* - ssh://[user@]host.xz[:port]/path/to/repo.git/ * - ssh://[user@]host.xz[:port]/path/to/repo.git/
* - git://host.xz[:port]/path/to/repo.git/ * - git://host.xz[:port]/path/to/repo.git/
* - http[s]://host.xz[:port]/path/to/repo.git/ * - http[s]://host.xz[:port]/path/to/repo.git/
* - ftp[s]://host.xz[:port]/path/to/repo.git/ * - ftp[s]://host.xz[:port]/path/to/repo.git/
* - [user@]host.xz:path/to/repo.git/
*/ */
if let Some((git_protocol, uri)) = git_url.split_once("://") { fn parse_git_url(&self, git_url: &str) -> Result<(String, String, String), AppError> {
match Url::parse(git_url) {
// If the given URL is 'http', keep it that way Ok(url) => Ok((
if git_protocol == "http" { url.scheme().to_string(),
protocol = String::from("http"); url.host_str()
} .map_or_else(|| "github.com", |host| host)
.to_string(),
// Trim potential username from URI url.path()
let uri = match uri.split_once('@').map(|(_username, url)| url) { .trim_end_matches('/')
Some(path) => path, .trim_end_matches(".git")
None => uri, .to_string(),
}; )),
Err(_) => Err(AppError::InvalidGitUrl(format!(
// Retrieve domain & url_path "Sorry, couldn't parse git url '{}'",
match uri.split_once('/') { git_url
Some((dom, url)) => { ))),
domain.push_str(dom); }
url_path.push_str(url); }
} }
None => todo!(),
} fn stdout_to_string(bytes: &[u8]) -> Result<String, AppError> {
} else {
todo!()
}
(protocol, domain, url_path)
}
}
fn stdout_to_string(bytes: &[u8]) -> Result<String, std::str::Utf8Error> {
let mut utf_8_string = String::from(std::str::from_utf8(bytes)?.trim()); let mut utf_8_string = String::from(std::str::from_utf8(bytes)?.trim());
if utf_8_string.ends_with('\n') { if utf_8_string.ends_with('\n') {
@ -231,23 +236,59 @@ fn stdout_to_string(bytes: &[u8]) -> Result<String, std::str::Utf8Error> {
} }
#[cfg(test)] #[cfg(test)]
mod lib_tests { mod parse_git_url {
use crate::GitView; use crate::{error::AppError, GitView};
#[test] fn instantiate_handler() -> GitView {
fn test_extract_args_from_git_url() { GitView::new(
let handler = GitView::new(
Some(String::from("main")), Some(String::from("main")),
Some(String::from("origin")), Some(String::from("origin")),
Some(String::from("latest")), Some(String::from("latest")),
false, false,
); )
let git_url = "https://github.com/sgoudham/git-view.git"; }
#[test]
fn with_dot_git() -> Result<(), AppError> {
let handler = instantiate_handler();
let git_url_normal = "https://github.com/sgoudham/git-view.git";
let (protocol, domain, urlpath) = handler.extract_args_from_git_url(git_url); let (protocol, domain, urlpath) = handler.parse_git_url(git_url_normal)?;
assert_eq!(protocol, "https"); assert_eq!(protocol, "https");
assert_eq!(domain, "github.com"); assert_eq!(domain, "github.com");
assert_eq!(urlpath, "sgoudham/git-view.git") assert_eq!(urlpath, "/sgoudham/git-view");
Ok(())
}
#[test]
fn with_dot_git_and_trailing_slash() -> Result<(), AppError> {
let handler = instantiate_handler();
let git_url_normal = "https://github.com/sgoudham/git-view.git/";
let (protocol, domain, urlpath) = handler.parse_git_url(git_url_normal)?;
assert_eq!(protocol, "https");
assert_eq!(domain, "github.com");
assert_eq!(urlpath, "/sgoudham/git-view");
Ok(())
}
#[test]
fn invalid_git_url() {
let handler = instantiate_handler();
let git_url_normal = "This isn't a git url";
let error = handler.parse_git_url(git_url_normal);
assert!(error.is_err());
assert_eq!(
error.unwrap_err().print(),
"Sorry, couldn't parse git url 'This isn't a git url'"
);
} }
} }

Loading…
Cancel
Save