Support Bitbucket.org in permalinks (#8601)

Thorsten Ball created

This adds support for Bitbucket.org/Bitbucket Cloud repositories to the
`editor: copy permalink to line` and `editor: open permalink to line`
actions.

Fixes #5110.



Release Notes:

- Added support for repositories hosted on Bitbucket.org (Bitbucket
Cloud) to the `editor: copy permalink to line` and `editor: open
permalink to line` actions.
([#5110](https://github.com/zed-industries/zed/issues/5110)).

Change summary

crates/editor/src/editor.rs        |  15 
crates/editor/src/git/permalink.rs | 339 ++++++++++++++++++++-----------
2 files changed, 220 insertions(+), 134 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -8625,8 +8625,6 @@ impl Editor {
     }
 
     fn get_permalink_to_line(&mut self, cx: &mut ViewContext<Self>) -> Result<url::Url> {
-        use git::permalink::{build_permalink, BuildPermalinkParams};
-
         let project = self.project.clone().ok_or_else(|| anyhow!("no project"))?;
         let project = project.read(cx);
 
@@ -8651,7 +8649,6 @@ impl Editor {
             .lock()
             .head_sha()
             .ok_or_else(|| anyhow!("failed to read HEAD SHA"))?;
-
         let path = maybe!({
             let buffer = self.buffer().read(cx).as_singleton()?;
             let file = buffer.read(cx).file().and_then(|f| f.as_local())?;
@@ -8662,12 +8659,12 @@ impl Editor {
         let selections = self.selections.all::<Point>(cx);
         let selection = selections.iter().peekable().next();
 
-        build_permalink(BuildPermalinkParams {
-            remote_url: &origin_url,
-            sha: &sha,
-            path: &path,
-            selection: selection.map(|selection| selection.range()),
-        })
+        git::permalink::build_permalink(
+            &origin_url,
+            &sha,
+            &path,
+            selection.map(|selection| selection.range()),
+        )
     }
 
     pub fn copy_permalink_to_line(&mut self, _: &CopyPermalinkToLine, cx: &mut ViewContext<Self>) {

crates/editor/src/git/permalink.rs 🔗

@@ -8,6 +8,7 @@ enum GitHostingProvider {
     Github,
     Gitlab,
     Gitee,
+    BitbucketCloud,
 }
 
 impl GitHostingProvider {
@@ -16,6 +17,7 @@ impl GitHostingProvider {
             Self::Github => "https://github.com",
             Self::Gitlab => "https://gitlab.com",
             Self::Gitee => "https://gitee.com",
+            Self::BitbucketCloud => "https://bitbucket.org",
         };
 
         Url::parse(&base_url).unwrap()
@@ -29,6 +31,7 @@ impl GitHostingProvider {
 
             match self {
                 Self::Github | Self::Gitlab | Self::Gitee => format!("L{}", line),
+                Self::BitbucketCloud => format!("lines-{}", line),
             }
         } else {
             let start_line = selection.start.row + 1;
@@ -36,28 +39,19 @@ impl GitHostingProvider {
 
             match self {
                 Self::Github => format!("L{}-L{}", start_line, end_line),
-                Self::Gitlab => format!("L{}-{}", start_line, end_line),
-                Self::Gitee => format!("L{}-{}", start_line, end_line),
+                Self::Gitlab | Self::Gitee => format!("L{}-{}", start_line, end_line),
+                Self::BitbucketCloud => format!("lines-{}:{}", start_line, end_line),
             }
         }
     }
 }
 
-pub struct BuildPermalinkParams<'a> {
-    pub remote_url: &'a str,
-    pub sha: &'a str,
-    pub path: &'a str,
-    pub selection: Option<Range<Point>>,
-}
-
-pub fn build_permalink(params: BuildPermalinkParams) -> Result<Url> {
-    let BuildPermalinkParams {
-        remote_url,
-        sha,
-        path,
-        selection,
-    } = params;
-
+pub fn build_permalink(
+    remote_url: &str,
+    sha: &str,
+    path: &str,
+    selection: Option<Range<Point>>,
+) -> Result<Url> {
     let ParsedGitRemote {
         provider,
         owner,
@@ -69,6 +63,7 @@ pub fn build_permalink(params: BuildPermalinkParams) -> Result<Url> {
         GitHostingProvider::Github => format!("{owner}/{repo}/blob/{sha}/{path}"),
         GitHostingProvider::Gitlab => format!("{owner}/{repo}/-/blob/{sha}/{path}"),
         GitHostingProvider::Gitee => format!("{owner}/{repo}/blob/{sha}/{path}"),
+        GitHostingProvider::BitbucketCloud => format!("{owner}/{repo}/src/{sha}/{path}"),
     };
     let line_fragment = selection.map(|selection| provider.line_fragment(&selection));
 
@@ -130,6 +125,20 @@ fn parse_git_remote_url(url: &str) -> Option<ParsedGitRemote> {
         });
     }
 
+    if url.contains("bitbucket.org") {
+        let (_, repo_with_owner) = url.trim_end_matches(".git").split_once("bitbucket.org")?;
+        let (owner, repo) = repo_with_owner
+            .trim_start_matches("/")
+            .trim_start_matches(":")
+            .split_once("/")?;
+
+        return Some(ParsedGitRemote {
+            provider: GitHostingProvider::BitbucketCloud,
+            owner,
+            repo,
+        });
+    }
+
     None
 }
 
@@ -139,12 +148,12 @@ mod tests {
 
     #[test]
     fn test_build_github_permalink_from_ssh_url() {
-        let permalink = build_permalink(BuildPermalinkParams {
-            remote_url: "git@github.com:zed-industries/zed.git",
-            sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
-            path: "crates/editor/src/git/permalink.rs",
-            selection: None,
-        })
+        let permalink = build_permalink(
+            "git@github.com:zed-industries/zed.git",
+            "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
+            "crates/editor/src/git/permalink.rs",
+            None,
+        )
         .unwrap();
 
         let expected_url = "https://github.com/zed-industries/zed/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs";
@@ -153,12 +162,12 @@ mod tests {
 
     #[test]
     fn test_build_github_permalink_from_ssh_url_single_line_selection() {
-        let permalink = build_permalink(BuildPermalinkParams {
-            remote_url: "git@github.com:zed-industries/zed.git",
-            sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
-            path: "crates/editor/src/git/permalink.rs",
-            selection: Some(Point::new(6, 1)..Point::new(6, 10)),
-        })
+        let permalink = build_permalink(
+            "git@github.com:zed-industries/zed.git",
+            "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
+            "crates/editor/src/git/permalink.rs",
+            Some(Point::new(6, 1)..Point::new(6, 10)),
+        )
         .unwrap();
 
         let expected_url = "https://github.com/zed-industries/zed/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs#L7";
@@ -167,12 +176,12 @@ mod tests {
 
     #[test]
     fn test_build_github_permalink_from_ssh_url_multi_line_selection() {
-        let permalink = build_permalink(BuildPermalinkParams {
-            remote_url: "git@github.com:zed-industries/zed.git",
-            sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
-            path: "crates/editor/src/git/permalink.rs",
-            selection: Some(Point::new(23, 1)..Point::new(47, 10)),
-        })
+        let permalink = build_permalink(
+            "git@github.com:zed-industries/zed.git",
+            "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
+            "crates/editor/src/git/permalink.rs",
+            Some(Point::new(23, 1)..Point::new(47, 10)),
+        )
         .unwrap();
 
         let expected_url = "https://github.com/zed-industries/zed/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs#L24-L48";
@@ -181,12 +190,12 @@ mod tests {
 
     #[test]
     fn test_build_github_permalink_from_https_url() {
-        let permalink = build_permalink(BuildPermalinkParams {
-            remote_url: "https://github.com/zed-industries/zed.git",
-            sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
-            path: "crates/zed/src/main.rs",
-            selection: None,
-        })
+        let permalink = build_permalink(
+            "https://github.com/zed-industries/zed.git",
+            "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
+            "crates/zed/src/main.rs",
+            None,
+        )
         .unwrap();
 
         let expected_url = "https://github.com/zed-industries/zed/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs";
@@ -195,12 +204,12 @@ mod tests {
 
     #[test]
     fn test_build_github_permalink_from_https_url_single_line_selection() {
-        let permalink = build_permalink(BuildPermalinkParams {
-            remote_url: "https://github.com/zed-industries/zed.git",
-            sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
-            path: "crates/zed/src/main.rs",
-            selection: Some(Point::new(6, 1)..Point::new(6, 10)),
-        })
+        let permalink = build_permalink(
+            "https://github.com/zed-industries/zed.git",
+            "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
+            "crates/zed/src/main.rs",
+            Some(Point::new(6, 1)..Point::new(6, 10)),
+        )
         .unwrap();
 
         let expected_url = "https://github.com/zed-industries/zed/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs#L7";
@@ -209,12 +218,12 @@ mod tests {
 
     #[test]
     fn test_build_github_permalink_from_https_url_multi_line_selection() {
-        let permalink = build_permalink(BuildPermalinkParams {
-            remote_url: "https://github.com/zed-industries/zed.git",
-            sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
-            path: "crates/zed/src/main.rs",
-            selection: Some(Point::new(23, 1)..Point::new(47, 10)),
-        })
+        let permalink = build_permalink(
+            "https://github.com/zed-industries/zed.git",
+            "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
+            "crates/zed/src/main.rs",
+            Some(Point::new(23, 1)..Point::new(47, 10)),
+        )
         .unwrap();
 
         let expected_url = "https://github.com/zed-industries/zed/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs#L24-L48";
@@ -223,12 +232,12 @@ mod tests {
 
     #[test]
     fn test_build_gitlab_permalink_from_ssh_url() {
-        let permalink = build_permalink(BuildPermalinkParams {
-            remote_url: "git@gitlab.com:zed-industries/zed.git",
-            sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
-            path: "crates/editor/src/git/permalink.rs",
-            selection: None,
-        })
+        let permalink = build_permalink(
+            "git@gitlab.com:zed-industries/zed.git",
+            "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
+            "crates/editor/src/git/permalink.rs",
+            None,
+        )
         .unwrap();
 
         let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs";
@@ -237,12 +246,12 @@ mod tests {
 
     #[test]
     fn test_build_gitlab_permalink_from_ssh_url_single_line_selection() {
-        let permalink = build_permalink(BuildPermalinkParams {
-            remote_url: "git@gitlab.com:zed-industries/zed.git",
-            sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
-            path: "crates/editor/src/git/permalink.rs",
-            selection: Some(Point::new(6, 1)..Point::new(6, 10)),
-        })
+        let permalink = build_permalink(
+            "git@gitlab.com:zed-industries/zed.git",
+            "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
+            "crates/editor/src/git/permalink.rs",
+            Some(Point::new(6, 1)..Point::new(6, 10)),
+        )
         .unwrap();
 
         let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs#L7";
@@ -251,12 +260,12 @@ mod tests {
 
     #[test]
     fn test_build_gitlab_permalink_from_ssh_url_multi_line_selection() {
-        let permalink = build_permalink(BuildPermalinkParams {
-            remote_url: "git@gitlab.com:zed-industries/zed.git",
-            sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
-            path: "crates/editor/src/git/permalink.rs",
-            selection: Some(Point::new(23, 1)..Point::new(47, 10)),
-        })
+        let permalink = build_permalink(
+            "git@gitlab.com:zed-industries/zed.git",
+            "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
+            "crates/editor/src/git/permalink.rs",
+            Some(Point::new(23, 1)..Point::new(47, 10)),
+        )
         .unwrap();
 
         let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs#L24-48";
@@ -265,12 +274,12 @@ mod tests {
 
     #[test]
     fn test_build_gitlab_permalink_from_https_url() {
-        let permalink = build_permalink(BuildPermalinkParams {
-            remote_url: "https://gitlab.com/zed-industries/zed.git",
-            sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
-            path: "crates/zed/src/main.rs",
-            selection: None,
-        })
+        let permalink = build_permalink(
+            "https://gitlab.com/zed-industries/zed.git",
+            "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
+            "crates/zed/src/main.rs",
+            None,
+        )
         .unwrap();
 
         let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs";
@@ -279,12 +288,12 @@ mod tests {
 
     #[test]
     fn test_build_gitlab_permalink_from_https_url_single_line_selection() {
-        let permalink = build_permalink(BuildPermalinkParams {
-            remote_url: "https://gitlab.com/zed-industries/zed.git",
-            sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
-            path: "crates/zed/src/main.rs",
-            selection: Some(Point::new(6, 1)..Point::new(6, 10)),
-        })
+        let permalink = build_permalink(
+            "https://gitlab.com/zed-industries/zed.git",
+            "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
+            "crates/zed/src/main.rs",
+            Some(Point::new(6, 1)..Point::new(6, 10)),
+        )
         .unwrap();
 
         let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs#L7";
@@ -293,12 +302,12 @@ mod tests {
 
     #[test]
     fn test_build_gitlab_permalink_from_https_url_multi_line_selection() {
-        let permalink = build_permalink(BuildPermalinkParams {
-            remote_url: "https://gitlab.com/zed-industries/zed.git",
-            sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
-            path: "crates/zed/src/main.rs",
-            selection: Some(Point::new(23, 1)..Point::new(47, 10)),
-        })
+        let permalink = build_permalink(
+            "https://gitlab.com/zed-industries/zed.git",
+            "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
+            "crates/zed/src/main.rs",
+            Some(Point::new(23, 1)..Point::new(47, 10)),
+        )
         .unwrap();
 
         let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs#L24-48";
@@ -307,12 +316,12 @@ mod tests {
 
     #[test]
     fn test_build_gitee_permalink_from_ssh_url() {
-        let permalink = build_permalink(BuildPermalinkParams {
-            remote_url: "git@gitee.com:libkitten/zed.git",
-            sha: "e5fe811d7ad0fc26934edd76f891d20bdc3bb194",
-            path: "crates/editor/src/git/permalink.rs",
-            selection: None,
-        })
+        let permalink = build_permalink(
+            "git@gitee.com:libkitten/zed.git",
+            "e5fe811d7ad0fc26934edd76f891d20bdc3bb194",
+            "crates/editor/src/git/permalink.rs",
+            None,
+        )
         .unwrap();
 
         let expected_url = "https://gitee.com/libkitten/zed/blob/e5fe811d7ad0fc26934edd76f891d20bdc3bb194/crates/editor/src/git/permalink.rs";
@@ -321,12 +330,12 @@ mod tests {
 
     #[test]
     fn test_build_gitee_permalink_from_ssh_url_single_line_selection() {
-        let permalink = build_permalink(BuildPermalinkParams {
-            remote_url: "git@gitee.com:libkitten/zed.git",
-            sha: "e5fe811d7ad0fc26934edd76f891d20bdc3bb194",
-            path: "crates/editor/src/git/permalink.rs",
-            selection: Some(Point::new(6, 1)..Point::new(6, 10)),
-        })
+        let permalink = build_permalink(
+            "git@gitee.com:libkitten/zed.git",
+            "e5fe811d7ad0fc26934edd76f891d20bdc3bb194",
+            "crates/editor/src/git/permalink.rs",
+            Some(Point::new(6, 1)..Point::new(6, 10)),
+        )
         .unwrap();
 
         let expected_url = "https://gitee.com/libkitten/zed/blob/e5fe811d7ad0fc26934edd76f891d20bdc3bb194/crates/editor/src/git/permalink.rs#L7";
@@ -335,12 +344,12 @@ mod tests {
 
     #[test]
     fn test_build_gitee_permalink_from_ssh_url_multi_line_selection() {
-        let permalink = build_permalink(BuildPermalinkParams {
-            remote_url: "git@gitee.com:libkitten/zed.git",
-            sha: "e5fe811d7ad0fc26934edd76f891d20bdc3bb194",
-            path: "crates/editor/src/git/permalink.rs",
-            selection: Some(Point::new(23, 1)..Point::new(47, 10)),
-        })
+        let permalink = build_permalink(
+            "git@gitee.com:libkitten/zed.git",
+            "e5fe811d7ad0fc26934edd76f891d20bdc3bb194",
+            "crates/editor/src/git/permalink.rs",
+            Some(Point::new(23, 1)..Point::new(47, 10)),
+        )
         .unwrap();
 
         let expected_url = "https://gitee.com/libkitten/zed/blob/e5fe811d7ad0fc26934edd76f891d20bdc3bb194/crates/editor/src/git/permalink.rs#L24-48";
@@ -349,12 +358,12 @@ mod tests {
 
     #[test]
     fn test_build_gitee_permalink_from_https_url() {
-        let permalink = build_permalink(BuildPermalinkParams {
-            remote_url: "https://gitee.com/libkitten/zed.git",
-            sha: "e5fe811d7ad0fc26934edd76f891d20bdc3bb194",
-            path: "crates/zed/src/main.rs",
-            selection: None,
-        })
+        let permalink = build_permalink(
+            "https://gitee.com/libkitten/zed.git",
+            "e5fe811d7ad0fc26934edd76f891d20bdc3bb194",
+            "crates/zed/src/main.rs",
+            None,
+        )
         .unwrap();
 
         let expected_url = "https://gitee.com/libkitten/zed/blob/e5fe811d7ad0fc26934edd76f891d20bdc3bb194/crates/zed/src/main.rs";
@@ -363,12 +372,12 @@ mod tests {
 
     #[test]
     fn test_build_gitee_permalink_from_https_url_single_line_selection() {
-        let permalink = build_permalink(BuildPermalinkParams {
-            remote_url: "https://gitee.com/libkitten/zed.git",
-            sha: "e5fe811d7ad0fc26934edd76f891d20bdc3bb194",
-            path: "crates/zed/src/main.rs",
-            selection: Some(Point::new(6, 1)..Point::new(6, 10)),
-        })
+        let permalink = build_permalink(
+            "https://gitee.com/libkitten/zed.git",
+            "e5fe811d7ad0fc26934edd76f891d20bdc3bb194",
+            "crates/zed/src/main.rs",
+            Some(Point::new(6, 1)..Point::new(6, 10)),
+        )
         .unwrap();
 
         let expected_url = "https://gitee.com/libkitten/zed/blob/e5fe811d7ad0fc26934edd76f891d20bdc3bb194/crates/zed/src/main.rs#L7";
@@ -377,14 +386,94 @@ mod tests {
 
     #[test]
     fn test_build_gitee_permalink_from_https_url_multi_line_selection() {
-        let permalink = build_permalink(BuildPermalinkParams {
-            remote_url: "https://gitee.com/libkitten/zed.git",
-            sha: "e5fe811d7ad0fc26934edd76f891d20bdc3bb194",
-            path: "crates/zed/src/main.rs",
-            selection: Some(Point::new(23, 1)..Point::new(47, 10)),
-        })
+        let permalink = build_permalink(
+            "https://gitee.com/libkitten/zed.git",
+            "e5fe811d7ad0fc26934edd76f891d20bdc3bb194",
+            "crates/zed/src/main.rs",
+            Some(Point::new(23, 1)..Point::new(47, 10)),
+        )
         .unwrap();
         let expected_url = "https://gitee.com/libkitten/zed/blob/e5fe811d7ad0fc26934edd76f891d20bdc3bb194/crates/zed/src/main.rs#L24-48";
         assert_eq!(permalink.to_string(), expected_url.to_string())
     }
+
+    #[test]
+    fn test_parse_git_remote_url_bitbucket_https_with_username() {
+        let url = "https://thorstenballzed@bitbucket.org/thorstenzed/testingrepo.git";
+        let parsed = parse_git_remote_url(url).unwrap();
+        assert!(matches!(
+            parsed.provider,
+            GitHostingProvider::BitbucketCloud
+        ));
+        assert_eq!(parsed.owner, "thorstenzed");
+        assert_eq!(parsed.repo, "testingrepo");
+    }
+
+    #[test]
+    fn test_parse_git_remote_url_bitbucket_https_without_username() {
+        let url = "https://bitbucket.org/thorstenzed/testingrepo.git";
+        let parsed = parse_git_remote_url(url).unwrap();
+        assert!(matches!(
+            parsed.provider,
+            GitHostingProvider::BitbucketCloud
+        ));
+        assert_eq!(parsed.owner, "thorstenzed");
+        assert_eq!(parsed.repo, "testingrepo");
+    }
+
+    #[test]
+    fn test_parse_git_remote_url_bitbucket_git() {
+        let url = "git@bitbucket.org:thorstenzed/testingrepo.git";
+        let parsed = parse_git_remote_url(url).unwrap();
+        assert!(matches!(
+            parsed.provider,
+            GitHostingProvider::BitbucketCloud
+        ));
+        assert_eq!(parsed.owner, "thorstenzed");
+        assert_eq!(parsed.repo, "testingrepo");
+    }
+
+    #[test]
+    fn test_build_bitbucket_permalink_from_ssh_url() {
+        let permalink = build_permalink(
+            "git@bitbucket.org:thorstenzed/testingrepo.git",
+            "f00b4r",
+            "main.rs",
+            None,
+        )
+        .unwrap();
+
+        let expected_url = "https://bitbucket.org/thorstenzed/testingrepo/src/f00b4r/main.rs";
+        assert_eq!(permalink.to_string(), expected_url.to_string())
+    }
+
+    #[test]
+    fn test_build_bitbucket_permalink_from_ssh_url_single_line_selection() {
+        let permalink = build_permalink(
+            "git@bitbucket.org:thorstenzed/testingrepo.git",
+            "f00b4r",
+            "main.rs",
+            Some(Point::new(6, 1)..Point::new(6, 10)),
+        )
+        .unwrap();
+
+        let expected_url =
+            "https://bitbucket.org/thorstenzed/testingrepo/src/f00b4r/main.rs#lines-7";
+        assert_eq!(permalink.to_string(), expected_url.to_string())
+    }
+
+    #[test]
+    fn test_build_bitbucket_permalink_from_ssh_url_multi_line_selection() {
+        let permalink = build_permalink(
+            "git@bitbucket.org:thorstenzed/testingrepo.git",
+            "f00b4r",
+            "main.rs",
+            Some(Point::new(23, 1)..Point::new(47, 10)),
+        )
+        .unwrap();
+
+        let expected_url =
+            "https://bitbucket.org/thorstenzed/testingrepo/src/f00b4r/main.rs#lines-24:48";
+        assert_eq!(permalink.to_string(), expected_url.to_string())
+    }
 }