diff --git a/README.md b/README.md index 59fc347..b32b9b6 100644 --- a/README.md +++ b/README.md @@ -14,17 +14,15 @@ Are you _**also**_ frustrated from moving your hands away from the keyboard to v `git-view` alleviates that pain by allowing you to chuck away your mouse! -> (n)vim users rejoice :P - -**_Important Note: You should always use `git view -h` instead of `git view --help` as the manpage/html files are NOT included._** +> **Note:**
+> You should always use `git view -h` instead of `git view --help` as the manpage/html files are **NOT** included. ## Features - [x] View Branches, Commits & Issues - [x] Custom Suffix - [x] Custom Remote -- [ ] View Profile -- [ ] View Current Directory +- [x] View Current Directory ## Installation diff --git a/src/bin/git-view.rs b/src/bin/git-view.rs index 07c6942..295980b 100644 --- a/src/bin/git-view.rs +++ b/src/bin/git-view.rs @@ -35,6 +35,16 @@ fn main() { .takes_value(true) .display_order(2), ) + .arg( + Arg::new("issue") + .long_help("The issue number to view on the git repository\n[default: open issue from current branch]") + .short('i') + .long("issue") + .default_missing_value("branch") + .conflicts_with("commit") + .takes_value(true) + .display_order(3), + ) .arg( Arg::new("commit") .long_help("The commit to view git repository on\n[default: current commit]") @@ -43,33 +53,33 @@ fn main() { .value_name("hash") .default_missing_value("current") .conflicts_with_all(&["remote", "branch"]) - .display_order(3), + .display_order(4), + ) + .arg( + Arg::new("path") + .long_help("The directory/file to view on the git repository\n[default: current working directory]") + .short('p') + .long("path") + .default_missing_value("current-working-directory") + .conflicts_with("issue") + .takes_value(true) + .value_hint(clap::ValueHint::AnyPath) + .display_order(5), ) .arg( Arg::new("suffix") - .long_help("A suffix to append onto the git repository URL") + .long_help("A suffix to append onto the base git repository URL") .short('s') .long("suffix") .value_name("suffix") .takes_value(true) - .display_order(4), - ) - .arg( - Arg::new("issue") - .long_help("The issue number to view\n[default: open issue from remote branch]") - .short('i') - .long("issue") - .default_missing_value("branch") - .conflicts_with("commit") - .takes_value(true) - .display_order(5), + .display_order(6), ) .arg( Arg::new("print") .long_help("Don't open browser and print the URL") - .short('p') .long("print") - .display_order(6), + .display_order(7), ); let matches = matches.get_matches(); @@ -79,6 +89,7 @@ fn main() { matches.value_of("commit"), matches.value_of("suffix"), matches.value_of("issue"), + matches.value_of("path"), matches.is_present("print"), ); diff --git a/src/git.rs b/src/git.rs index 83d4e7d..4a700f1 100644 --- a/src/git.rs +++ b/src/git.rs @@ -33,6 +33,7 @@ pub(crate) enum GitCommand<'a> { IsValidRemote(&'a str), CurrentTag, CurrentCommit, + CurrentWorkingDirectory, } pub enum GitOutput { @@ -51,6 +52,7 @@ pub trait GitTrait { fn is_valid_remote(&self, remote: &str) -> Result; fn get_current_tag(&self) -> Result; fn get_current_commit(&self) -> Result; + fn get_current_working_directory(&self) -> Result; } impl GitTrait for Git { @@ -89,6 +91,10 @@ impl GitTrait for Git { fn get_current_commit(&self) -> Result { execute(command(GitCommand::CurrentCommit)?) } + + fn get_current_working_directory(&self) -> Result { + execute(command(GitCommand::CurrentWorkingDirectory)?) + } } fn command(git_command: GitCommand) -> Result { @@ -131,6 +137,10 @@ fn command(git_command: GitCommand) -> Result { .arg("--exact-match") .output(), GitCommand::CurrentCommit => Command::new("git").arg("rev-parse").arg("HEAD").output(), + GitCommand::CurrentWorkingDirectory => Command::new("git") + .arg("rev-parse") + .arg("--show-prefix") + .output(), } } diff --git a/src/lib.rs b/src/lib.rs index ae75197..08e4c24 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,6 +15,7 @@ pub struct GitView<'a> { commit: Option<&'a str>, suffix: Option<&'a str>, issue: Option<&'a str>, + path: Option<&'a str>, is_print: bool, } @@ -25,6 +26,7 @@ impl<'a> GitView<'a> { commit: Option<&'a str>, suffix: Option<&'a str>, issue: Option<&'a str>, + path: Option<&'a str>, is_print: bool, ) -> Self { Self { @@ -33,6 +35,7 @@ impl<'a> GitView<'a> { commit, suffix, issue, + path, is_print, } } @@ -215,51 +218,111 @@ impl<'a> GitView<'a> { git: &impl GitTrait, ) -> Result { let mut open_url = format!("{}://{}/{}", url.protocol, url.domain, url.path); + let escaped_remote_ref = escape_ascii_chars(remote_ref); - // Handle commit flag + if let Some(issue) = self.issue { + self.handle_issue_flag(issue, &escaped_remote_ref, &mut open_url)?; + return Ok(open_url); + } if let Some(commit) = self.commit { - if commit == "current" { - let commit_hash = match git.get_current_commit()? { - GitOutput::Ok(hash) => Ok(hash), - GitOutput::Err(err) => Err(AppError::new(ErrorType::CommandFailed, err)), - }?; - open_url.push_str(format!("/tree/{}", commit_hash).as_str()); - } else { - open_url.push_str(format!("/tree/{}", commit).as_str()); - } - + self.handle_commit_flag(commit, &mut open_url, git)?; return Ok(open_url); } + if let Some(path) = self.path { + let prefix = format!("/tree/{}/", escaped_remote_ref); + self.handle_path_flag(prefix.as_str(), path, &mut open_url, git)?; + return Ok(open_url); + } + + println!("{escaped_remote_ref}"); + open_url.push_str(format!("/tree/{}", escaped_remote_ref).as_str()); + self.handle_suffix_flag(&mut open_url); + + Ok(open_url) + } - // Handle issue flag - let branch_ref = if let Some(issue) = self.issue { - if issue == "branch" { - format!("/issues/{}", capture_digits(remote_ref)) + fn handle_issue_flag( + &self, + issue: &str, + remote_ref: &str, + open_url: &mut String, + ) -> Result<(), AppError> { + if issue == "branch" { + if let Some(issue_num) = capture_digits(remote_ref) { + open_url.push_str(format!("/issues/{}", issue_num).as_str()); } else { - format!("/issues/{}", issue) + open_url.push_str("/issues"); } } else { - format!("/tree/{}", escape_ascii_chars(remote_ref)) - }; + open_url.push_str(format!("/issues/{}", issue).as_str()); + } + + // no reason why suffix can't be appended after issue + self.handle_suffix_flag(open_url); - if remote_ref != "master" && remote_ref != "main" { - open_url.push_str(&branch_ref); + Ok(()) + } + + fn handle_commit_flag( + &self, + commit: &str, + open_url: &mut String, + git: &impl GitTrait, + ) -> Result<(), AppError> { + if commit == "current" { + match git.get_current_commit()? { + GitOutput::Ok(hash) => { + open_url.push_str(format!("/tree/{}", hash).as_str()); + } + GitOutput::Err(err) => return Err(AppError::new(ErrorType::CommandFailed, err)), + }; } else { - // Edge Case: If the branch is master/main, still append "/issues" - if self.issue.is_some() { - open_url.push_str("/issues"); + open_url.push_str(format!("/tree/{}", commit).as_str()); + } + + // path can still be appended after commit hash + if let Some(path) = self.path { + self.handle_path_flag("/", path, open_url, git)?; + } + + Ok(()) + } + + fn handle_path_flag( + &self, + prefix: &str, + path: &str, + open_url: &mut String, + git: &impl GitTrait, + ) -> Result<(), AppError> { + if path == "current-working-directory" { + match git.get_current_working_directory()? { + GitOutput::Ok(cwd) => { + // If the current working directory is not the root of the repo, append it + if !cwd.is_empty() { + open_url.push_str(format!("{}/{}", prefix, cwd).as_str()); + } + } + GitOutput::Err(err) => return Err(AppError::new(ErrorType::CommandFailed, err)), } + } else { + open_url.push_str(format!("{}{}", prefix, path).as_str()); } + // suffix can still be appended after path + self.handle_suffix_flag(open_url); + + Ok(()) + } + + fn handle_suffix_flag(&self, open_url: &mut String) { if let Some(suffix) = self.suffix { - open_url.push_str(suffix); + open_url.push_str(format!("/{}", suffix).as_str()); } - - Ok(open_url) } } -fn capture_digits(remote_ref: &str) -> &str { +fn capture_digits(remote_ref: &str) -> Option<&str> { let mut start = 0; let mut end = 0; let mut found = false; @@ -278,9 +341,9 @@ fn capture_digits(remote_ref: &str) -> &str { } if found { - &remote_ref[start..=end] + Some(&remote_ref[start..=end]) } else { - remote_ref + None } } @@ -322,6 +385,7 @@ mod lib_tests { commit: Option<&'a str>, suffix: Option<&'a str>, issue: Option<&'a str>, + path: Option<&'a str>, is_print: bool, } @@ -351,6 +415,11 @@ mod lib_tests { self } + pub(crate) fn with_path(mut self, path: &'a str) -> Self { + self.path = Some(path); + self + } + pub(crate) fn build(self) -> GitView<'a> { GitView::new( self.branch, @@ -358,6 +427,7 @@ mod lib_tests { self.commit, self.suffix, self.issue, + self.path, self.is_print, ) } @@ -767,11 +837,11 @@ mod lib_tests { fn is_latest_commit() { let handler = GitView::builder().with_commit("current").build(); let url = Url::new("https", "github.com", "sgoudham/git-view"); - let expected_final_url = "https://github.com/sgoudham/git-view/tree/commit_hash"; + let expected_final_url = "https://github.com/sgoudham/git-view/tree/eafdb9a"; let mut mock = MockGitTrait::default(); mock.expect_get_current_commit() - .returning(|| Ok(GitOutput::Ok("commit_hash".into()))); + .returning(|| Ok(GitOutput::Ok("eafdb9a".into()))); let actual_final_url = handler.generate_final_url("main", &url, &mock); @@ -795,12 +865,32 @@ mod lib_tests { assert_eq!(actual_final_url.unwrap(), expected_final_url); } + #[test] + fn is_latest_commit_with_path_current_working_directory() { + let handler = GitView::builder() + .with_commit("current") + .with_path("src/main.rs") + .build(); + let url = Url::new("https", "github.com", "sgoudham/git-view"); + let expected_final_url = + "https://github.com/sgoudham/git-view/tree/eafdb9a/src/main.rs"; + + let mut mock = MockGitTrait::default(); + mock.expect_get_current_commit() + .returning(|| Ok(GitOutput::Ok("eafdb9a".into()))); + + let actual_final_url = handler.generate_final_url("main", &url, &mock); + + assert!(actual_final_url.is_ok()); + assert_eq!(actual_final_url.unwrap(), expected_final_url); + } + #[test_case("main" ; "main")] #[test_case("master" ; "master")] fn is_master_or_main(branch: &str) { let handler = GitView::default(); let url = Url::new("https", "github.com", "sgoudham/git-view"); - let expected_final_url = "https://github.com/sgoudham/git-view"; + let expected_final_url = format!("https://github.com/sgoudham/git-view/tree/{branch}"); let mock = MockGitTrait::default(); let actual_final_url = handler.generate_final_url(branch, &url, &mock); @@ -823,7 +913,6 @@ mod lib_tests { assert_eq!(actual_final_url.unwrap(), expected_final_url); } - #[test] fn is_user_issue() { let handler = GitView::builder().with_issue("branch").build(); @@ -844,7 +933,7 @@ mod lib_tests { let expected_final_url = "https://github.com/sgoudham/git-view/issues/42"; let mock = MockGitTrait::default(); - let actual_final_url = handler.generate_final_url("mock ref", &url, &mock); + let actual_final_url = handler.generate_final_url("main", &url, &mock); assert!(actual_final_url.is_ok()); assert_eq!(actual_final_url.unwrap(), expected_final_url); @@ -863,10 +952,41 @@ mod lib_tests { assert_eq!(actual_final_url.unwrap(), expected_final_url); } - #[test_case("main", "https://github.com/sgoudham/git-view/releases" ; "with_branch_main")] + #[test] + fn is_user_path() { + let handler = GitView::builder().with_path("src/main.rs").build(); + let url = Url::new("https", "github.com", "sgoudham/git-view"); + let expected_final_url = "https://github.com/sgoudham/git-view/tree/main/src/main.rs"; + let mock = MockGitTrait::default(); + + let actual_final_url = handler.generate_final_url("main", &url, &mock); + + assert!(actual_final_url.is_ok()); + assert_eq!(actual_final_url.unwrap(), expected_final_url); + } + + #[test] + fn is_path_at_repo_root() { + let handler = GitView::builder() + .with_path("current-working-directory") + .build(); + let url = Url::new("https", "github.com", "sgoudham/git-view"); + let expected_final_url = "https://github.com/sgoudham/git-view"; + + let mut mock = MockGitTrait::default(); + mock.expect_get_current_working_directory() + .returning(|| Ok(GitOutput::Ok("".into()))); + + let actual_final_url = handler.generate_final_url("main", &url, &mock); + + assert!(actual_final_url.is_ok()); + assert_eq!(actual_final_url.unwrap(), expected_final_url); + } + + #[test_case("main", "https://github.com/sgoudham/git-view/tree/main/releases" ; "with_branch_main")] #[test_case("dev", "https://github.com/sgoudham/git-view/tree/dev/releases" ; "with_branch_dev")] fn with_suffix(remote_ref: &str, expected_final_url: &str) { - let handler = GitView::builder().with_suffix("/releases").build(); + let handler = GitView::builder().with_suffix("releases").build(); let url = Url::new("https", "github.com", "sgoudham/git-view"); let mock = MockGitTrait::default(); @@ -882,7 +1002,6 @@ mod lib_tests { use crate::capture_digits; - #[test_case("TICKET-WITH-NO-NUMBERS", "TICKET-WITH-NO-NUMBERS" ; "with no numbers")] #[test_case("🥵🥵Hazel🥵-1234🥵🥵", "1234" ; "with emojis")] #[test_case("TICKET-1234-To-V10", "1234" ; "with multiple issue numbers")] #[test_case("TICKET-1234", "1234" ; "with issue number at end")] @@ -890,7 +1009,14 @@ mod lib_tests { #[test_case("1234", "1234" ; "with no letters")] fn branch(input: &str, expected_remote_ref: &str) { let actual_remote_ref = capture_digits(input); - assert_eq!(actual_remote_ref, expected_remote_ref); + assert_eq!(actual_remote_ref, Some(expected_remote_ref)); + } + + #[test] + fn branch_no_numbers() { + let input = "TICKET-WITH-NO-NUMBERS"; + let actual_remote_ref = capture_digits(input); + assert_eq!(actual_remote_ref, None); } }