codeberg.rs

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