1use std::str::FromStr;
2
3use url::Url;
4
5use git::{
6 BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, ParsedGitRemote,
7 RemoteUrl,
8};
9
10pub struct Bitbucket {
11 name: String,
12 base_url: Url,
13}
14
15impl Bitbucket {
16 pub fn new(name: impl Into<String>, base_url: Url) -> Self {
17 Self {
18 name: name.into(),
19 base_url,
20 }
21 }
22
23 pub fn public_instance() -> Self {
24 Self::new("Bitbucket", Url::parse("https://bitbucket.org").unwrap())
25 }
26}
27
28impl GitHostingProvider for Bitbucket {
29 fn name(&self) -> String {
30 self.name.clone()
31 }
32
33 fn base_url(&self) -> Url {
34 self.base_url.clone()
35 }
36
37 fn supports_avatars(&self) -> bool {
38 false
39 }
40
41 fn format_line_number(&self, line: u32) -> String {
42 format!("lines-{line}")
43 }
44
45 fn format_line_numbers(&self, start_line: u32, end_line: u32) -> String {
46 format!("lines-{start_line}:{end_line}")
47 }
48
49 fn parse_remote_url(&self, url: &str) -> Option<ParsedGitRemote> {
50 let url = RemoteUrl::from_str(url).ok()?;
51
52 let host = url.host_str()?;
53 if host != "bitbucket.org" {
54 return None;
55 }
56
57 let mut path_segments = url.path_segments()?;
58 let owner = path_segments.next()?;
59 let repo = path_segments.next()?.trim_end_matches(".git");
60
61 Some(ParsedGitRemote {
62 owner: owner.into(),
63 repo: repo.into(),
64 })
65 }
66
67 fn build_commit_permalink(
68 &self,
69 remote: &ParsedGitRemote,
70 params: BuildCommitPermalinkParams,
71 ) -> Url {
72 let BuildCommitPermalinkParams { sha } = params;
73 let ParsedGitRemote { owner, repo } = remote;
74
75 self.base_url()
76 .join(&format!("{owner}/{repo}/commits/{sha}"))
77 .unwrap()
78 }
79
80 fn build_permalink(&self, remote: ParsedGitRemote, params: BuildPermalinkParams) -> Url {
81 let ParsedGitRemote { owner, repo } = remote;
82 let BuildPermalinkParams {
83 sha,
84 path,
85 selection,
86 } = params;
87
88 let mut permalink = self
89 .base_url()
90 .join(&format!("{owner}/{repo}/src/{sha}/{path}"))
91 .unwrap();
92 permalink.set_fragment(
93 selection
94 .map(|selection| self.line_fragment(&selection))
95 .as_deref(),
96 );
97 permalink
98 }
99}
100
101#[cfg(test)]
102mod tests {
103 use pretty_assertions::assert_eq;
104
105 use super::*;
106
107 #[test]
108 fn test_parse_remote_url_given_ssh_url() {
109 let parsed_remote = Bitbucket::public_instance()
110 .parse_remote_url("git@bitbucket.org:zed-industries/zed.git")
111 .unwrap();
112
113 assert_eq!(
114 parsed_remote,
115 ParsedGitRemote {
116 owner: "zed-industries".into(),
117 repo: "zed".into(),
118 }
119 );
120 }
121
122 #[test]
123 fn test_parse_remote_url_given_https_url() {
124 let parsed_remote = Bitbucket::public_instance()
125 .parse_remote_url("https://bitbucket.org/zed-industries/zed.git")
126 .unwrap();
127
128 assert_eq!(
129 parsed_remote,
130 ParsedGitRemote {
131 owner: "zed-industries".into(),
132 repo: "zed".into(),
133 }
134 );
135 }
136
137 #[test]
138 fn test_parse_remote_url_given_https_url_with_username() {
139 let parsed_remote = Bitbucket::public_instance()
140 .parse_remote_url("https://thorstenballzed@bitbucket.org/zed-industries/zed.git")
141 .unwrap();
142
143 assert_eq!(
144 parsed_remote,
145 ParsedGitRemote {
146 owner: "zed-industries".into(),
147 repo: "zed".into(),
148 }
149 );
150 }
151
152 #[test]
153 fn test_build_bitbucket_permalink() {
154 let permalink = Bitbucket::public_instance().build_permalink(
155 ParsedGitRemote {
156 owner: "zed-industries".into(),
157 repo: "zed".into(),
158 },
159 BuildPermalinkParams {
160 sha: "f00b4r",
161 path: "main.rs",
162 selection: None,
163 },
164 );
165
166 let expected_url = "https://bitbucket.org/zed-industries/zed/src/f00b4r/main.rs";
167 assert_eq!(permalink.to_string(), expected_url.to_string())
168 }
169
170 #[test]
171 fn test_build_bitbucket_permalink_with_single_line_selection() {
172 let permalink = Bitbucket::public_instance().build_permalink(
173 ParsedGitRemote {
174 owner: "zed-industries".into(),
175 repo: "zed".into(),
176 },
177 BuildPermalinkParams {
178 sha: "f00b4r",
179 path: "main.rs",
180 selection: Some(6..6),
181 },
182 );
183
184 let expected_url = "https://bitbucket.org/zed-industries/zed/src/f00b4r/main.rs#lines-7";
185 assert_eq!(permalink.to_string(), expected_url.to_string())
186 }
187
188 #[test]
189 fn test_build_bitbucket_permalink_with_multi_line_selection() {
190 let permalink = Bitbucket::public_instance().build_permalink(
191 ParsedGitRemote {
192 owner: "zed-industries".into(),
193 repo: "zed".into(),
194 },
195 BuildPermalinkParams {
196 sha: "f00b4r",
197 path: "main.rs",
198 selection: Some(23..47),
199 },
200 );
201
202 let expected_url =
203 "https://bitbucket.org/zed-industries/zed/src/f00b4r/main.rs#lines-24:48";
204 assert_eq!(permalink.to_string(), expected_url.to_string())
205 }
206}