From 9011c18d4952a404b4638190b25293c25f634238 Mon Sep 17 00:00:00 2001 From: sgoudham Date: Tue, 7 Jun 2022 03:00:19 +0100 Subject: [PATCH] feat: Parse URL in a robust way & start adding tests --- src/lib.rs | 232 +++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 200 insertions(+), 32 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 30ac6f5..166a44b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,18 +1,24 @@ -use std::process::Command; +use std::{fmt::format, process::Command}; +#[derive(Debug)] pub struct GitView { - remote: String, + remote: Option, branch: Option, - is_commit: bool, + commit: Option, is_print: bool, } impl GitView { - pub fn new(branch: Option, remote: String, is_commit: bool, is_print: bool) -> Self { + pub fn new( + branch: Option, + remote: Option, + commit: Option, + is_print: bool, + ) -> Self { Self { remote, branch, - is_commit, + commit, is_print, } } @@ -22,23 +28,36 @@ impl GitView { self.is_inside_git_repository()?; // Retrieve the current branch self.populate_branch()?; + // Retrieve the remote + self.populate_remote()?; + // Retrieve the upstream branch + let upstream_branch = self.get_upstream_branch()?; + // Retrieve the full git_url + // e.g https://github.com/sgoudham/git-view.git + let git_url = self.get_git_url()?; + // Extract protocol, domain and urlpath + let (protocol, domain, urlpath) = self.extract_args_from_git_url(&git_url); - let git_url = Command::new("git") - .args(["ls-remote", "--get-url", &self.remote]) + webbrowser::open(&git_url).expect("Sorry, I couldn't find the default browser!"); + + Ok(()) + } + + fn is_inside_git_repository(&self) -> Result<(), String> { + let output = Command::new("git") + .arg("rev-parse") + .arg("--is-inside-work-tree") .output() - .expect("`git ls-remote --get-url `"); + .expect("`git rev-parse --is-inside-work-tree`"); - if git_url.status.success() { - webbrowser::open(unsafe { - trim_string(std::str::from_utf8_unchecked(&git_url.stdout)) - }) - .expect("Couldn't find the browser!"); + if output.status.success() { + Ok(()) } else { - return Err(unsafe { String::from_utf8_unchecked(git_url.stderr) }); + Err(String::from( + "Looks like you're not in a valid git repository!", + )) } - - Ok(()) } fn populate_branch(&mut self) -> Result<(), String> { @@ -52,34 +71,183 @@ impl GitView { .expect("`git symbolic-ref -q --short HEAD`"); if branch.status.success() { - let branch_str = unsafe { std::str::from_utf8_unchecked(&branch.stdout) }; - self.branch = Some(String::from(trim_string(branch_str))); + match stdout_to_string(&branch.stdout) { + Ok(str) => self.branch = Some(str), + Err(_) => return Err(String::from("Git branch is not valid UTF-8!")), + } } else { - return Err(unsafe { String::from_utf8_unchecked(branch.stderr) }); + return Err(String::from_utf8_lossy(&branch.stderr).to_string()); } } Ok(()) } - fn is_inside_git_repository(&self) -> Result<(), String> { - let output = Command::new("git") - .arg("rev-parse") - .arg("--is-inside-work-tree") + fn get_upstream_branch(&mut self) -> Result { + let absolute_upstream_branch = Command::new("git") + .arg("config") + .arg(format!("branch.{}.merge", self.branch.as_ref().unwrap())) .output() - .expect("`git rev-parse --is-inside-work-tree`"); + .expect("`git config branch..merge`"); - if output.status.success() { - Ok(()) + 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(String::from("Git upstream branch is not valid UTF-8!")), + } + } else { + Err(format!( + "Looks like the branch '{}' doesn't exist upstream!", + self.branch.as_ref().unwrap() + )) + } + } + + /// Populates the remote variable within [`GitView`] + /// User Given Remote -> Default Remote in Config -> Tracked Remote -> 'origin' + fn populate_remote(&mut self) -> Result<(), String> { + // Priority goes to user given remote + if self.remote.is_none() { + // Priority then goes to the default remote + let default_remote = Command::new("git") + .arg("config") + .arg("open.default.remote") + .output() + .expect("`git config open.default.remote`"); + + if default_remote.status.success() { + return match stdout_to_string(&default_remote.stdout) { + Ok(str) => { + self.remote = Some(str); + Ok(()) + } + Err(_) => Err(String::from("Git default remote is not valid UTF-8!")), + }; + } + + // Priority then goes to the tracked remote + let tracked_remote = Command::new("git") + .arg("config") + .arg(format!("branch.{}.remote", self.branch.as_ref().unwrap())) + .output() + .expect("`git config branch..remote`"); + + if tracked_remote.status.success() { + return match stdout_to_string(&tracked_remote.stdout) { + Ok(str) => { + self.remote = Some(str); + Ok(()) + } + Err(_) => Err(String::from("Git tracked remote is not valid UTF-8!")), + }; + } + + // Priority then goes to the default 'origin' + self.remote = Some(String::from("origin")); + } + + Ok(()) + } + + fn get_git_url(&self) -> Result { + let is_valid_remote = Command::new("git") + .arg("ls-remote") + .arg("--get-url") + .arg(self.remote.as_ref().unwrap()) + .output() + .expect("`git ls-remote --get-url `"); + + 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 { + Err(format!( + "Looks like your git remote isn't set for '{}'", + self.remote.as_ref().unwrap(), + )) + } + } + Err(_) => Err(String::from("Git URL is not valid UTF-8!")), + } + } else { + Err(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: + * - ssh://[user@]host.xz[:port]/path/to/repo.git/ + * - git://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/ + */ + if let Some((git_protocol, uri)) = git_url.split_once("://") { + + // If the given URL is 'http', keep it that way + if git_protocol == "http" { + protocol = String::from("http"); + } + + // Trim potential username from URI + let uri = match uri.split_once('@').map(|(_username, url)| url) { + Some(path) => path, + None => uri, + }; + + // Retrieve domain & url_path + match uri.split_once('/') { + Some((dom, url)) => { + domain.push_str(dom); + url_path.push_str(url); + } + None => todo!(), + } } else { - Err(String::from("Looks like you're not in a valid git repository!")) + todo!() + } + + (protocol, domain, url_path) + } +} + +fn stdout_to_string(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) } -fn trim_string(str: &str) -> &str { - str.trim() - .strip_suffix("\r\n") - .or_else(|| str.strip_suffix('\n')) - .unwrap_or(str) +#[cfg(test)] +mod lib_tests { + use crate::GitView; + + #[test] + fn test_extract_args_from_git_url() { + let handler = GitView::new( + Some(String::from("main")), + Some(String::from("origin")), + Some(String::from("latest")), + false, + ); + let git_url = "https://github.com/sgoudham/git-view.git"; + + let (protocol, domain, urlpath) = handler.extract_args_from_git_url(git_url); + + assert_eq!(protocol, "https"); + assert_eq!(domain, "github.com"); + assert_eq!(urlpath, "sgoudham/git-view.git") + } }