feat: implement `--path` option

closes #3
pull/25/head
sgoudham 1 year ago committed by Hamothy
parent 3f9c7bd376
commit 41ad13e569

@ -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! `git-view` alleviates that pain by allowing you to chuck away your mouse!
> (n)vim users rejoice :P > **Note:** <br>
> You should always use `git view -h` instead of `git view --help` as the manpage/html files are **NOT** included.
**_Important Note: You should always use `git view -h` instead of `git view --help` as the manpage/html files are NOT included._**
## Features ## Features
- [x] View Branches, Commits & Issues - [x] View Branches, Commits & Issues
- [x] Custom Suffix - [x] Custom Suffix
- [x] Custom Remote - [x] Custom Remote
- [ ] View Profile - [x] View Current Directory
- [ ] View Current Directory
## Installation ## Installation

@ -35,6 +35,16 @@ fn main() {
.takes_value(true) .takes_value(true)
.display_order(2), .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(
Arg::new("commit") Arg::new("commit")
.long_help("The commit to view git repository on\n[default: current commit]") .long_help("The commit to view git repository on\n[default: current commit]")
@ -43,33 +53,33 @@ fn main() {
.value_name("hash") .value_name("hash")
.default_missing_value("current") .default_missing_value("current")
.conflicts_with_all(&["remote", "branch"]) .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(
Arg::new("suffix") 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') .short('s')
.long("suffix") .long("suffix")
.value_name("suffix") .value_name("suffix")
.takes_value(true) .takes_value(true)
.display_order(4), .display_order(6),
)
.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),
) )
.arg( .arg(
Arg::new("print") Arg::new("print")
.long_help("Don't open browser and print the URL") .long_help("Don't open browser and print the URL")
.short('p')
.long("print") .long("print")
.display_order(6), .display_order(7),
); );
let matches = matches.get_matches(); let matches = matches.get_matches();
@ -79,6 +89,7 @@ fn main() {
matches.value_of("commit"), matches.value_of("commit"),
matches.value_of("suffix"), matches.value_of("suffix"),
matches.value_of("issue"), matches.value_of("issue"),
matches.value_of("path"),
matches.is_present("print"), matches.is_present("print"),
); );

