1mod error;
2mod git;
3
4use std::borrow::Cow;
5
6use error::{AppError, ErrorType};
7use git::{GitOutput, GitTrait, Local, Url};
8
9pub use git::Git;
10
11#[derive(Default)]
12pub struct GitView<'a> {
13 remote: Option<&'a str>,
14 branch: Option<&'a str>,
15 commit: Option<&'a str>,
16 issue: Option<&'a str>,
17 path: Option<&'a str>,
18 is_print: bool,
19}
20
21impl<'a> GitView<'a> {
22 pub fn new(
23 branch: Option<&'a str>,
24 remote: Option<&'a str>,
25 commit: Option<&'a str>,
26 issue: Option<&'a str>,
27 path: Option<&'a str>,
28 is_print: bool,
29 ) -> Self {
30 Self {
31 remote,
32 branch,
33 commit,
34 issue,
35 path,
36 is_print,
37 }
38 }
39
40 pub fn view_repository(&self, git: impl GitTrait) -> Result<(), AppError> {
41 self.is_valid_repository(&git)?;
42 let local_ref = self.get_local_ref(&git)?;
43 let remote = self.populate_remote(&local_ref, &git)?;
44 let remote_ref = self.get_remote_reference(&local_ref, &remote, &git)?;
45
46 let git_url = self.get_git_url(&remote, &git)?;
49 let url = self.parse_git_url(&git_url)?;
50 let final_url = self.generate_final_url(&remote_ref, &url, &git)?;
51
52 if self.is_print {
53 println!("{}", final_url);
54 } else {
55 webbrowser::open(final_url.as_str())?;
56 }
57
58 Ok(())
59 }
60
61 fn is_valid_repository(&self, git: &impl GitTrait) -> Result<(), AppError> {
62 match git.is_valid_repository()? {
63 GitOutput::Ok(_) => Ok(()),
64 GitOutput::Err(_) => Err(AppError::new(
65 ErrorType::MissingGitRepository,
66 "Looks like you're not in a valid git repository!".to_string(),
67 )),
68 }
69 }
70
71 fn get_local_ref(&self, git: &impl GitTrait) -> Result<Local, AppError> {
72 if self.branch.is_none() {
73 match git.get_local_branch()? {
74 GitOutput::Ok(output) => Ok(Local::Branch(Cow::Owned(output))),
75 GitOutput::Err(_) => Ok(Local::NotBranch),
76 }
77 } else {
78 Ok(Local::Branch(Cow::Borrowed(self.branch.as_ref().unwrap())))
79 }
80 }
81
82 fn populate_remote(
85 &self,
86 local: &Local,
87 git: &impl GitTrait,
88 ) -> Result<Cow<'_, str>, AppError> {
89 if self.remote.is_none() {
91 match local {
92 Local::Branch(branch) => {
93 match git.get_default_remote()? {
95 GitOutput::Ok(def) => Ok(Cow::Owned(def)),
96 GitOutput::Err(_) => match git.get_tracked_remote(branch)? {
98 GitOutput::Ok(tracked) => Ok(Cow::Owned(tracked)),
99 GitOutput::Err(_) => Ok(Cow::Owned("origin".into())),
101 },
102 }
103 }
104 Local::NotBranch => Ok(Cow::Owned("origin".into())),
105 }
106 } else {
107 Ok(Cow::Borrowed(self.remote.as_ref().unwrap()))
108 }
109 }
110
111 fn get_remote_reference(
112 &self,
113 local: &'a Local,
114 remote: &'a str,
115 git: &impl GitTrait,
116 ) -> Result<Cow<'a, str>, AppError> {
117 match local {
118 Local::Branch(branch) => {
119 match git.get_upstream_branch(branch)? {
120 GitOutput::Ok(output) => Ok(Cow::Owned(
121 output.trim_start_matches("refs/heads/").to_string(),
122 )),
123 GitOutput::Err(_) => match git.get_default_branch(remote)? {
125 GitOutput::Ok(default_branch) => {
126 println!("Cannot verify '{remote}/{branch}' exists, defaulting to '{default_branch}'");
127 return match default_branch.split_once('/') {
128 Some((_, split_branch)) => Ok(Cow::Owned(split_branch.into())),
129 None => Ok(Cow::Borrowed(branch)),
130 };
131 }
132 GitOutput::Err(_) => Err(AppError::new(
133 ErrorType::MissingDefaultBranch,
134 format!("Could not verify '{remote}/{branch}' exists and could not retrieve default branch")
135 )),
136 },
137 }
138 }
139 Local::NotBranch => match git.get_current_tag()? {
141 GitOutput::Ok(tag) => Ok(Cow::Owned(tag)),
142 GitOutput::Err(_) => match git.get_current_commit()? {
144 GitOutput::Ok(commit_hash) => Ok(Cow::Owned(commit_hash)),
145 GitOutput::Err(err) => Err(AppError::new(ErrorType::CommandFailed, err)),
147 },
148 },
149 }
150 }
151
152 fn get_git_url(&self, remote: &str, git: &impl GitTrait) -> Result<String, AppError> {
153 match git.is_valid_remote(remote)? {
154 GitOutput::Ok(output) => {
155 if output != remote {
156 Ok(output)
157 } else {
158 Err(AppError::new(
159 ErrorType::MissingGitRemote,
160 format!("Looks like your git remote isn't set for '{}'", remote),
161 ))
162 }
163 }
164 GitOutput::Err(err) => Err(AppError::new(ErrorType::CommandFailed, err)),
165 }
166 }
167
168 fn parse_git_url(&self, git_url: &str) -> Result<Url, AppError> {
177 if git_url.contains("://") {
181 match url::Url::parse(git_url) {
182 Ok(url) => Ok(Url::new(
183 url.scheme(),
184 url.host_str().map_or_else(|| "github.com", |host| host),
185 url.path()
186 .trim_start_matches('/')
187 .trim_end_matches('/')
188 .trim_end_matches(".git"),
189 )),
190 Err(_) => Err(AppError::new(
191 ErrorType::InvalidGitUrl,
192 format!("Sorry, couldn't parse git url '{}'", git_url),
193 )),
194 }
195 } else {
196 match git_url.split_once(':') {
197 Some((domain, path)) => {
198 let protocol = "https";
199 let path = path.trim_end_matches('/').trim_end_matches(".git");
200 let split_domain = match domain.split_once('@') {
201 Some((_username, dom)) => dom,
202 None => domain,
203 };
204
205 Ok(Url::new(protocol, split_domain, path))
206 }
207 None => Err(AppError::new(
208 ErrorType::InvalidGitUrl,
209 format!("Sorry, couldn't parse git url '{}'", git_url),
210 )),
211 }
212 }
213 }
214
215 fn generate_final_url(
216 &self,
217 remote_ref: &str,
218 url: &Url,
219 git: &impl GitTrait,
220 ) -> Result<String, AppError> {
221 let mut open_url = format!("{}://{}/{}", url.protocol, url.domain, url.path);
222 let escaped_remote_ref = escape_ascii_chars(remote_ref);
223
224 if let Some(issue) = self.issue {
225 self.handle_issue_flag(issue, &escaped_remote_ref, &mut open_url)?;
226 return Ok(open_url);
227 }
228 if let Some(commit) = self.commit {
229 self.handle_commit_flag(commit, &mut open_url, git)?;
230 return Ok(open_url);
231 }
232 if let Some(path) = self.path {
233 let prefix = format!("/tree/{}", escaped_remote_ref);
234 self.handle_path_flag(Some(prefix.as_str()), path, &mut open_url, git)?;
235 return Ok(open_url);
236 }
237
238 open_url.push_str(format!("/tree/{}", escaped_remote_ref).as_str());
239
240 Ok(open_url)
241 }
242
243 fn handle_issue_flag(
244 &self,
245 issue: &str,
246 remote_ref: &str,
247 open_url: &mut String,
248 ) -> Result<(), AppError> {
249 if issue == "branch" {
250 if let Some(issue_num) = capture_digits(remote_ref) {
251 open_url.push_str(format!("/issues/{}", issue_num).as_str());
252 } else {
253 open_url.push_str("/issues");
254 }
255 } else {
256 open_url.push_str(format!("/issues/{}", issue).as_str());
257 }
258
259 Ok(())
260 }
261
262 fn handle_commit_flag(
263 &self,
264 commit: &str,
265 open_url: &mut String,
266 git: &impl GitTrait,
267 ) -> Result<(), AppError> {
268 if commit == "current" {
269 match git.get_current_commit()? {
270 GitOutput::Ok(hash) => {
271 open_url.push_str(format!("/tree/{}", hash).as_str());
272 }
273 GitOutput::Err(err) => return Err(AppError::new(ErrorType::CommandFailed, err)),
274 };
275 } else {
276 open_url.push_str(format!("/tree/{}", commit).as_str());
277 }
278
279 if let Some(path) = self.path {
281 self.handle_path_flag(None, path, open_url, git)?;
283 }
284
285 Ok(())
286 }
287
288 fn handle_path_flag(
289 &self,
290 prefix: Option<&str>,
291 path: &str,
292 open_url: &mut String,
293 git: &impl GitTrait,
294 ) -> Result<(), AppError> {
295 if path == "current-working-directory" {
296 match git.get_current_working_directory()? {
297 GitOutput::Ok(cwd) => {
298 if !cwd.is_empty() {
300 open_url.push_str(format!("{}/{}", prefix.unwrap(), cwd).as_str());
301 }
302 }
303 GitOutput::Err(err) => return Err(AppError::new(ErrorType::CommandFailed, err)),
304 }
305 } else if let Some(prefix) = prefix {
306 open_url.push_str(format!("{}/{}", prefix, path).as_str());
307 } else {
308 open_url.push_str(format!("/{}", path).as_str());
309 }
310
311 Ok(())
312 }
313}
314
315fn capture_digits(remote_ref: &str) -> Option<&str> {
316 let mut start = 0;
317 let mut end = 0;
318 let mut found = false;
319
320 for (indice, char) in remote_ref.char_indices() {
321 if found {
322 if char.is_numeric() {
323 end = indice;
324 } else {
325 break;
326 }
327 } else if char.is_numeric() {
328 start = indice;
329 found = true;
330 }
331 }
332
333 if found {
334 Some(&remote_ref[start..=end])
335 } else {
336 None
337 }
338}
339
340fn escape_ascii_chars(remote_ref: &str) -> Cow<'_, str> {
341 if remote_ref.contains(['%', '#']) {
345 let mut escaped_str = String::with_capacity(remote_ref.len());
346
347 for char in remote_ref.chars() {
348 match char {
349 '%' => escaped_str.push_str("%25"),
350 '#' => escaped_str.push_str("%23"),
351 _ => escaped_str.push(char),
352 };
353 }
354
355 Cow::Owned(escaped_str)
356 } else {
357 Cow::Borrowed(remote_ref)
358 }
359}
360
361#[cfg(test)]
362mod lib_tests {
363 use crate::GitView;
364
365 impl<'a> GitView<'a> {
366 fn builder() -> GitViewBuilder<'a> {
367 GitViewBuilder::default()
368 }
369 }
370
371 #[derive(Default)]
372 pub(crate) struct GitViewBuilder<'a> {
373 remote: Option<&'a str>,
374 branch: Option<&'a str>,
375 commit: Option<&'a str>,
376 issue: Option<&'a str>,
377 path: Option<&'a str>,
378 is_print: bool,
379 }
380
381 impl<'a> GitViewBuilder<'a> {
382 pub(crate) fn with_remote(mut self, remote: &'a str) -> Self {
383 self.remote = Some(remote);
384 self
385 }
386
387 pub(crate) fn with_branch(mut self, branch: &'a str) -> Self {
388 self.branch = Some(branch);
389 self
390 }
391
392 pub(crate) fn with_commit(mut self, commit: &'a str) -> Self {
393 self.commit = Some(commit);
394 self
395 }
396
397 pub(crate) fn with_issue(mut self, issue: &'a str) -> Self {
398 self.issue = Some(issue);
399 self
400 }
401
402 pub(crate) fn with_path(mut self, path: &'a str) -> Self {
403 self.path = Some(path);
404 self
405 }
406
407 pub(crate) fn build(self) -> GitView<'a> {
408 GitView::new(
409 self.branch,
410 self.remote,
411 self.commit,
412 self.issue,
413 self.path,
414 self.is_print,
415 )
416 }
417 }
418
419 mod is_valid_repository {
420 use crate::{
421 error::ErrorType,
422 git::{GitOutput, MockGitTrait},
423 GitView,
424 };
425
426 #[test]
427 fn yes() {
428 let handler = GitView::default();
429 let mut mock = MockGitTrait::default();
430
431 mock.expect_is_valid_repository()
432 .returning(|| Ok(GitOutput::Ok("Valid".to_owned())));
433
434 let is_valid_repository = handler.is_valid_repository(&mock);
435
436 assert!(is_valid_repository.is_ok());
437 }
438
439 #[test]
440 fn no() {
441 let handler = GitView::default();
442 let mut mock = MockGitTrait::default();
443
444 mock.expect_is_valid_repository()
445 .returning(|| Ok(GitOutput::Err("Error".to_owned())));
446
447 let is_valid_repository = handler.is_valid_repository(&mock);
448
449 assert!(is_valid_repository.is_err());
450 let error = is_valid_repository.as_ref().unwrap_err();
451 assert_eq!(error.error_type, ErrorType::MissingGitRepository);
452 assert_eq!(
453 error.error_str,
454 "Looks like you're not in a valid git repository!"
455 );
456 }
457 }
458
459 mod get_local_ref {
460 use std::borrow::Cow;
461
462 use crate::{
463 git::{GitOutput, MockGitTrait},
464 GitView, Local,
465 };
466
467 #[test]
468 fn user_given_branch() {
469 let handler = GitView::builder().with_branch("main").build();
470 let mock = MockGitTrait::default();
471 let expected_local_ref = Ok(Local::Branch(Cow::Borrowed("main")));
472
473 let actual_local_ref = handler.get_local_ref(&mock);
474
475 assert!(actual_local_ref.is_ok());
476 assert_eq!(actual_local_ref, expected_local_ref);
477 }
478
479 #[test]
480 fn is_branch() {
481 let handler = GitView::default();
482 let mut mock = MockGitTrait::default();
483 let expected_local_ref = Ok(Local::Branch(Cow::Borrowed("dev")));
484
485 mock.expect_get_local_branch()
486 .returning(|| Ok(GitOutput::Ok("dev".into())));
487
488 let actual_local_ref = handler.get_local_ref(&mock);
489
490 assert!(actual_local_ref.is_ok());
491 assert_eq!(actual_local_ref, expected_local_ref);
492 }
493
494 #[test]
495 fn is_not_branch() {
496 let handler = GitView::default();
497 let mut mock = MockGitTrait::default();
498 let expected_local_ref = Ok(Local::NotBranch);
499
500 mock.expect_get_local_branch()
501 .returning(|| Ok(GitOutput::Err("Error".into())));
502
503 let actual_local_ref = handler.get_local_ref(&mock);
504
505 assert!(actual_local_ref.is_ok());
506 assert_eq!(actual_local_ref, expected_local_ref);
507 }
508 }
509
510 mod populate_remote {
511 use std::borrow::Cow;
512
513 use mockall::predicate::eq;
514
515 use crate::{
516 git::{GitOutput, MockGitTrait},
517 GitView, Local,
518 };
519
520 #[test]
521 fn is_not_branch() {
522 let handler = GitView::builder().with_remote("origin").build();
523 let mock = MockGitTrait::default();
524
525 let actual_remote = handler.populate_remote(&Local::NotBranch, &mock);
526
527 assert!(actual_remote.is_ok());
528 assert_eq!(actual_remote.unwrap(), "origin");
529 }
530
531 #[test]
532 fn user_given_remote() {
533 let handler = GitView::builder().with_remote("origin").build();
534 let mock = MockGitTrait::default();
535
536 let actual_remote = handler.populate_remote(&Local::Branch(Cow::Borrowed("")), &mock);
537
538 assert!(actual_remote.is_ok());
539 assert_eq!(actual_remote.unwrap(), handler.remote.unwrap());
540 }
541
542 #[test]
543 fn is_default_remote() {
544 let handler = GitView::default();
545 let mut mock = MockGitTrait::default();
546
547 mock.expect_get_default_remote()
548 .returning(|| Ok(GitOutput::Ok("default_remote".into())));
549
550 let actual_remote = handler.populate_remote(&Local::Branch(Cow::Borrowed("")), &mock);
551
552 assert!(actual_remote.is_ok());
553 assert_eq!(actual_remote.unwrap(), "default_remote");
554 }
555
556 #[test]
557 fn is_tracked_remote() {
558 let handler = GitView::default();
559 let mut mock = MockGitTrait::default();
560
561 mock.expect_get_default_remote()
562 .returning(|| Ok(GitOutput::Err("error".into())));
563 mock.expect_get_tracked_remote()
564 .with(eq("branch"))
565 .returning(|_| Ok(GitOutput::Ok("tracked_remote".into())));
566
567 let actual_remote =
568 handler.populate_remote(&Local::Branch(Cow::Borrowed("branch")), &mock);
569
570 assert!(actual_remote.is_ok());
571 assert_eq!(actual_remote.unwrap(), "tracked_remote");
572 }
573
574 #[test]
575 fn is_not_default_or_tracked() {
576 let handler = GitView::default();
577 let mut mock = MockGitTrait::default();
578
579 mock.expect_get_default_remote()
580 .returning(|| Ok(GitOutput::Err("error".into())));
581 mock.expect_get_tracked_remote()
582 .with(eq("branch"))
583 .returning(|_| Ok(GitOutput::Err("error".into())));
584
585 let actual_remote =
586 handler.populate_remote(&Local::Branch(Cow::Borrowed("branch")), &mock);
587
588 assert!(actual_remote.is_ok());
589 assert_eq!(actual_remote.unwrap(), "origin");
590 }
591 }
592
593 mod get_remote_reference {
594 use std::borrow::Cow;
595
596 use crate::{
597 error::ErrorType,
598 git::{GitOutput, MockGitTrait},
599 GitView, Local,
600 };
601
602 #[test]
603 fn is_branch_and_exists_on_remote() {
604 let handler = GitView::default();
605 let local = Local::Branch(Cow::Borrowed("main"));
606 let mut mock = MockGitTrait::default();
607
608 mock.expect_get_upstream_branch()
609 .returning(|_| Ok(GitOutput::Ok("refs/heads/main".into())));
610
611 let actual_upstream_branch = handler.get_remote_reference(&local, "origin", &mock);
612
613 assert!(actual_upstream_branch.is_ok());
614 assert_eq!(actual_upstream_branch.unwrap(), "main");
615 }
616
617 #[test]
618 fn is_branch_and_successfully_get_default() {
619 let handler = GitView::default();
620 let local = Local::Branch(Cow::Borrowed("main"));
621 let mut mock = MockGitTrait::default();
622
623 mock.expect_get_upstream_branch()
624 .returning(|_| Ok(GitOutput::Err("error".into())));
625 mock.expect_get_default_branch()
626 .returning(|_| Ok(GitOutput::Ok("origin/main".into())));
627
628 let actual_upstream_branch = handler.get_remote_reference(&local, "origin", &mock);
629
630 assert!(actual_upstream_branch.is_ok());
631 assert_eq!(actual_upstream_branch.unwrap(), "main")
632 }
633
634 #[test]
635 fn is_branch_and_fail_to_get_default() {
636 let handler = GitView::default();
637 let local = Local::Branch(Cow::Borrowed("testing"));
638 let mut mock = MockGitTrait::default();
639
640 mock.expect_get_upstream_branch()
641 .returning(|_| Ok(GitOutput::Err("error".into())));
642 mock.expect_get_default_branch()
643 .returning(|_| Ok(GitOutput::Err("error".into())));
644
645 let actual_upstream_branch = handler.get_remote_reference(&local, "origin", &mock);
646
647 assert!(actual_upstream_branch.is_err());
648 assert_eq!(
649 actual_upstream_branch.unwrap_err().error_str,
650 "Could not verify 'origin/testing' exists and could not retrieve default branch"
651 );
652 }
653
654 #[test]
655 fn not_branch_and_get_current_tag() {
656 let handler = GitView::default();
657 let local = Local::NotBranch;
658 let mut mock = MockGitTrait::default();
659
660 mock.expect_get_current_tag()
661 .returning(|| Ok(GitOutput::Ok("v1.0.0".into())));
662
663 let actual_upstream_branch = handler.get_remote_reference(&local, "origin", &mock);
664
665 assert!(actual_upstream_branch.is_ok());
666 assert_eq!(actual_upstream_branch.unwrap(), "v1.0.0")
667 }
668
669 #[test]
670 fn not_branch_and_get_current_commit() {
671 let handler = GitView::default();
672 let local = Local::NotBranch;
673 let mut mock = MockGitTrait::default();
674
675 mock.expect_get_current_tag()
676 .returning(|| Ok(GitOutput::Err("error".into())));
677 mock.expect_get_current_commit()
678 .returning(|| Ok(GitOutput::Ok("hash".into())));
679
680 let actual_upstream_branch = handler.get_remote_reference(&local, "origin", &mock);
681
682 assert!(actual_upstream_branch.is_ok());
683 assert_eq!(actual_upstream_branch.unwrap(), "hash")
684 }
685
686 #[test]
687 fn not_branch_and_no_tag_or_commit() {
688 let handler = GitView::default();
689 let local = Local::NotBranch;
690 let mut mock = MockGitTrait::default();
691
692 mock.expect_get_current_tag()
693 .returning(|| Ok(GitOutput::Err("error".into())));
694 mock.expect_get_current_commit()
695 .returning(|| Ok(GitOutput::Err("error".into())));
696
697 let actual_upstream_branch = handler.get_remote_reference(&local, "origin", &mock);
698
699 assert!(actual_upstream_branch.is_err());
700
701 let error = actual_upstream_branch.as_ref().unwrap_err();
702 assert_eq!(error.error_type, ErrorType::CommandFailed);
703 assert_eq!(error.error_str, "error");
704 }
705 }
706
707 mod get_git_url {
708 use crate::{
709 error::{AppError, ErrorType},
710 git::{GitOutput, MockGitTrait},
711 GitView,
712 };
713
714 #[test]
715 fn is_valid_remote() {
716 let handler = GitView::default();
717 let expected_remote = "origin";
718 let mut mock = MockGitTrait::default();
719
720 mock.expect_is_valid_remote()
721 .returning(|_| Ok(GitOutput::Ok("https://github.com/sgoudham/git-view".into())));
722
723 let actual_remote = handler.get_git_url(expected_remote, &mock);
724
725 assert!(actual_remote.is_ok());
726 assert_eq!(
727 actual_remote.unwrap(),
728 "https://github.com/sgoudham/git-view"
729 )
730 }
731
732 #[test]
733 fn is_not_valid_remote() {
734 let handler = GitView::default();
735 let expected_remote = "origin";
736 let mut mock = MockGitTrait::default();
737
738 mock.expect_is_valid_remote()
739 .returning(|_| Ok(GitOutput::Ok("origin".into())));
740
741 let actual_remote = handler.get_git_url(expected_remote, &mock);
742
743 assert!(actual_remote.is_err());
744 assert_eq!(
745 actual_remote.unwrap_err().error_str,
746 "Looks like your git remote isn't set for 'origin'"
747 );
748 }
749
750 #[test]
751 fn command_failed() {
752 let handler = GitView::default();
753 let expected_remote = "origin";
754 let mut mock = MockGitTrait::default();
755
756 mock.expect_is_valid_remote()
757 .returning(|_| Err(AppError::new(ErrorType::CommandFailed, "error".into())));
758
759 let actual_remote = handler.get_git_url(expected_remote, &mock);
760
761 assert!(actual_remote.is_err());
762 assert_eq!(actual_remote.unwrap_err().error_str, "error");
763 }
764 }
765
766 mod parse_git_url {
767 use crate::{error::AppError, GitView};
768 use test_case::test_case;
769
770 #[test_case("https://github.com:8080/sgoudham/git-view.git" ; "with port")]
771 #[test_case("https://github.com/sgoudham/git-view.git" ; "normal")]
772 #[test_case("https://github.com/sgoudham/git-view.git/" ; "with trailing slash")]
773 fn https(git_url: &str) -> Result<(), AppError> {
774 let handler = GitView::default();
775
776 let url = handler.parse_git_url(git_url)?;
777
778 assert_eq!(url.protocol, "https");
779 assert_eq!(url.domain, "github.com");
780 assert_eq!(url.path, "sgoudham/git-view");
781
782 Ok(())
783 }
784
785 #[test_case("git@github.com:sgoudham/git-view.git" ; "with username")]
786 #[test_case("github.com:sgoudham/git-view.git" ; "normal")]
787 #[test_case("github.com:sgoudham/git-view.git/" ; "with trailing slash")]
788 fn scp_like(git_url: &str) -> Result<(), AppError> {
789 let handler = GitView::default();
790
791 let url = handler.parse_git_url(git_url)?;
792
793 assert_eq!(url.protocol, "https");
794 assert_eq!(url.domain, "github.com");
795 assert_eq!(url.path, "sgoudham/git-view");
796
797 Ok(())
798 }
799
800 #[test]
801 fn invalid_git_url() {
802 let handler = GitView::default();
803 let git_url_normal = "This isn't a git url";
804
805 let error = handler.parse_git_url(git_url_normal);
806
807 assert!(error.is_err());
808 assert_eq!(
809 error.unwrap_err().error_str,
810 "Sorry, couldn't parse git url 'This isn't a git url'"
811 );
812 }
813 }
814
815 mod generate_final_url {
816 use crate::{
817 git::{GitOutput, MockGitTrait, Url},
818 GitView,
819 };
820 use test_case::test_case;
821
822 #[test]
823 fn is_latest_commit() {
824 let handler = GitView::builder().with_commit("current").build();
825 let url = Url::new("https", "github.com", "sgoudham/git-view");
826 let expected_final_url = "https://github.com/sgoudham/git-view/tree/eafdb9a";
827 let mut mock = MockGitTrait::default();
828
829 mock.expect_get_current_commit()
830 .returning(|| Ok(GitOutput::Ok("eafdb9a".into())));
831
832 let actual_final_url = handler.generate_final_url("main", &url, &mock);
833
834 assert!(actual_final_url.is_ok());
835 assert_eq!(actual_final_url.unwrap(), expected_final_url);
836 }
837
838 #[test]
839 fn is_user_commit() {
840 let handler = GitView::builder()
841 .with_commit("8s2jl250as7f234jasfjj")
842 .build();
843 let url = Url::new("https", "github.com", "sgoudham/git-view");
844 let expected_final_url =
845 "https://github.com/sgoudham/git-view/tree/8s2jl250as7f234jasfjj";
846 let mock = MockGitTrait::default();
847
848 let actual_final_url = handler.generate_final_url("main", &url, &mock);
849
850 assert!(actual_final_url.is_ok());
851 assert_eq!(actual_final_url.unwrap(), expected_final_url);
852 }
853
854 #[test]
855 fn is_latest_commit_with_path_current_working_directory() {
856 let handler = GitView::builder()
857 .with_commit("current")
858 .with_path("src/main.rs")
859 .build();
860 let url = Url::new("https", "github.com", "sgoudham/git-view");
861 let expected_final_url =
862 "https://github.com/sgoudham/git-view/tree/eafdb9a/src/main.rs";
863
864 let mut mock = MockGitTrait::default();
865 mock.expect_get_current_commit()
866 .returning(|| Ok(GitOutput::Ok("eafdb9a".into())));
867
868 let actual_final_url = handler.generate_final_url("main", &url, &mock);
869
870 assert!(actual_final_url.is_ok());
871 assert_eq!(actual_final_url.unwrap(), expected_final_url);
872 }
873
874 #[test_case("main" ; "main")]
875 #[test_case("master" ; "master")]
876 fn is_master_or_main(branch: &str) {
877 let handler = GitView::default();
878 let url = Url::new("https", "github.com", "sgoudham/git-view");
879 let expected_final_url = format!("https://github.com/sgoudham/git-view/tree/{branch}");
880 let mock = MockGitTrait::default();
881
882 let actual_final_url = handler.generate_final_url(branch, &url, &mock);
883
884 assert!(actual_final_url.is_ok());
885 assert_eq!(actual_final_url.unwrap(), expected_final_url);
886 }
887
888 #[test_case("main" ; "main")]
889 #[test_case("master" ; "master")]
890 fn is_master_or_main_with_issue_flag(branch: &str) {
891 let handler = GitView::builder().with_issue("branch").build();
892 let url = Url::new("https", "github.com", "sgoudham/git-view");
893 let expected_final_url = "https://github.com/sgoudham/git-view/issues";
894 let mock = MockGitTrait::default();
895
896 let actual_final_url = handler.generate_final_url(branch, &url, &mock);
897
898 assert!(actual_final_url.is_ok());
899 assert_eq!(actual_final_url.unwrap(), expected_final_url);
900 }
901
902 #[test]
903 fn is_user_issue() {
904 let handler = GitView::builder().with_issue("branch").build();
905 let url = Url::new("https", "github.com", "sgoudham/git-view");
906 let expected_final_url = "https://github.com/sgoudham/git-view/issues/1234";
907 let mock = MockGitTrait::default();
908
909 let actual_final_url = handler.generate_final_url("TICKET-1234", &url, &mock);
910
911 assert!(actual_final_url.is_ok());
912 assert_eq!(actual_final_url.unwrap(), expected_final_url);
913 }
914
915 #[test]
916 fn is_user_issue_with_args() {
917 let handler = GitView::builder().with_issue("42").build();
918 let url = Url::new("https", "github.com", "sgoudham/git-view");
919 let expected_final_url = "https://github.com/sgoudham/git-view/issues/42";
920 let mock = MockGitTrait::default();
921
922 let actual_final_url = handler.generate_final_url("main", &url, &mock);
923
924 assert!(actual_final_url.is_ok());
925 assert_eq!(actual_final_url.unwrap(), expected_final_url);
926 }
927
928 #[test]
929 fn is_normal_branch() {
930 let handler = GitView::builder().build();
931 let url = Url::new("https", "github.com", "sgoudham/git-view");
932 let expected_final_url = "https://github.com/sgoudham/git-view/tree/%23test%23";
933 let mock = MockGitTrait::default();
934
935 let actual_final_url = handler.generate_final_url("#test#", &url, &mock);
936
937 assert!(actual_final_url.is_ok());
938 assert_eq!(actual_final_url.unwrap(), expected_final_url);
939 }
940
941 #[test]
942 fn is_user_path() {
943 let handler = GitView::builder().with_path("src/main.rs").build();
944 let url = Url::new("https", "github.com", "sgoudham/git-view");
945 let expected_final_url = "https://github.com/sgoudham/git-view/tree/main/src/main.rs";
946 let mock = MockGitTrait::default();
947
948 let actual_final_url = handler.generate_final_url("main", &url, &mock);
949
950 assert!(actual_final_url.is_ok());
951 assert_eq!(actual_final_url.unwrap(), expected_final_url);
952 }
953
954 #[test]
955 fn is_path_at_repo_root() {
956 let handler = GitView::builder()
957 .with_path("current-working-directory")
958 .build();
959 let url = Url::new("https", "github.com", "sgoudham/git-view");
960 let expected_final_url = "https://github.com/sgoudham/git-view";
961
962 let mut mock = MockGitTrait::default();
963 mock.expect_get_current_working_directory()
964 .returning(|| Ok(GitOutput::Ok("".into())));
965
966 let actual_final_url = handler.generate_final_url("main", &url, &mock);
967
968 assert!(actual_final_url.is_ok());
969 assert_eq!(actual_final_url.unwrap(), expected_final_url);
970 }
971
972 #[test]
973 fn is_path_at_sub_directory() {
974 let handler = GitView::builder()
975 .with_path("current-working-directory")
976 .build();
977 let url = Url::new("https", "github.com", "sgoudham/git-view");
978 let expected_final_url = "https://github.com/sgoudham/git-view/tree/main/src/";
979
980 let mut mock = MockGitTrait::default();
982 mock.expect_get_current_working_directory()
983 .returning(|| Ok(GitOutput::Ok("src/".into())));
984
985 let actual_final_url = handler.generate_final_url("main", &url, &mock);
986
987 assert!(actual_final_url.is_ok());
988 assert_eq!(actual_final_url.unwrap(), expected_final_url);
989 }
990 }
991
992 mod capture_digits {
993 use test_case::test_case;
994
995 use crate::capture_digits;
996
997 #[test_case("🥵🥵Hazel🥵-1234🥵🥵", "1234" ; "with emojis")]
998 #[test_case("TICKET-1234-To-V10", "1234" ; "with multiple issue numbers")]
999 #[test_case("TICKET-1234", "1234" ; "with issue number at end")]
1000 #[test_case("1234-TICKET", "1234" ; "with issue number at start")]
1001 #[test_case("1234", "1234" ; "with no letters")]
1002 fn branch(input: &str, expected_remote_ref: &str) {
1003 let actual_remote_ref = capture_digits(input);
1004 assert_eq!(actual_remote_ref, Some(expected_remote_ref));
1005 }
1006
1007 #[test]
1008 fn branch_no_numbers() {
1009 let input = "TICKET-WITH-NO-NUMBERS";
1010 let actual_remote_ref = capture_digits(input);
1011 assert_eq!(actual_remote_ref, None);
1012 }
1013 }
1014
1015 mod escape_ascii_chars {
1016 use test_case::test_case;
1017
1018 use crate::escape_ascii_chars;
1019
1020 #[test_case("🥵🥵Hazel🥵-%1234#🥵🥵", "🥵🥵Hazel🥵-%251234%23🥵🥵" ; "with emojis")]
1021 #[test_case("TICKET-%1234#", "TICKET-%251234%23" ; "with hashtag and percentage")]
1022 #[test_case("TICKET-%1234", "TICKET-%251234" ; "with percentage")]
1023 #[test_case("TICKET-#1234", "TICKET-%231234" ; "with hashtag")]
1024 #[test_case("TICKET", "TICKET" ; "with only alphabet")]
1025 fn branch(input: &str, expected_remote_ref: &str) {
1026 let actual_remote_ref = escape_ascii_chars(input);
1027 assert_eq!(actual_remote_ref, expected_remote_ref);
1028 }
1029 }
1030}