codeberg.rs

  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}