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!
> (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:** <br>
> 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

@ -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"),
);

@ -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<GitOutput, AppError>;
fn get_current_tag(&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 {
@ -89,6 +91,10 @@ impl GitTrait for Git {
fn get_current_commit(&self) -> Result<GitOutput, AppError> {
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> {
@ -131,6 +137,10 @@ fn command(git_command: GitCommand) -> Result<Output, std::io::Error> {
.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(),
}
}

@ -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<String, AppError> {
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);
}
}

Loading…
Cancel
Save