@ -33,6 +33,7 @@ pub(crate) enum GitCommand<'a> {
IsValidRemote(&'a str), IsValidRemote(&'a str),
CurrentTag, CurrentTag,
CurrentCommit, CurrentCommit,
CurrentWorkingDirectory,
} }
pub enum GitOutput { pub enum GitOutput {
@ -51,6 +52,7 @@ pub trait GitTrait {
fn is_valid_remote(&self, remote: &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_tag(&self) -> Result<GitOutput, AppError>;
fn get_current_commit(&self) -> Result<GitOutput, AppError>; fn get_current_commit(&self) -> Result<GitOutput, AppError>;
fn get_current_working_directory(&self) -> Result<GitOutput, AppError>;
} }
impl GitTrait for Git { impl GitTrait for Git {
@ -89,6 +91,10 @@ impl GitTrait for Git {
fn get_current_commit(&self) -> Result<GitOutput, AppError> { fn get_current_commit(&self) -> Result<GitOutput, AppError> {
execute(command(GitCommand::CurrentCommit)?) execute(command(GitCommand::CurrentCommit)?)
} }
fn get_current_working_directory(&self) -> Result<GitOutput, AppError> {
execute(command(GitCommand::CurrentWorkingDirectory)?)
}
} }
fn command(git_command: GitCommand) -> Result<Output, std::io::Error> { fn command(git_command: GitCommand) -> Result<Output, std::io::Error> {
@ -131,6 +137,10 @@ fn command(git_command: GitCommand) -> Result<Output, std::io::Error> {
.arg("--exact-match") .arg("--exact-match")
.output(), .output(),
GitCommand::CurrentCommit => Command::new("git").arg("rev-parse").arg("HEAD").output(), GitCommand::CurrentCommit => Command::new("git").arg("rev-parse").arg("HEAD").output(),
GitCommand::CurrentWorkingDirectory => Command::new("git")
.arg("rev-parse")
.arg("--show-prefix")
.output(),
} }
} }

@ -15,6 +15,7 @@ pub struct GitView<'a> {
commit: Option<&'a str>, commit: Option<&'a str>,
suffix: Option<&'a str>, suffix: Option<&'a str>,
issue: Option<&'a str>, issue: Option<&'a str>,
path: Option<&'a str>,
is_print: bool, is_print: bool,
} }
@ -25,6 +26,7 @@ impl<'a> GitView<'a> {
commit: Option<&'a str>, commit: Option<&'a str>,
suffix: Option<&'a str>, suffix: Option<&'a str>,
issue: Option<&'a str>, issue: Option<&'a str>,
path: Option<&'a str>,
is_print: bool, is_print: bool,
) -> Self { ) -> Self {
Self { Self {
@ -33,6 +35,7 @@ impl<'a> GitView<'a> {
commit, commit,
suffix, suffix,
issue, issue,
path,
is_print, is_print,
} }
} }
@ -215,51 +218,111 @@ impl<'a> GitView<'a> {
git: &impl GitTrait, git: &impl GitTrait,
) -> Result<String, AppError> { ) -> Result<String, AppError> {
let mut open_url = format!("{}://{}/{}", url.protocol, url.domain, url.path); 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 let Some(commit) = self.commit {
if commit == "current" { self.handle_commit_flag(commit, &mut open_url, git)?;
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());
}
return Ok(open_url); 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 fn handle_issue_flag(
let branch_ref = if let Some(issue) = self.issue { &self,
if issue == "branch" { issue: &str,
format!("/issues/{}", capture_digits(remote_ref)) 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 { } else {
format!("/issues/{}", issue) open_url.push_str("/issues");
} }
} else { } 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" { Ok(())
open_url.push_str(&branch_ref); }
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 { } else {
// Edge Case: If the branch is master/main, still append "/issues" open_url.push_str(format!("/tree/{}", commit).as_str());
if self.issue.is_some() { }
open_url.push_str("/issues");
// 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 { 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 start = 0;
let mut end = 0; let mut end = 0;
let mut found = false; let mut found = false;
@ -278,9 +341,9 @@ fn capture_digits(remote_ref: &str) -> &str {
} }
if found { if found {
&remote_ref[start..=end] Some(&remote_ref[start..=end])
} else { } else {
remote_ref None
} }
} }
@ -322,6 +385,7 @@ mod lib_tests {
commit: Option<&'a str>, commit: Option<&'a str>,
suffix: Option<&'a str>, suffix: Option<&'a str>,
issue: Option<&'a str>, issue: Option<&'a str>,
path: Option<&'a str>,
is_print: bool, is_print: bool,
} }
@ -351,6 +415,11 @@ mod lib_tests {
self self
} }
pub(crate) fn with_path(mut self, path: &'a str) -> Self {
self.path = Some(path);
self
}
pub(crate) fn build(self) -> GitView<'a> { pub(crate) fn build(self) -> GitView<'a> {
GitView::new( GitView::new(
self.branch, self.branch,
@ -358,6 +427,7 @@ mod lib_tests {
self.commit, self.commit,
self.suffix, self.suffix,
self.issue, self.issue,
self.path,
self.is_print, self.is_print,
) )
} }
@ -767,11 +837,11 @@ mod lib_tests {
fn is_latest_commit() { fn is_latest_commit() {
let handler = GitView::builder().with_commit("current").build(); let handler = GitView::builder().with_commit("current").build();
let url = Url::new("https", "github.com", "sgoudham/git-view"); 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(); let mut mock = MockGitTrait::default();
mock.expect_get_current_commit() 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); 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); 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("main" ; "main")]
#[test_case("master" ; "master")] #[test_case("master" ; "master")]
fn is_master_or_main(branch: &str) { fn is_master_or_main(branch: &str) {
let handler = GitView::default(); let handler = GitView::default();
let url = Url::new("https", "github.com", "sgoudham/git-view"); 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 mock = MockGitTrait::default();
let actual_final_url = handler.generate_final_url(branch, &url, &mock); 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); assert_eq!(actual_final_url.unwrap(), expected_final_url);
} }
#[test] #[test]
fn is_user_issue() { fn is_user_issue() {
let handler = GitView::builder().with_issue("branch").build(); 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 expected_final_url = "https://github.com/sgoudham/git-view/issues/42";
let mock = MockGitTrait::default(); 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!(actual_final_url.is_ok());
assert_eq!(actual_final_url.unwrap(), expected_final_url); 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); 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")] #[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) { 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 url = Url::new("https", "github.com", "sgoudham/git-view");
let mock = MockGitTrait::default(); let mock = MockGitTrait::default();
@ -882,7 +1002,6 @@ mod lib_tests {
use crate::capture_digits; 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("🥵🥵Hazel🥵-1234🥵🥵", "1234" ; "with emojis")]
#[test_case("TICKET-1234-To-V10", "1234" ; "with multiple issue numbers")] #[test_case("TICKET-1234-To-V10", "1234" ; "with multiple issue numbers")]
#[test_case("TICKET-1234", "1234" ; "with issue number at end")] #[test_case("TICKET-1234", "1234" ; "with issue number at end")]
@ -890,7 +1009,14 @@ mod lib_tests {
#[test_case("1234", "1234" ; "with no letters")] #[test_case("1234", "1234" ; "with no letters")]
fn branch(input: &str, expected_remote_ref: &str) { fn branch(input: &str, expected_remote_ref: &str) {
let actual_remote_ref = capture_digits(input); 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);
} }
} }

Loading…
Cancel
Save