Skip to content

Commit 35c5815

Browse files
authored
git: Fix support for self-hosted Bitbucket (#42002)
Closes #41995 Release Notes: - Fixed support for self-hosted Bitbucket
1 parent e025ee6 commit 35c5815

File tree

2 files changed

+200
-7
lines changed

2 files changed

+200
-7
lines changed

crates/git_hosting_providers/src/git_hosting_providers.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ pub fn register_additional_providers(
4949
provider_registry.register_hosting_provider(Arc::new(forgejo_self_hosted));
5050
} else if let Ok(gitea_self_hosted) = Gitea::from_remote_url(&origin_url) {
5151
provider_registry.register_hosting_provider(Arc::new(gitea_self_hosted));
52+
} else if let Ok(bitbucket_self_hosted) = Bitbucket::from_remote_url(&origin_url) {
53+
provider_registry.register_hosting_provider(Arc::new(bitbucket_self_hosted));
5254
}
5355
}
5456

crates/git_hosting_providers/src/providers/bitbucket.rs

Lines changed: 198 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use std::str::FromStr;
22
use std::sync::LazyLock;
33

4+
use anyhow::{Result, bail};
45
use regex::Regex;
56
use url::Url;
67

@@ -9,6 +10,8 @@ use git::{
910
PullRequest, RemoteUrl,
1011
};
1112

