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