1use std::ops::Range;
2
3use anyhow::{anyhow, Result};
4use language::Point;
5use url::Url;
6
7enum GitHostingProvider {
8 Github,
9 Gitlab,
10 Gitee,
11}
12
13impl GitHostingProvider {
14 fn base_url(&self) -> Url {
15 let base_url = match self {
16 Self::Github => "https://github.com",
17 Self::Gitlab => "https://gitlab.com",
18 Self::Gitee => "https://gitee.com",
19 };
20
21 Url::parse(&base_url).unwrap()
22 }
23
24 /// Returns the fragment portion of the URL for the selected lines in
25 /// the representation the [`GitHostingProvider`] expects.
26 fn line_fragment(&self, selection: &Range<Point>) -> String {
27 if selection.start.row == selection.end.row {
28 let line = selection.start.row + 1;
29
30 match self {
31 Self::Github | Self::Gitlab | Self::Gitee => format!("L{}", line),
32 }
33 } else {
34 let start_line = selection.start.row + 1;
35 let end_line = selection.end.row + 1;
36
37 match self {
38 Self::Github => format!("L{}-L{}", start_line, end_line),
39 Self::Gitlab => format!("L{}-{}", start_line, end_line),
40 Self::Gitee => format!("L{}-{}", start_line, end_line),
41 }
42 }
43 }
44}
45
46pub struct BuildPermalinkParams<'a> {
47 pub remote_url: &'a str,
48 pub sha: &'a str,
49 pub path: &'a str,
50 pub selection: Option<Range<Point>>,
51}
52
53pub fn build_permalink(params: BuildPermalinkParams) -> Result<Url> {
54 let BuildPermalinkParams {
55 remote_url,
56 sha,
57 path,
58 selection,
59 } = params;
60
61 let ParsedGitRemote {
62 provider,
63 owner,
64 repo,
65 } = parse_git_remote_url(remote_url)
66 .ok_or_else(|| anyhow!("failed to parse Git remote URL"))?;
67
68 let path = match provider {
69 GitHostingProvider::Github => format!("{owner}/{repo}/blob/{sha}/{path}"),
70 GitHostingProvider::Gitlab => format!("{owner}/{repo}/-/blob/{sha}/{path}"),
71 GitHostingProvider::Gitee => format!("{owner}/{repo}/blob/{sha}/{path}"),
72 };
73 let line_fragment = selection.map(|selection| provider.line_fragment(&selection));
74
75 let mut permalink = provider.base_url().join(&path).unwrap();
76 permalink.set_fragment(line_fragment.as_deref());
77
78 Ok(permalink)
79}
80
81struct ParsedGitRemote<'a> {
82 pub provider: GitHostingProvider,
83 pub owner: &'a str,
84 pub repo: &'a str,
85}
86
87fn parse_git_remote_url(url: &str) -> Option<ParsedGitRemote> {
88 if url.starts_with("git@github.com:") || url.starts_with("https://github.com/") {
89 let repo_with_owner = url
90 .trim_start_matches("git@github.com:")
91 .trim_start_matches("https://github.com/")
92 .trim_end_matches(".git");
93
94 let (owner, repo) = repo_with_owner.split_once("/")?;
95
96 return Some(ParsedGitRemote {
97 provider: GitHostingProvider::Github,
98 owner,
99 repo,
100 });
101 }
102
103 if url.starts_with("git@gitlab.com:") || url.starts_with("https://gitlab.com/") {
104 let repo_with_owner = url
105 .trim_start_matches("git@gitlab.com:")
106 .trim_start_matches("https://gitlab.com/")
107 .trim_end_matches(".git");
108
109 let (owner, repo) = repo_with_owner.split_once("/")?;
110
111 return Some(ParsedGitRemote {
112 provider: GitHostingProvider::Gitlab,
113 owner,
114 repo,
115 });
116 }
117
118 if url.starts_with("git@gitee.com:") || url.starts_with("https://gitee.com/") {
119 let repo_with_owner = url
120 .trim_start_matches("git@gitee.com:")
121 .trim_start_matches("https://gitee.com/")
122 .trim_end_matches(".git");
123
124 let (owner, repo) = repo_with_owner.split_once("/")?;
125
126 return Some(ParsedGitRemote {
127 provider: GitHostingProvider::Gitee,
128 owner,
129 repo,
130 });
131 }
132
133 None
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 #[test]
141 fn test_build_github_permalink_from_ssh_url() {
142 let permalink = build_permalink(BuildPermalinkParams {
143 remote_url: "git@github.com:zed-industries/zed.git",
144 sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
145 path: "crates/editor/src/git/permalink.rs",
146 selection: None,
147 })
148 .unwrap();
149
150 let expected_url = "https://github.com/zed-industries/zed/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs";
151 assert_eq!(permalink.to_string(), expected_url.to_string())
152 }
153
154 #[test]
155 fn test_build_github_permalink_from_ssh_url_single_line_selection() {
156 let permalink = build_permalink(BuildPermalinkParams {
157 remote_url: "git@github.com:zed-industries/zed.git",
158 sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
159 path: "crates/editor/src/git/permalink.rs",
160 selection: Some(Point::new(6, 1)..Point::new(6, 10)),
161 })
162 .unwrap();
163
164 let expected_url = "https://github.com/zed-industries/zed/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs#L7";
165 assert_eq!(permalink.to_string(), expected_url.to_string())
166 }
167
168 #[test]
169 fn test_build_github_permalink_from_ssh_url_multi_line_selection() {
170 let permalink = build_permalink(BuildPermalinkParams {
171 remote_url: "git@github.com:zed-industries/zed.git",
172 sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
173 path: "crates/editor/src/git/permalink.rs",
174 selection: Some(Point::new(23, 1)..Point::new(47, 10)),
175 })
176 .unwrap();
177
178 let expected_url = "https://github.com/zed-industries/zed/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs#L24-L48";
179 assert_eq!(permalink.to_string(), expected_url.to_string())
180 }
181
182 #[test]
183 fn test_build_github_permalink_from_https_url() {
184 let permalink = build_permalink(BuildPermalinkParams {
185 remote_url: "https://github.com/zed-industries/zed.git",
186 sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
187 path: "crates/zed/src/main.rs",
188 selection: None,
189 })
190 .unwrap();
191
192 let expected_url = "https://github.com/zed-industries/zed/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs";
193 assert_eq!(permalink.to_string(), expected_url.to_string())
194 }
195
196 #[test]
197 fn test_build_github_permalink_from_https_url_single_line_selection() {
198 let permalink = build_permalink(BuildPermalinkParams {
199 remote_url: "https://github.com/zed-industries/zed.git",
200 sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
201 path: "crates/zed/src/main.rs",
202 selection: Some(Point::new(6, 1)..Point::new(6, 10)),
203 })
204 .unwrap();
205
206 let expected_url = "https://github.com/zed-industries/zed/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs#L7";
207 assert_eq!(permalink.to_string(), expected_url.to_string())
208 }
209
210 #[test]
211 fn test_build_github_permalink_from_https_url_multi_line_selection() {
212 let permalink = build_permalink(BuildPermalinkParams {
213 remote_url: "https://github.com/zed-industries/zed.git",
214 sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
215 path: "crates/zed/src/main.rs",
216 selection: Some(Point::new(23, 1)..Point::new(47, 10)),
217 })
218 .unwrap();
219
220 let expected_url = "https://github.com/zed-industries/zed/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs#L24-L48";
221 assert_eq!(permalink.to_string(), expected_url.to_string())
222 }
223
224 #[test]
225 fn test_build_gitlab_permalink_from_ssh_url() {
226 let permalink = build_permalink(BuildPermalinkParams {
227 remote_url: "git@gitlab.com:zed-industries/zed.git",
228 sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
229 path: "crates/editor/src/git/permalink.rs",
230 selection: None,
231 })
232 .unwrap();
233
234 let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs";
235 assert_eq!(permalink.to_string(), expected_url.to_string())
236 }
237
238 #[test]
239 fn test_build_gitlab_permalink_from_ssh_url_single_line_selection() {
240 let permalink = build_permalink(BuildPermalinkParams {
241 remote_url: "git@gitlab.com:zed-industries/zed.git",
242 sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
243 path: "crates/editor/src/git/permalink.rs",
244 selection: Some(Point::new(6, 1)..Point::new(6, 10)),
245 })
246 .unwrap();
247
248 let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs#L7";
249 assert_eq!(permalink.to_string(), expected_url.to_string())
250 }
251
252 #[test]
253 fn test_build_gitlab_permalink_from_ssh_url_multi_line_selection() {
254 let permalink = build_permalink(BuildPermalinkParams {
255 remote_url: "git@gitlab.com:zed-industries/zed.git",
256 sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
257 path: "crates/editor/src/git/permalink.rs",
258 selection: Some(Point::new(23, 1)..Point::new(47, 10)),
259 })
260 .unwrap();
261
262 let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs#L24-48";
263 assert_eq!(permalink.to_string(), expected_url.to_string())
264 }
265
266 #[test]
267 fn test_build_gitlab_permalink_from_https_url() {
268 let permalink = build_permalink(BuildPermalinkParams {
269 remote_url: "https://gitlab.com/zed-industries/zed.git",
270 sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
271 path: "crates/zed/src/main.rs",
272 selection: None,
273 })
274 .unwrap();
275
276 let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs";
277 assert_eq!(permalink.to_string(), expected_url.to_string())
278 }
279
280 #[test]
281 fn test_build_gitlab_permalink_from_https_url_single_line_selection() {
282 let permalink = build_permalink(BuildPermalinkParams {
283 remote_url: "https://gitlab.com/zed-industries/zed.git",
284 sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
285 path: "crates/zed/src/main.rs",
286 selection: Some(Point::new(6, 1)..Point::new(6, 10)),
287 })
288 .unwrap();
289
290 let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs#L7";
291 assert_eq!(permalink.to_string(), expected_url.to_string())
292 }
293
294 #[test]
295 fn test_build_gitlab_permalink_from_https_url_multi_line_selection() {
296 let permalink = build_permalink(BuildPermalinkParams {
297 remote_url: "https://gitlab.com/zed-industries/zed.git",
298 sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
299 path: "crates/zed/src/main.rs",
300 selection: Some(Point::new(23, 1)..Point::new(47, 10)),
301 })
302 .unwrap();
303
304 let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs#L24-48";
305 assert_eq!(permalink.to_string(), expected_url.to_string())
306 }
307
308 #[test]
309 fn test_build_gitee_permalink_from_ssh_url() {
310 let permalink = build_permalink(BuildPermalinkParams {
311 remote_url: "git@gitee.com:libkitten/zed.git",
312 sha: "e5fe811d7ad0fc26934edd76f891d20bdc3bb194",
313 path: "crates/editor/src/git/permalink.rs",
314 selection: None,
315 })
316 .unwrap();
317
318 let expected_url = "https://gitee.com/libkitten/zed/blob/e5fe811d7ad0fc26934edd76f891d20bdc3bb194/crates/editor/src/git/permalink.rs";
319 assert_eq!(permalink.to_string(), expected_url.to_string())
320 }
321
322 #[test]
323 fn test_build_gitee_permalink_from_ssh_url_single_line_selection() {
324 let permalink = build_permalink(BuildPermalinkParams {
325 remote_url: "git@gitee.com:libkitten/zed.git",
326 sha: "e5fe811d7ad0fc26934edd76f891d20bdc3bb194",
327 path: "crates/editor/src/git/permalink.rs",
328 selection: Some(Point::new(6, 1)..Point::new(6, 10)),
329 })
330 .unwrap();
331
332 let expected_url = "https://gitee.com/libkitten/zed/blob/e5fe811d7ad0fc26934edd76f891d20bdc3bb194/crates/editor/src/git/permalink.rs#L7";
333 assert_eq!(permalink.to_string(), expected_url.to_string())
334 }
335
336 #[test]
337 fn test_build_gitee_permalink_from_ssh_url_multi_line_selection() {
338 let permalink = build_permalink(BuildPermalinkParams {
339 remote_url: "git@gitee.com:libkitten/zed.git",
340 sha: "e5fe811d7ad0fc26934edd76f891d20bdc3bb194",
341 path: "crates/editor/src/git/permalink.rs",
342 selection: Some(Point::new(23, 1)..Point::new(47, 10)),
343 })
344 .unwrap();
345
346 let expected_url = "https://gitee.com/libkitten/zed/blob/e5fe811d7ad0fc26934edd76f891d20bdc3bb194/crates/editor/src/git/permalink.rs#L24-48";
347 assert_eq!(permalink.to_string(), expected_url.to_string())
348 }
349
350 #[test]
351 fn test_build_gitee_permalink_from_https_url() {
352 let permalink = build_permalink(BuildPermalinkParams {
353 remote_url: "https://gitee.com/libkitten/zed.git",
354 sha: "e5fe811d7ad0fc26934edd76f891d20bdc3bb194",
355 path: "crates/zed/src/main.rs",
356 selection: None,
357 })
358 .unwrap();
359
360 let expected_url = "https://gitee.com/libkitten/zed/blob/e5fe811d7ad0fc26934edd76f891d20bdc3bb194/crates/zed/src/main.rs";
361 assert_eq!(permalink.to_string(), expected_url.to_string())
362 }
363
364 #[test]
365 fn test_build_gitee_permalink_from_https_url_single_line_selection() {
366 let permalink = build_permalink(BuildPermalinkParams {
367 remote_url: "https://gitee.com/libkitten/zed.git",
368 sha: "e5fe811d7ad0fc26934edd76f891d20bdc3bb194",
369 path: "crates/zed/src/main.rs",
370 selection: Some(Point::new(6, 1)..Point::new(6, 10)),
371 })
372 .unwrap();
373
374 let expected_url = "https://gitee.com/libkitten/zed/blob/e5fe811d7ad0fc26934edd76f891d20bdc3bb194/crates/zed/src/main.rs#L7";
375 assert_eq!(permalink.to_string(), expected_url.to_string())
376 }
377
378 #[test]
379 fn test_build_gitee_permalink_from_https_url_multi_line_selection() {
380 let permalink = build_permalink(BuildPermalinkParams {
381 remote_url: "https://gitee.com/libkitten/zed.git",
382 sha: "e5fe811d7ad0fc26934edd76f891d20bdc3bb194",
383 path: "crates/zed/src/main.rs",
384 selection: Some(Point::new(23, 1)..Point::new(47, 10)),
385 })
386 .unwrap();
387 let expected_url = "https://gitee.com/libkitten/zed/blob/e5fe811d7ad0fc26934edd76f891d20bdc3bb194/crates/zed/src/main.rs#L24-48";
388 assert_eq!(permalink.to_string(), expected_url.to_string())
389 }
390}