13+
use crate::get_host_from_git_remote_url;
14+
1215
fn pull_request_regex() -> &'static Regex {
1316
static PULL_REQUEST_REGEX: LazyLock<Regex> = LazyLock::new(|| {
1417
// This matches Bitbucket PR reference pattern: (pull request #xxx)
@@ -33,6 +36,31 @@ impl Bitbucket {
3336
pub fn public_instance() -> Self {
3437
Self::new("Bitbucket", Url::parse("https://bitbucket.org").unwrap())
3538
}
39+
40+
pub fn from_remote_url(remote_url: &str) -> Result<Self> {
41+
let host = get_host_from_git_remote_url(remote_url)?;
42+
if host == "bitbucket.org" {
43+
bail!("the BitBucket instance is not self-hosted");
44+
}
45+
46+
// TODO: detecting self hosted instances by checking whether "bitbucket" is in the url or not
47+
// is not very reliable. See https://github.com/zed-industries/zed/issues/26393 for more
48+
// information.
49+
if !host.contains("bitbucket") {
50+
bail!("not a BitBucket URL");
51+
}
52+
53+
Ok(Self::new(
54+
"BitBucket Self-Hosted",
55+
Url::parse(&format!("https://{}", host))?,
56+
))
57+
}
58+
59+
fn is_self_hosted(&self) -> bool {
60+
self.base_url
61+
.host_str()
62+
.is_some_and(|host| host != "bitbucket.org")
63+
}
3664
}
3765

3866
impl GitHostingProvider for Bitbucket {
@@ -49,18 +77,24 @@ impl GitHostingProvider for Bitbucket {
4977
}
5078

5179
fn format_line_number(&self, line: u32) -> String {
80+
if self.is_self_hosted() {
81+
return format!("{line}");
82+
}
5283
format!("lines-{line}")
5384
}
5485

5586
fn format_line_numbers(&self, start_line: u32, end_line: u32) -> String {
87+
if self.is_self_hosted() {
88+
return format!("{start_line}-{end_line}");
89+
}
5690
format!("lines-{start_line}:{end_line}")
5791
}
5892

5993
fn parse_remote_url(&self, url: &str) -> Option<ParsedGitRemote> {
6094
let url = RemoteUrl::from_str(url).ok()?;
6195

6296
let host = url.host_str()?;
63-
if host != "bitbucket.org" {
97+
if host != self.base_url.host_str()? {
6498
return None;
6599
}
66100

@@ -81,7 +115,12 @@ impl GitHostingProvider for Bitbucket {
81115
) -> Url {
82116
let BuildCommitPermalinkParams { sha } = params;
83117
let ParsedGitRemote { owner, repo } = remote;
84-
118+
if self.is_self_hosted() {
119+
return self
120+
.base_url()
121+
.join(&format!("projects/{owner}/repos/{repo}/commits/{sha}"))
122+
.unwrap();
123+
}
85124
self.base_url()
86125
.join(&format!("{owner}/{repo}/commits/{sha}"))
87126
.unwrap()
@@ -95,10 +134,18 @@ impl GitHostingProvider for Bitbucket {
95134
selection,
96135
} = params;
97136

98-
let mut permalink = self
99-
.base_url()
100-
.join(&format!("{owner}/{repo}/src/{sha}/{path}"))
101-
.unwrap();
137+
let mut permalink = if self.is_self_hosted() {
138+
self.base_url()
139+
.join(&format!(
140+
"projects/{owner}/repos/{repo}/browse/{path}?at={sha}"
141+
))
142+
.unwrap()
143+
} else {
144+
self.base_url()
145+
.join(&format!("{owner}/{repo}/src/{sha}/{path}"))
146+
.unwrap()
147+
};
148+
102149
permalink.set_fragment(
103150
selection
104151
.map(|selection| self.line_fragment(&selection))
@@ -117,7 +164,14 @@ impl GitHostingProvider for Bitbucket {
117164

118165
// Construct the PR URL in Bitbucket format
119166
let mut url = self.base_url();
120-
let path = format!("/{}/{}/pull-requests/{}", remote.owner, remote.repo, number);
167+
let path = if self.is_self_hosted() {
168+
format!(
169+
"/projects/{}/repos/{}/pull-requests/{}",
170+
remote.owner, remote.repo, number
171+
)
172+
} else {
173+
format!("/{}/{}/pull-requests/{}", remote.owner, remote.repo, number)
174+
};
121175
url.set_path(&path);
122176

123177
Some(PullRequest { number, url })
@@ -176,6 +230,60 @@ mod tests {
176230
);
177231
}
178232

233+
#[test]
234+
fn test_parse_remote_url_given_self_hosted_ssh_url() {
235+
let remote_url = "git@bitbucket.company.com:zed-industries/zed.git";
236+
237+
let parsed_remote = Bitbucket::from_remote_url(remote_url)
238+
.unwrap()
239+
.parse_remote_url(remote_url)
240+
.unwrap();
241+
242+
assert_eq!(
243+
parsed_remote,
244+
ParsedGitRemote {
245+
owner: "zed-industries".into(),
246+
repo: "zed".into(),
247+
}
248+
);
249+
}
250+
251+
#[test]
252+
fn test_parse_remote_url_given_self_hosted_https_url() {
253+
let remote_url = "https://bitbucket.company.com/zed-industries/zed.git";
254+
255+
let parsed_remote = Bitbucket::from_remote_url(remote_url)
256+
.unwrap()
257+
.parse_remote_url(remote_url)
258+
.unwrap();
259+
260+
assert_eq!(
261+
parsed_remote,
262+
ParsedGitRemote {
263+
owner: "zed-industries".into(),
264+
repo: "zed".into(),
265+
}
266+
);
267+
}
268+
269+
#[test]
270+
fn test_parse_remote_url_given_self_hosted_https_url_with_username() {
271+
let remote_url = "https://thorstenballzed@bitbucket.company.com/zed-industries/zed.git";
272+
273+
let parsed_remote = Bitbucket::from_remote_url(remote_url)
274+
.unwrap()
275+
.parse_remote_url(remote_url)
276+
.unwrap();
277+
278+
assert_eq!(
279+
parsed_remote,
280+
ParsedGitRemote {
281+
owner: "zed-industries".into(),
282+
repo: "zed".into(),
283+
}
284+
);
285+
}
286+
179287
#[test]
180288
fn test_build_bitbucket_permalink() {
181289
let permalink = Bitbucket::public_instance().build_permalink(
@@ -190,6 +298,23 @@ mod tests {
190298
assert_eq!(permalink.to_string(), expected_url.to_string())
191299
}
192300

301+
#[test]
302+
fn test_build_bitbucket_self_hosted_permalink() {
303+
let permalink =
304+
Bitbucket::from_remote_url("git@bitbucket.company.com:zed-industries/zed.git")
305+
.unwrap()
306+
.build_permalink(
307+
ParsedGitRemote {
308+
owner: "zed-industries".into(),
309+
repo: "zed".into(),
310+
},
311+
BuildPermalinkParams::new("f00b4r", &repo_path("main.rs"), None),
312+
);
313+
314+
let expected_url = "https://bitbucket.company.com/projects/zed-industries/repos/zed/browse/main.rs?at=f00b4r";
315+
assert_eq!(permalink.to_string(), expected_url.to_string())
316+
}
317+
193318
#[test]
194319
fn test_build_bitbucket_permalink_with_single_line_selection() {
195320
let permalink = Bitbucket::public_instance().build_permalink(
@@ -204,6 +329,23 @@ mod tests {
204329
assert_eq!(permalink.to_string(), expected_url.to_string())
205330
}
206331

332+
#[test]
333+
fn test_build_bitbucket_self_hosted_permalink_with_single_line_selection() {
334+
let permalink =
335+
Bitbucket::from_remote_url("https://bitbucket.company.com/zed-industries/zed.git")
336+
.unwrap()
337+
.build_permalink(
338+
ParsedGitRemote {
339+
owner: "zed-industries".into(),
340+
repo: "zed".into(),
341+
},
342+
BuildPermalinkParams::new("f00b4r", &repo_path("main.rs"), Some(6..6)),
343+
);
344+
345+
let expected_url = "https://bitbucket.company.com/projects/zed-industries/repos/zed/browse/main.rs?at=f00b4r#7";
346+
assert_eq!(permalink.to_string(), expected_url.to_string())
347+
}
348+
207349
#[test]
208350
fn test_build_bitbucket_permalink_with_multi_line_selection() {
209351
let permalink = Bitbucket::public_instance().build_permalink(
@@ -219,6 +361,23 @@ mod tests {
219361
assert_eq!(permalink.to_string(), expected_url.to_string())
220362
}
221363

364+
#[test]
365+
fn test_build_bitbucket_self_hosted_permalink_with_multi_line_selection() {
366+
let permalink =
367+
Bitbucket::from_remote_url("git@bitbucket.company.com:zed-industries/zed.git")
368+
.unwrap()
369+
.build_permalink(
370+
ParsedGitRemote {
371+
owner: "zed-industries".into(),
372+
repo: "zed".into(),
373+
},
374+
BuildPermalinkParams::new("f00b4r", &repo_path("main.rs"), Some(23..47)),
375+
);
376+
377+
let expected_url = "https://bitbucket.company.com/projects/zed-industries/repos/zed/browse/main.rs?at=f00b4r#24-48";
378+
assert_eq!(permalink.to_string(), expected_url.to_string())
379+
}
380+
222381
#[test]
223382
fn test_bitbucket_pull_requests() {
224383
use indoc::indoc;
@@ -248,4 +407,36 @@ mod tests {
248407
"https://bitbucket.org/zed-industries/zed/pull-requests/123"
249408
);
250409
}
410+
411+
#[test]
412+
fn test_bitbucket_self_hosted_pull_requests() {
413+
use indoc::indoc;
414+
415+
let remote = ParsedGitRemote {
416+
owner: "zed-industries".into(),
417+
repo: "zed".into(),
418+
};
419+
420+
let bitbucket =
421+
Bitbucket::from_remote_url("https://bitbucket.company.com/zed-industries/zed.git")
422+
.unwrap();
423+
424+
// Test message without PR reference
425+
let message = "This does not contain a pull request";
426+
assert!(bitbucket.extract_pull_request(&remote, message).is_none());
427+
428+
// Pull request number at end of first line
429+
let message = indoc! {r#"
430+
Merged in feature-branch (pull request #123)
431+
432+
Some detailed description of the changes.
433+
"#};
434+
435+
let pr = bitbucket.extract_pull_request(&remote, message).unwrap();
436+
assert_eq!(pr.number, 123);
437+
assert_eq!(
438+
pr.url.as_str(),
439+
"https://bitbucket.company.com/projects/zed-industries/repos/zed/pull-requests/123"
440+
);
441+
}
251442
}

0 commit comments

Comments
 (0)