gitee.rs

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