1use std::str::FromStr;
2
3use anyhow::{Result, bail};
4use url::Url;
5
6use git::{
7 BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, ParsedGitRemote,
8 RemoteUrl,
9};
10
11use crate::get_host_from_git_remote_url;
12
13pub struct SourceHut {
14 name: String,
15 base_url: Url,
16}
17
18impl SourceHut {
19 pub fn new(name: &str, base_url: Url) -> Self {
20 Self {
21 name: name.to_string(),
22 base_url,
23 }
24 }
25
26 pub fn public_instance() -> Self {
27 Self::new("SourceHut", Url::parse("https://git.sr.ht").unwrap())
28 }
29
30 pub fn from_remote_url(remote_url: &str) -> Result<Self> {
31 let host = get_host_from_git_remote_url(remote_url)?;
32 if host == "git.sr.ht" {
33 bail!("the SourceHut instance is not self-hosted");
34 }
35
36 // TODO: detecting self hosted instances by checking whether "sourcehut" is in the url or not
37 // is not very reliable. See https://github.com/zed-industries/zed/issues/26393 for more
38 // information.
39 if !host.contains("sourcehut") {
40 bail!("not a SourceHut URL");
41 }
42
43 Ok(Self::new(
44 "SourceHut Self-Hosted",
45 Url::parse(&format!("https://{}", host))?,
46 ))
47 }
48}
49
50impl GitHostingProvider for SourceHut {
51 fn name(&self) -> String {
52 self.name.clone()
53 }
54
55 fn base_url(&self) -> Url {
56 self.base_url.clone()
57 }
58
59 fn supports_avatars(&self) -> bool {
60 false
61 }
62
63 fn format_line_number(&self, line: u32) -> String {
64 format!("L{line}")
65 }
66
67 fn format_line_numbers(&self, start_line: u32, end_line: u32) -> String {
68 format!("L{start_line}-{end_line}")
69 }
70
71 fn parse_remote_url(&self, url: &str) -> Option<ParsedGitRemote> {
72 let url = RemoteUrl::from_str(url).ok()?;
73
74 let host = url.host_str()?;
75 if host != self.base_url.host_str()? {
76 return None;
77 }
78
79 let mut path_segments = url.path_segments()?;
80 let owner = path_segments.next()?.trim_start_matches('~');
81 // We don't trim the `.git` suffix here like we do elsewhere, as
82 // sourcehut treats a repo with `.git` suffix as a separate repo.
83 //
84 // For example, `git@git.sr.ht:~username/repo` and `git@git.sr.ht:~username/repo.git`
85 // are two distinct repositories.
86 let repo = path_segments.next()?;
87
88 Some(ParsedGitRemote {
89 owner: owner.into(),
90 repo: repo.into(),
91 })
92 }
93
94 fn build_commit_permalink(
95 &self,
96 remote: &ParsedGitRemote,
97 params: BuildCommitPermalinkParams,
98 ) -> Url {
99 let BuildCommitPermalinkParams { sha } = params;
100 let ParsedGitRemote { owner, repo } = remote;
101
102 self.base_url()
103 .join(&format!("~{owner}/{repo}/commit/{sha}"))
104 .unwrap()
105 }
106
107 fn build_permalink(&self, remote: ParsedGitRemote, params: BuildPermalinkParams) -> Url {
108 let ParsedGitRemote { owner, repo } = remote;
109 let BuildPermalinkParams {
110 sha,
111 path,
112 selection,
113 } = params;
114
115 let mut permalink = self
116 .base_url()
117 .join(&format!("~{owner}/{repo}/tree/{sha}/item/{path}"))
118 .unwrap();
119 permalink.set_fragment(
120 selection
121 .map(|selection| self.line_fragment(&selection))
122 .as_deref(),
123 );
124 permalink
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use git::repository::repo_path;
131 use pretty_assertions::assert_eq;
132
133 use super::*;
134
135 #[test]
136 fn test_parse_remote_url_given_ssh_url() {
137 let parsed_remote = SourceHut::public_instance()
138 .parse_remote_url("git@git.sr.ht:~zed-industries/zed")
139 .unwrap();
140
141 assert_eq!(
142 parsed_remote,
143 ParsedGitRemote {
144 owner: "zed-industries".into(),
145 repo: "zed".into(),
146 }
147 );
148 }
149
150 #[test]
151 fn test_parse_remote_url_given_ssh_url_with_git_suffix() {
152 let parsed_remote = SourceHut::public_instance()
153 .parse_remote_url("git@git.sr.ht:~zed-industries/zed.git")
154 .unwrap();
155
156 assert_eq!(
157 parsed_remote,
158 ParsedGitRemote {
159 owner: "zed-industries".into(),
160 repo: "zed.git".into(),
161 }
162 );
163 }
164
165 #[test]
166 fn test_parse_remote_url_given_https_url() {
167 let parsed_remote = SourceHut::public_instance()
168 .parse_remote_url("https://git.sr.ht/~zed-industries/zed")
169 .unwrap();
170
171 assert_eq!(
172 parsed_remote,
173 ParsedGitRemote {
174 owner: "zed-industries".into(),
175 repo: "zed".into(),
176 }
177 );
178 }
179
180 #[test]
181 fn test_parse_remote_url_given_self_hosted_ssh_url() {
182 let remote_url = "git@sourcehut.org:~zed-industries/zed";
183
184 let parsed_remote = SourceHut::from_remote_url(remote_url)
185 .unwrap()
186 .parse_remote_url(remote_url)
187 .unwrap();
188
189 assert_eq!(
190 parsed_remote,
191 ParsedGitRemote {
192 owner: "zed-industries".into(),
193 repo: "zed".into(),
194 }
195 );
196 }
197
198 #[test]
199 fn test_parse_remote_url_given_self_hosted_ssh_url_with_git_suffix() {
200 let remote_url = "git@sourcehut.org:~zed-industries/zed.git";
201
202 let parsed_remote = SourceHut::from_remote_url(remote_url)
203 .unwrap()
204 .parse_remote_url(remote_url)
205 .unwrap();
206
207 assert_eq!(
208 parsed_remote,
209 ParsedGitRemote {
210 owner: "zed-industries".into(),
211 repo: "zed.git".into(),
212 }
213 );
214 }
215
216 #[test]
217 fn test_parse_remote_url_given_self_hosted_https_url() {
218 let remote_url = "https://sourcehut.org/~zed-industries/zed";
219
220 let parsed_remote = SourceHut::from_remote_url(remote_url)
221 .unwrap()
222 .parse_remote_url(remote_url)
223 .unwrap();
224
225 assert_eq!(
226 parsed_remote,
227 ParsedGitRemote {
228 owner: "zed-industries".into(),
229 repo: "zed".into(),
230 }
231 );
232 }
233
234 #[test]
235 fn test_build_sourcehut_permalink() {
236 let permalink = SourceHut::public_instance().build_permalink(
237 ParsedGitRemote {
238 owner: "zed-industries".into(),
239 repo: "zed".into(),
240 },
241 BuildPermalinkParams::new(
242 "faa6f979be417239b2e070dbbf6392b909224e0b",
243 &repo_path("crates/editor/src/git/permalink.rs"),
244 None,
245 ),
246 );
247
248 let expected_url = "https://git.sr.ht/~zed-industries/zed/tree/faa6f979be417239b2e070dbbf6392b909224e0b/item/crates/editor/src/git/permalink.rs";
249 assert_eq!(permalink.to_string(), expected_url.to_string())
250 }
251
252 #[test]
253 fn test_build_sourcehut_permalink_with_git_suffix() {
254 let permalink = SourceHut::public_instance().build_permalink(
255 ParsedGitRemote {
256 owner: "zed-industries".into(),
257 repo: "zed.git".into(),
258 },
259 BuildPermalinkParams::new(
260 "faa6f979be417239b2e070dbbf6392b909224e0b",
261 &repo_path("crates/editor/src/git/permalink.rs"),
262 None,
263 ),
264 );
265
266 let expected_url = "https://git.sr.ht/~zed-industries/zed.git/tree/faa6f979be417239b2e070dbbf6392b909224e0b/item/crates/editor/src/git/permalink.rs";
267 assert_eq!(permalink.to_string(), expected_url.to_string())
268 }
269
270 #[test]
271 fn test_build_sourcehut_self_hosted_permalink() {
272 let permalink = SourceHut::from_remote_url("https://sourcehut.org/~zed-industries/zed")
273 .unwrap()
274 .build_permalink(
275 ParsedGitRemote {
276 owner: "zed-industries".into(),
277 repo: "zed".into(),
278 },
279 BuildPermalinkParams::new(
280 "faa6f979be417239b2e070dbbf6392b909224e0b",
281 &repo_path("crates/editor/src/git/permalink.rs"),
282 None,
283 ),
284 );
285
286 let expected_url = "https://sourcehut.org/~zed-industries/zed/tree/faa6f979be417239b2e070dbbf6392b909224e0b/item/crates/editor/src/git/permalink.rs";
287 assert_eq!(permalink.to_string(), expected_url.to_string())
288 }
289
290 #[test]
291 fn test_build_sourcehut_self_hosted_permalink_with_git_suffix() {
292 let permalink = SourceHut::from_remote_url("https://sourcehut.org/~zed-industries/zed.git")
293 .unwrap()
294 .build_permalink(
295 ParsedGitRemote {
296 owner: "zed-industries".into(),
297 repo: "zed.git".into(),
298 },
299 BuildPermalinkParams::new(
300 "faa6f979be417239b2e070dbbf6392b909224e0b",
301 &repo_path("crates/editor/src/git/permalink.rs"),
302 None,
303 ),
304 );
305
306 let expected_url = "https://sourcehut.org/~zed-industries/zed.git/tree/faa6f979be417239b2e070dbbf6392b909224e0b/item/crates/editor/src/git/permalink.rs";
307 assert_eq!(permalink.to_string(), expected_url.to_string())
308 }
309
310 #[test]
311 fn test_build_sourcehut_permalink_with_single_line_selection() {
312 let permalink = SourceHut::public_instance().build_permalink(
313 ParsedGitRemote {
314 owner: "zed-industries".into(),
315 repo: "zed".into(),
316 },
317 BuildPermalinkParams::new(
318 "faa6f979be417239b2e070dbbf6392b909224e0b",
319 &repo_path("crates/editor/src/git/permalink.rs"),
320 Some(6..6),
321 ),
322 );
323
324 let expected_url = "https://git.sr.ht/~zed-industries/zed/tree/faa6f979be417239b2e070dbbf6392b909224e0b/item/crates/editor/src/git/permalink.rs#L7";
325 assert_eq!(permalink.to_string(), expected_url.to_string())
326 }
327
328 #[test]
329 fn test_build_sourcehut_permalink_with_multi_line_selection() {
330 let permalink = SourceHut::public_instance().build_permalink(
331 ParsedGitRemote {
332 owner: "zed-industries".into(),
333 repo: "zed".into(),
334 },
335 BuildPermalinkParams::new(
336 "faa6f979be417239b2e070dbbf6392b909224e0b",
337 &repo_path("crates/editor/src/git/permalink.rs"),
338 Some(23..47),
339 ),
340 );
341
342 let expected_url = "https://git.sr.ht/~zed-industries/zed/tree/faa6f979be417239b2e070dbbf6392b909224e0b/item/crates/editor/src/git/permalink.rs#L24-48";
343 assert_eq!(permalink.to_string(), expected_url.to_string())
344 }
345
346 #[test]
347 fn test_build_sourcehut_self_hosted_permalink_with_single_line_selection() {
348 let permalink = SourceHut::from_remote_url("https://sourcehut.org/~zed-industries/zed")
349 .unwrap()
350 .build_permalink(
351 ParsedGitRemote {
352 owner: "zed-industries".into(),
353 repo: "zed".into(),
354 },
355 BuildPermalinkParams::new(
356 "faa6f979be417239b2e070dbbf6392b909224e0b",
357 &repo_path("crates/editor/src/git/permalink.rs"),
358 Some(6..6),
359 ),
360 );
361
362 let expected_url = "https://sourcehut.org/~zed-industries/zed/tree/faa6f979be417239b2e070dbbf6392b909224e0b/item/crates/editor/src/git/permalink.rs#L7";
363 assert_eq!(permalink.to_string(), expected_url.to_string())
364 }
365
366 #[test]
367 fn test_build_sourcehut_self_hosted_permalink_with_multi_line_selection() {
368 let permalink = SourceHut::from_remote_url("https://sourcehut.org/~zed-industries/zed")
369 .unwrap()
370 .build_permalink(
371 ParsedGitRemote {
372 owner: "zed-industries".into(),
373 repo: "zed".into(),
374 },
375 BuildPermalinkParams::new(
376 "faa6f979be417239b2e070dbbf6392b909224e0b",
377 &repo_path("crates/editor/src/git/permalink.rs"),
378 Some(23..47),
379 ),
380 );
381
382 let expected_url = "https://sourcehut.org/~zed-industries/zed/tree/faa6f979be417239b2e070dbbf6392b909224e0b/item/crates/editor/src/git/permalink.rs#L24-48";
383 assert_eq!(permalink.to_string(), expected_url.to_string())
384 }
385}