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