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