1use std::sync::Arc;
2
3use anyhow::{bail, Context, Result};
4use async_trait::async_trait;
5use futures::AsyncReadExt;
6use http_client::{AsyncBody, HttpClient, HttpRequestExt, Request};
7use serde::Deserialize;
8use url::Url;
9
10use git::{
11 BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, Oid, ParsedGitRemote,
12};
13
14#[derive(Debug, Deserialize)]
15struct CommitDetails {
16 commit: Commit,
17 author: Option<User>,
18}
19
20#[derive(Debug, Deserialize)]
21struct Commit {
22 author: Author,
23}
24
25#[derive(Debug, Deserialize)]
26struct Author {
27 name: String,
28 email: String,
29 date: String,
30}
31
32#[derive(Debug, Deserialize)]
33struct User {
34 pub login: String,
35 pub id: u64,
36 pub avatar_url: String,
37}
38
39pub struct Codeberg;
40
41impl Codeberg {
42 async fn fetch_codeberg_commit_author(
43 &self,
44 repo_owner: &str,
45 repo: &str,
46 commit: &str,
47 client: &Arc<dyn HttpClient>,
48 ) -> Result<Option<User>> {
49 let url =
50 format!("https://codeberg.org/api/v1/repos/{repo_owner}/{repo}/git/commits/{commit}");
51
52 let mut request = Request::get(&url)
53 .header("Content-Type", "application/json")
54 .follow_redirects(http_client::RedirectPolicy::FollowAll);
55
56 if let Ok(codeberg_token) = std::env::var("CODEBERG_TOKEN") {
57 request = request.header("Authorization", format!("Bearer {}", codeberg_token));
58 }
59
60 let mut response = client
61 .send(request.body(AsyncBody::default())?)
62 .await
63 .with_context(|| format!("error fetching Codeberg commit details at {:?}", url))?;
64
65 let mut body = Vec::new();
66 response.body_mut().read_to_end(&mut body).await?;
67
68 if response.status().is_client_error() {
69 let text = String::from_utf8_lossy(body.as_slice());
70 bail!(
71 "status error {}, response: {text:?}",
72 response.status().as_u16()
73 );
74 }
75
76 let body_str = std::str::from_utf8(&body)?;
77
78 serde_json::from_str::<CommitDetails>(body_str)
79 .map(|commit| commit.author)
80 .context("failed to deserialize Codeberg commit details")
81 }
82}
83
84#[async_trait]
85impl GitHostingProvider for Codeberg {
86 fn name(&self) -> String {
87 "Codeberg".to_string()
88 }
89
90 fn base_url(&self) -> Url {
91 Url::parse("https://codeberg.org").unwrap()
92 }
93
94 fn supports_avatars(&self) -> bool {
95 true
96 }
97
98 fn format_line_number(&self, line: u32) -> String {
99 format!("L{line}")
100 }
101
102 fn format_line_numbers(&self, start_line: u32, end_line: u32) -> String {
103 format!("L{start_line}-L{end_line}")
104 }
105
106 fn parse_remote_url<'a>(&self, url: &'a str) -> Option<ParsedGitRemote<'a>> {
107 if url.starts_with("git@codeberg.org:") || url.starts_with("https://codeberg.org/") {
108 let repo_with_owner = url
109 .trim_start_matches("git@codeberg.org:")
110 .trim_start_matches("https://codeberg.org/")
111 .trim_end_matches(".git");
112
113 let (owner, repo) = repo_with_owner.split_once('/')?;
114
115 return Some(ParsedGitRemote { owner, repo });
116 }
117
118 None
119 }
120
121 fn build_commit_permalink(
122 &self,
123 remote: &ParsedGitRemote,
124 params: BuildCommitPermalinkParams,
125 ) -> Url {
126 let BuildCommitPermalinkParams { sha } = params;
127 let ParsedGitRemote { owner, repo } = remote;
128
129 self.base_url()
130 .join(&format!("{owner}/{repo}/commit/{sha}"))
131 .unwrap()
132 }
133
134 fn build_permalink(&self, remote: ParsedGitRemote, params: BuildPermalinkParams) -> Url {
135 let ParsedGitRemote { owner, repo } = remote;
136 let BuildPermalinkParams {
137 sha,
138 path,
139 selection,
140 } = params;
141
142 let mut permalink = self
143 .base_url()
144 .join(&format!("{owner}/{repo}/src/commit/{sha}/{path}"))
145 .unwrap();
146 permalink.set_fragment(
147 selection
148 .map(|selection| self.line_fragment(&selection))
149 .as_deref(),
150 );
151 permalink
152 }
153
154 async fn commit_author_avatar_url(
155 &self,
156 repo_owner: &str,
157 repo: &str,
158 commit: Oid,
159 http_client: Arc<dyn HttpClient>,
160 ) -> Result<Option<Url>> {
161 let commit = commit.to_string();
162 let avatar_url = self
163 .fetch_codeberg_commit_author(repo_owner, repo, &commit, &http_client)
164 .await?
165 .map(|author| Url::parse(&author.avatar_url))
166 .transpose()?;
167 Ok(avatar_url)
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174
175 #[test]
176 fn test_build_codeberg_permalink_from_ssh_url() {
177 let remote = ParsedGitRemote {
178 owner: "rajveermalviya",
179 repo: "zed",
180 };
181 let permalink = Codeberg.build_permalink(
182 remote,
183 BuildPermalinkParams {
184 sha: "faa6f979be417239b2e070dbbf6392b909224e0b",
185 path: "crates/editor/src/git/permalink.rs",
186 selection: None,
187 },
188 );
189
190 let expected_url = "https://codeberg.org/rajveermalviya/zed/src/commit/faa6f979be417239b2e070dbbf6392b909224e0b/crates/editor/src/git/permalink.rs";
191 assert_eq!(permalink.to_string(), expected_url.to_string())
192 }
193
194 #[test]
195 fn test_build_codeberg_permalink_from_ssh_url_single_line_selection() {
196 let remote = ParsedGitRemote {
197 owner: "rajveermalviya",
198 repo: "zed",
199 };
200 let permalink = Codeberg.build_permalink(
201 remote,
202 BuildPermalinkParams {
203 sha: "faa6f979be417239b2e070dbbf6392b909224e0b",
204 path: "crates/editor/src/git/permalink.rs",
205 selection: Some(6..6),
206 },
207 );
208
209 let expected_url = "https://codeberg.org/rajveermalviya/zed/src/commit/faa6f979be417239b2e070dbbf6392b909224e0b/crates/editor/src/git/permalink.rs#L7";
210 assert_eq!(permalink.to_string(), expected_url.to_string())
211 }
212
213 #[test]
214 fn test_build_codeberg_permalink_from_ssh_url_multi_line_selection() {
215 let remote = ParsedGitRemote {
216 owner: "rajveermalviya",
217 repo: "zed",
218 };
219 let permalink = Codeberg.build_permalink(
220 remote,
221 BuildPermalinkParams {
222 sha: "faa6f979be417239b2e070dbbf6392b909224e0b",
223 path: "crates/editor/src/git/permalink.rs",
224 selection: Some(23..47),
225 },
226 );
227
228 let expected_url = "https://codeberg.org/rajveermalviya/zed/src/commit/faa6f979be417239b2e070dbbf6392b909224e0b/crates/editor/src/git/permalink.rs#L24-L48";
229 assert_eq!(permalink.to_string(), expected_url.to_string())
230 }
231
232 #[test]
233 fn test_build_codeberg_permalink_from_https_url() {
234 let remote = ParsedGitRemote {
235 owner: "rajveermalviya",
236 repo: "zed",
237 };
238 let permalink = Codeberg.build_permalink(
239 remote,
240 BuildPermalinkParams {
241 sha: "faa6f979be417239b2e070dbbf6392b909224e0b",
242 path: "crates/zed/src/main.rs",
243 selection: None,
244 },
245 );
246
247 let expected_url = "https://codeberg.org/rajveermalviya/zed/src/commit/faa6f979be417239b2e070dbbf6392b909224e0b/crates/zed/src/main.rs";
248 assert_eq!(permalink.to_string(), expected_url.to_string())
249 }
250
251 #[test]
252 fn test_build_codeberg_permalink_from_https_url_single_line_selection() {
253 let remote = ParsedGitRemote {
254 owner: "rajveermalviya",
255 repo: "zed",
256 };
257 let permalink = Codeberg.build_permalink(
258 remote,
259 BuildPermalinkParams {
260 sha: "faa6f979be417239b2e070dbbf6392b909224e0b",
261 path: "crates/zed/src/main.rs",
262 selection: Some(6..6),
263 },
264 );
265
266 let expected_url = "https://codeberg.org/rajveermalviya/zed/src/commit/faa6f979be417239b2e070dbbf6392b909224e0b/crates/zed/src/main.rs#L7";
267 assert_eq!(permalink.to_string(), expected_url.to_string())
268 }
269
270 #[test]
271 fn test_build_codeberg_permalink_from_https_url_multi_line_selection() {
272 let remote = ParsedGitRemote {
273 owner: "rajveermalviya",
274 repo: "zed",
275 };
276 let permalink = Codeberg.build_permalink(
277 remote,
278 BuildPermalinkParams {
279 sha: "faa6f979be417239b2e070dbbf6392b909224e0b",
280 path: "crates/zed/src/main.rs",
281 selection: Some(23..47),
282 },
283 );
284
285 let expected_url = "https://codeberg.org/rajveermalviya/zed/src/commit/faa6f979be417239b2e070dbbf6392b909224e0b/crates/zed/src/main.rs#L24-L48";
286 assert_eq!(permalink.to_string(), expected_url.to_string())
287 }
288}