git: Fix support for self-hosted Bitbucket (#42002)
Closes #41995 Release Notes: - Fixed support for self-hosted Bitbucket
This commit is contained in:
@@ -49,6 +49,8 @@ pub fn register_additional_providers(
|
||||
provider_registry.register_hosting_provider(Arc::new(forgejo_self_hosted));
|
||||
} else if let Ok(gitea_self_hosted) = Gitea::from_remote_url(&origin_url) {
|
||||
provider_registry.register_hosting_provider(Arc::new(gitea_self_hosted));
|
||||
} else if let Ok(bitbucket_self_hosted) = Bitbucket::from_remote_url(&origin_url) {
|
||||
provider_registry.register_hosting_provider(Arc::new(bitbucket_self_hosted));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::str::FromStr;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use anyhow::{Result, bail};
|
||||
use regex::Regex;
|
||||
use url::Url;
|
||||
|
||||
@@ -9,6 +10,8 @@ use git::{
|
||||
PullRequest, RemoteUrl,
|
||||
};
|
||||
|
||||
use crate::get_host_from_git_remote_url;
|
||||
|
||||
fn pull_request_regex() -> &'static Regex {
|
||||
static PULL_REQUEST_REGEX: LazyLock<Regex> = LazyLock::new(|| {
|
||||
// This matches Bitbucket PR reference pattern: (pull request #xxx)
|
||||
@@ -33,6 +36,31 @@ impl Bitbucket {
|
||||
pub fn public_instance() -> Self {
|
||||
Self::new("Bitbucket", Url::parse("https://bitbucket.org").unwrap())
|
||||
}
|
||||
|
||||
pub fn from_remote_url(remote_url: &str) -> Result<Self> {
|
||||
let host = get_host_from_git_remote_url(remote_url)?;
|
||||
if host == "bitbucket.org" {
|
||||
bail!("the BitBucket instance is not self-hosted");
|
||||
}
|
||||
|
||||
// TODO: detecting self hosted instances by checking whether "bitbucket" is in the url or not
|
||||
// is not very reliable. See https://github.com/zed-industries/zed/issues/26393 for more
|
||||
// information.
|
||||
if !host.contains("bitbucket") {
|
||||
bail!("not a BitBucket URL");
|
||||
}
|
||||
|
||||
Ok(Self::new(
|
||||
"BitBucket Self-Hosted",
|
||||
Url::parse(&format!("https://{}", host))?,
|
||||
))
|
||||
}
|
||||
|
||||
fn is_self_hosted(&self) -> bool {
|
||||
self.base_url
|
||||
.host_str()
|
||||
.is_some_and(|host| host != "bitbucket.org")
|
||||
}
|
||||
}
|
||||
|
||||
impl GitHostingProvider for Bitbucket {
|
||||
@@ -49,10 +77,16 @@ impl GitHostingProvider for Bitbucket {
|
||||
}
|
||||
|
||||
fn format_line_number(&self, line: u32) -> String {
|
||||
if self.is_self_hosted() {
|
||||
return format!("{line}");
|
||||
}
|
||||
format!("lines-{line}")
|
||||
}
|
||||
|
||||
fn format_line_numbers(&self, start_line: u32, end_line: u32) -> String {
|
||||
if self.is_self_hosted() {
|
||||
return format!("{start_line}-{end_line}");
|
||||
}
|
||||
format!("lines-{start_line}:{end_line}")
|
||||
}
|
||||
|
||||
@@ -60,7 +94,7 @@ impl GitHostingProvider for Bitbucket {
|
||||
let url = RemoteUrl::from_str(url).ok()?;
|
||||
|
||||
let host = url.host_str()?;
|
||||
if host != "bitbucket.org" {
|
||||
if host != self.base_url.host_str()? {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -81,7 +115,12 @@ impl GitHostingProvider for Bitbucket {
|
||||
) -> Url {
|
||||
let BuildCommitPermalinkParams { sha } = params;
|
||||
let ParsedGitRemote { owner, repo } = remote;
|
||||
|
||||
if self.is_self_hosted() {
|
||||
return self
|
||||
.base_url()
|
||||
.join(&format!("projects/{owner}/repos/{repo}/commits/{sha}"))
|
||||
.unwrap();
|
||||
}
|
||||
self.base_url()
|
||||
.join(&format!("{owner}/{repo}/commits/{sha}"))
|
||||
.unwrap()
|
||||
@@ -95,10 +134,18 @@ impl GitHostingProvider for Bitbucket {
|
||||
selection,
|
||||
} = params;
|
||||
|
||||
let mut permalink = self
|
||||
.base_url()
|
||||
.join(&format!("{owner}/{repo}/src/{sha}/{path}"))
|
||||
.unwrap();
|
||||
let mut permalink = if self.is_self_hosted() {
|
||||
self.base_url()
|
||||
.join(&format!(
|
||||
"projects/{owner}/repos/{repo}/browse/{path}?at={sha}"
|
||||
))
|
||||
.unwrap()
|
||||
} else {
|
||||
self.base_url()
|
||||
.join(&format!("{owner}/{repo}/src/{sha}/{path}"))
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
permalink.set_fragment(
|
||||
selection
|
||||
.map(|selection| self.line_fragment(&selection))
|
||||
@@ -117,7 +164,14 @@ impl GitHostingProvider for Bitbucket {
|
||||
|
||||
// Construct the PR URL in Bitbucket format
|
||||
let mut url = self.base_url();
|
||||
let path = format!("/{}/{}/pull-requests/{}", remote.owner, remote.repo, number);
|
||||
let path = if self.is_self_hosted() {
|
||||
format!(
|
||||
"/projects/{}/repos/{}/pull-requests/{}",
|
||||
remote.owner, remote.repo, number
|
||||
)
|
||||
} else {
|
||||
format!("/{}/{}/pull-requests/{}", remote.owner, remote.repo, number)
|
||||
};
|
||||
url.set_path(&path);
|
||||
|
||||
Some(PullRequest { number, url })
|
||||
@@ -176,6 +230,60 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_remote_url_given_self_hosted_ssh_url() {
|
||||
let remote_url = "git@bitbucket.company.com:zed-industries/zed.git";
|
||||
|
||||
let parsed_remote = Bitbucket::from_remote_url(remote_url)
|
||||
.unwrap()
|
||||
.parse_remote_url(remote_url)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
parsed_remote,
|
||||
ParsedGitRemote {
|
||||
owner: "zed-industries".into(),
|
||||
repo: "zed".into(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_remote_url_given_self_hosted_https_url() {
|
||||
let remote_url = "https://bitbucket.company.com/zed-industries/zed.git";
|
||||
|
||||
let parsed_remote = Bitbucket::from_remote_url(remote_url)
|
||||
.unwrap()
|
||||
.parse_remote_url(remote_url)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
parsed_remote,
|
||||
ParsedGitRemote {
|
||||
owner: "zed-industries".into(),
|
||||
repo: "zed".into(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_remote_url_given_self_hosted_https_url_with_username() {
|
||||
let remote_url = "https://thorstenballzed@bitbucket.company.com/zed-industries/zed.git";
|
||||
|
||||
let parsed_remote = Bitbucket::from_remote_url(remote_url)
|
||||
.unwrap()
|
||||
.parse_remote_url(remote_url)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
parsed_remote,
|
||||
ParsedGitRemote {
|
||||
owner: "zed-industries".into(),
|
||||
repo: "zed".into(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_bitbucket_permalink() {
|
||||
let permalink = Bitbucket::public_instance().build_permalink(
|
||||
@@ -190,6 +298,23 @@ mod tests {
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_bitbucket_self_hosted_permalink() {
|
||||
let permalink =
|
||||
Bitbucket::from_remote_url("git@bitbucket.company.com:zed-industries/zed.git")
|
||||
.unwrap()
|
||||
.build_permalink(
|
||||
ParsedGitRemote {
|
||||
owner: "zed-industries".into(),
|
||||
repo: "zed".into(),
|
||||
},
|
||||
BuildPermalinkParams::new("f00b4r", &repo_path("main.rs"), None),
|
||||
);
|
||||
|
||||
let expected_url = "https://bitbucket.company.com/projects/zed-industries/repos/zed/browse/main.rs?at=f00b4r";
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_bitbucket_permalink_with_single_line_selection() {
|
||||
let permalink = Bitbucket::public_instance().build_permalink(
|
||||
@@ -204,6 +329,23 @@ mod tests {
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_bitbucket_self_hosted_permalink_with_single_line_selection() {
|
||||
let permalink =
|
||||
Bitbucket::from_remote_url("https://bitbucket.company.com/zed-industries/zed.git")
|
||||
.unwrap()
|
||||
.build_permalink(
|
||||
ParsedGitRemote {
|
||||
owner: "zed-industries".into(),
|
||||
repo: "zed".into(),
|
||||
},
|
||||
BuildPermalinkParams::new("f00b4r", &repo_path("main.rs"), Some(6..6)),
|
||||
);
|
||||
|
||||
let expected_url = "https://bitbucket.company.com/projects/zed-industries/repos/zed/browse/main.rs?at=f00b4r#7";
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_bitbucket_permalink_with_multi_line_selection() {
|
||||
let permalink = Bitbucket::public_instance().build_permalink(
|
||||
@@ -219,6 +361,23 @@ mod tests {
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_bitbucket_self_hosted_permalink_with_multi_line_selection() {
|
||||
let permalink =
|
||||
Bitbucket::from_remote_url("git@bitbucket.company.com:zed-industries/zed.git")
|
||||
.unwrap()
|
||||
.build_permalink(
|
||||
ParsedGitRemote {
|
||||
owner: "zed-industries".into(),
|
||||
repo: "zed".into(),
|
||||
},
|
||||
BuildPermalinkParams::new("f00b4r", &repo_path("main.rs"), Some(23..47)),
|
||||
);
|
||||
|
||||
let expected_url = "https://bitbucket.company.com/projects/zed-industries/repos/zed/browse/main.rs?at=f00b4r#24-48";
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bitbucket_pull_requests() {
|
||||
use indoc::indoc;
|
||||
@@ -248,4 +407,36 @@ mod tests {
|
||||
"https://bitbucket.org/zed-industries/zed/pull-requests/123"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bitbucket_self_hosted_pull_requests() {
|
||||
use indoc::indoc;
|
||||
|
||||
let remote = ParsedGitRemote {
|
||||
owner: "zed-industries".into(),
|
||||
repo: "zed".into(),
|
||||
};
|
||||
|
||||
let bitbucket =
|
||||
Bitbucket::from_remote_url("https://bitbucket.company.com/zed-industries/zed.git")
|
||||
.unwrap();
|
||||
|
||||
// Test message without PR reference
|
||||
let message = "This does not contain a pull request";
|
||||
assert!(bitbucket.extract_pull_request(&remote, message).is_none());
|
||||
|
||||
// Pull request number at end of first line
|
||||
let message = indoc! {r#"
|
||||
Merged in feature-branch (pull request #123)
|
||||
|
||||
Some detailed description of the changes.
|
||||
"#};
|
||||
|
||||
let pr = bitbucket.extract_pull_request(&remote, message).unwrap();
|
||||
assert_eq!(pr.number, 123);
|
||||
assert_eq!(
|
||||
pr.url.as_str(),
|
||||
"https://bitbucket.company.com/projects/zed-industries/repos/zed/pull-requests/123"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user