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, 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}