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 pretty_assertions::assert_eq;
93
94 use super::*;
95
96 #[test]
97 fn test_parse_remote_url_given_ssh_url() {
98 let parsed_remote = Sourcehut
99 .parse_remote_url("git@git.sr.ht:~zed-industries/zed")
100 .unwrap();
101
102 assert_eq!(
103 parsed_remote,
104 ParsedGitRemote {
105 owner: "zed-industries".into(),
106 repo: "zed".into(),
107 }
108 );
109 }
110
111 #[test]
112 fn test_parse_remote_url_given_ssh_url_with_git_suffix() {
113 let parsed_remote = Sourcehut
114 .parse_remote_url("git@git.sr.ht:~zed-industries/zed.git")
115 .unwrap();
116
117 assert_eq!(
118 parsed_remote,
119 ParsedGitRemote {
120 owner: "zed-industries".into(),
121 repo: "zed.git".into(),
122 }
123 );
124 }
125
126 #[test]
127 fn test_parse_remote_url_given_https_url() {
128 let parsed_remote = Sourcehut
129 .parse_remote_url("https://git.sr.ht/~zed-industries/zed")
130 .unwrap();
131
132 assert_eq!(
133 parsed_remote,
134 ParsedGitRemote {
135 owner: "zed-industries".into(),
136 repo: "zed".into(),
137 }
138 );
139 }
140
141 #[test]
142 fn test_build_sourcehut_permalink() {
143 let permalink = Sourcehut.build_permalink(
144 ParsedGitRemote {
145 owner: "zed-industries".into(),
146 repo: "zed".into(),
147 },
148 BuildPermalinkParams {
149 sha: "faa6f979be417239b2e070dbbf6392b909224e0b",
150 path: "crates/editor/src/git/permalink.rs",
151 selection: None,
152 },
153 );
154
155 let expected_url = "https://git.sr.ht/~zed-industries/zed/tree/faa6f979be417239b2e070dbbf6392b909224e0b/item/crates/editor/src/git/permalink.rs";
156 assert_eq!(permalink.to_string(), expected_url.to_string())
157 }
158
159 #[test]
160 fn test_build_sourcehut_permalink_with_git_suffix() {
161 let permalink = Sourcehut.build_permalink(
162 ParsedGitRemote {
163 owner: "zed-industries".into(),
164 repo: "zed.git".into(),
165 },
166 BuildPermalinkParams {
167 sha: "faa6f979be417239b2e070dbbf6392b909224e0b",
168 path: "crates/editor/src/git/permalink.rs",
169 selection: None,
170 },
171 );
172
173 let expected_url = "https://git.sr.ht/~zed-industries/zed.git/tree/faa6f979be417239b2e070dbbf6392b909224e0b/item/crates/editor/src/git/permalink.rs";
174 assert_eq!(permalink.to_string(), expected_url.to_string())
175 }
176
177 #[test]
178 fn test_build_sourcehut_permalink_with_single_line_selection() {
179 let permalink = Sourcehut.build_permalink(
180 ParsedGitRemote {
181 owner: "zed-industries".into(),
182 repo: "zed".into(),
183 },
184 BuildPermalinkParams {
185 sha: "faa6f979be417239b2e070dbbf6392b909224e0b",
186 path: "crates/editor/src/git/permalink.rs",
187 selection: Some(6..6),
188 },
189 );
190
191 let expected_url = "https://git.sr.ht/~zed-industries/zed/tree/faa6f979be417239b2e070dbbf6392b909224e0b/item/crates/editor/src/git/permalink.rs#L7";
192 assert_eq!(permalink.to_string(), expected_url.to_string())
193 }
194
195 #[test]
196 fn test_build_sourcehut_permalink_with_multi_line_selection() {
197 let permalink = Sourcehut.build_permalink(
198 ParsedGitRemote {
199 owner: "zed-industries".into(),
200 repo: "zed".into(),
201 },
202 BuildPermalinkParams {
203 sha: "faa6f979be417239b2e070dbbf6392b909224e0b",
204 path: "crates/editor/src/git/permalink.rs",
205 selection: Some(23..47),
206 },
207 );
208
209 let expected_url = "https://git.sr.ht/~zed-industries/zed/tree/faa6f979be417239b2e070dbbf6392b909224e0b/item/crates/editor/src/git/permalink.rs#L24-48";
210 assert_eq!(permalink.to_string(), expected_url.to_string())
211 }
212}