1use anyhow::{anyhow, bail, Result};
2use url::Url;
3use util::maybe;
4
5use git::{BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, ParsedGitRemote};
6
7#[derive(Debug)]
8pub struct Gitlab {
9 name: String,
10 base_url: Url,
11}
12
13impl Gitlab {
14 pub fn new() -> Self {
15 Self {
16 name: "GitLab".to_string(),
17 base_url: Url::parse("https://gitlab.com").unwrap(),
18 }
19 }
20
21 pub fn from_remote_url(remote_url: &str) -> Result<Self> {
22 let host = maybe!({
23 if let Some(remote_url) = remote_url.strip_prefix("git@") {
24 if let Some((host, _)) = remote_url.trim_start_matches("git@").split_once(':') {
25 return Some(host.to_string());
26 }
27 }
28
29 Url::parse(&remote_url)
30 .ok()
31 .and_then(|remote_url| remote_url.host_str().map(|host| host.to_string()))
32 })
33 .ok_or_else(|| anyhow!("URL has no host"))?;
34
35 if !host.contains("gitlab") {
36 bail!("not a GitLab URL");
37 }
38
39 Ok(Self {
40 name: "GitLab Self-Hosted".to_string(),
41 base_url: Url::parse(&format!("https://{}", host))?,
42 })
43 }
44}
45
46impl GitHostingProvider for Gitlab {
47 fn name(&self) -> String {
48 self.name.clone()
49 }
50
51 fn base_url(&self) -> Url {
52 self.base_url.clone()
53 }
54
55 fn supports_avatars(&self) -> bool {
56 false
57 }
58
59 fn format_line_number(&self, line: u32) -> String {
60 format!("L{line}")
61 }
62
63 fn format_line_numbers(&self, start_line: u32, end_line: u32) -> String {
64 format!("L{start_line}-{end_line}")
65 }
66
67 fn parse_remote_url<'a>(&self, url: &'a str) -> Option<ParsedGitRemote<'a>> {
68 let host = self.base_url.host_str()?;
69
70 if url.starts_with(&format!("git@{host}")) || url.starts_with(&format!("https://{host}/")) {
71 let repo_with_owner = url
72 .trim_start_matches(&format!("git@{host}:"))
73 .trim_start_matches(&format!("https://{host}/"))
74 .trim_end_matches(".git");
75
76 let (owner, repo) = repo_with_owner.split_once('/')?;
77
78 return Some(ParsedGitRemote { owner, repo });
79 }
80
81 None
82 }
83
84 fn build_commit_permalink(
85 &self,
86 remote: &ParsedGitRemote,
87 params: BuildCommitPermalinkParams,
88 ) -> Url {
89 let BuildCommitPermalinkParams { sha } = params;
90 let ParsedGitRemote { owner, repo } = remote;
91
92 self.base_url()
93 .join(&format!("{owner}/{repo}/-/commit/{sha}"))
94 .unwrap()
95 }
96
97 fn build_permalink(&self, remote: ParsedGitRemote, params: BuildPermalinkParams) -> Url {
98 let ParsedGitRemote { owner, repo } = remote;
99 let BuildPermalinkParams {
100 sha,
101 path,
102 selection,
103 } = params;
104
105 let mut permalink = self
106 .base_url()
107 .join(&format!("{owner}/{repo}/-/blob/{sha}/{path}"))
108 .unwrap();
109 if path.ends_with(".md") {
110 permalink.set_query(Some("plain=1"));
111 }
112 permalink.set_fragment(
113 selection
114 .map(|selection| self.line_fragment(&selection))
115 .as_deref(),
116 );
117 permalink
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use pretty_assertions::assert_eq;
124
125 use super::*;
126
127 #[test]
128 fn test_build_gitlab_permalink_from_ssh_url() {
129 let remote = ParsedGitRemote {
130 owner: "zed-industries",
131 repo: "zed",
132 };
133 let permalink = Gitlab::new().build_permalink(
134 remote,
135 BuildPermalinkParams {
136 sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
137 path: "crates/editor/src/git/permalink.rs",
138 selection: None,
139 },
140 );
141
142 let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs";
143 assert_eq!(permalink.to_string(), expected_url.to_string())
144 }
145
146 #[test]
147 fn test_build_gitlab_permalink_from_ssh_url_single_line_selection() {
148 let remote = ParsedGitRemote {
149 owner: "zed-industries",
150 repo: "zed",
151 };
152 let permalink = Gitlab::new().build_permalink(
153 remote,
154 BuildPermalinkParams {
155 sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
156 path: "crates/editor/src/git/permalink.rs",
157 selection: Some(6..6),
158 },
159 );
160
161 let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs#L7";
162 assert_eq!(permalink.to_string(), expected_url.to_string())
163 }
164
165 #[test]
166 fn test_build_gitlab_permalink_from_ssh_url_multi_line_selection() {
167 let remote = ParsedGitRemote {
168 owner: "zed-industries",
169 repo: "zed",
170 };
171 let permalink = Gitlab::new().build_permalink(
172 remote,
173 BuildPermalinkParams {
174 sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
175 path: "crates/editor/src/git/permalink.rs",
176 selection: Some(23..47),
177 },
178 );
179
180 let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs#L24-48";
181 assert_eq!(permalink.to_string(), expected_url.to_string())
182 }
183
184 #[test]
185 fn test_build_gitlab_permalink_from_https_url() {
186 let remote = ParsedGitRemote {
187 owner: "zed-industries",
188 repo: "zed",
189 };
190 let permalink = Gitlab::new().build_permalink(
191 remote,
192 BuildPermalinkParams {
193 sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
194 path: "crates/zed/src/main.rs",
195 selection: None,
196 },
197 );
198
199 let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs";
200 assert_eq!(permalink.to_string(), expected_url.to_string())
201 }
202
203 #[test]
204 fn test_build_gitlab_permalink_from_https_url_single_line_selection() {
205 let remote = ParsedGitRemote {
206 owner: "zed-industries",
207 repo: "zed",
208 };
209 let permalink = Gitlab::new().build_permalink(
210 remote,
211 BuildPermalinkParams {
212 sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
213 path: "crates/zed/src/main.rs",
214 selection: Some(6..6),
215 },
216 );
217
218 let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs#L7";
219 assert_eq!(permalink.to_string(), expected_url.to_string())
220 }
221
222 #[test]
223 fn test_build_gitlab_permalink_from_https_url_multi_line_selection() {
224 let remote = ParsedGitRemote {
225 owner: "zed-industries",
226 repo: "zed",
227 };
228 let permalink = Gitlab::new().build_permalink(
229 remote,
230 BuildPermalinkParams {
231 sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
232 path: "crates/zed/src/main.rs",
233 selection: Some(23..47),
234 },
235 );
236
237 let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs#L24-48";
238 assert_eq!(permalink.to_string(), expected_url.to_string())
239 }
240
241 #[test]
242 fn test_build_gitlab_self_hosted_permalink_from_ssh_url() {
243 let remote = ParsedGitRemote {
244 owner: "zed-industries",
245 repo: "zed",
246 };
247 let gitlab =
248 Gitlab::from_remote_url("git@gitlab.some-enterprise.com:zed-industries/zed.git")
249 .unwrap();
250 let permalink = gitlab.build_permalink(
251 remote,
252 BuildPermalinkParams {
253 sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
254 path: "crates/editor/src/git/permalink.rs",
255 selection: None,
256 },
257 );
258
259 let expected_url = "https://gitlab.some-enterprise.com/zed-industries/zed/-/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs";
260 assert_eq!(permalink.to_string(), expected_url.to_string())
261 }
262
263 #[test]
264 fn test_build_gitlab_self_hosted_permalink_from_https_url() {
265 let remote = ParsedGitRemote {
266 owner: "zed-industries",
267 repo: "zed",
268 };
269 let gitlab =
270 Gitlab::from_remote_url("https://gitlab-instance.big-co.com/zed-industries/zed.git")
271 .unwrap();
272 let permalink = gitlab.build_permalink(
273 remote,
274 BuildPermalinkParams {
275 sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
276 path: "crates/zed/src/main.rs",
277 selection: None,
278 },
279 );
280
281 let expected_url = "https://gitlab-instance.big-co.com/zed-industries/zed/-/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs";
282 assert_eq!(permalink.to_string(), expected_url.to_string())
283 }
284}