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        http_client: Arc<dyn HttpClient>,
145    ) -> Result<Option<Url>> {
146        let commit = commit.to_string();
147        let avatar_url = self
148            .fetch_gitee_commit_author(repo_owner, repo, &commit, &http_client)
149            .await?
150            .map(|author| -> Result<Url, url::ParseError> {
151                let mut url = Url::parse(&author.avatar_url)?;
152                url.set_query(Some("width=128"));
153                Ok(url)
154            })
155            .transpose()?;
156        Ok(avatar_url)
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use git::repository::repo_path;
163    use pretty_assertions::assert_eq;
164
165    use super::*;
166
167    #[test]
168    fn test_parse_remote_url_given_ssh_url() {
169        let parsed_remote = Gitee
170            .parse_remote_url("git@gitee.com:zed-industries/zed.git")
171            .unwrap();
172
173        assert_eq!(
174            parsed_remote,
175            ParsedGitRemote {
176                owner: "zed-industries".into(),
177                repo: "zed".into(),
178            }
179        );
180    }
181
182    #[test]
183    fn test_parse_remote_url_given_https_url() {
184        let parsed_remote = Gitee
185            .parse_remote_url("https://gitee.com/zed-industries/zed.git")
186            .unwrap();
187
188        assert_eq!(
189            parsed_remote,
190            ParsedGitRemote {
191                owner: "zed-industries".into(),
192                repo: "zed".into(),
193            }
194        );
195    }
196
197    #[test]
198    fn test_build_gitee_permalink() {
199        let permalink = Gitee.build_permalink(
200            ParsedGitRemote {
201                owner: "zed-industries".into(),
202                repo: "zed".into(),
203            },
204            BuildPermalinkParams::new(
205                "e5fe811d7ad0fc26934edd76f891d20bdc3bb194",
206                &repo_path("crates/editor/src/git/permalink.rs"),
207                None,
208            ),
209        );
210
211        let expected_url = "https://gitee.com/zed-industries/zed/blob/e5fe811d7ad0fc26934edd76f891d20bdc3bb194/crates/editor/src/git/permalink.rs";
212        assert_eq!(permalink.to_string(), expected_url.to_string())
213    }
214
215    #[test]
216    fn test_build_gitee_permalink_with_single_line_selection() {
217        let permalink = Gitee.build_permalink(
218            ParsedGitRemote {
219                owner: "zed-industries".into(),
220                repo: "zed".into(),
221            },
222            BuildPermalinkParams::new(
223                "e5fe811d7ad0fc26934edd76f891d20bdc3bb194",
224                &repo_path("crates/editor/src/git/permalink.rs"),
225                Some(6..6),
226            ),
227        );
228
229        let expected_url = "https://gitee.com/zed-industries/zed/blob/e5fe811d7ad0fc26934edd76f891d20bdc3bb194/crates/editor/src/git/permalink.rs#L7";
230        assert_eq!(permalink.to_string(), expected_url.to_string())
231    }
232
233    #[test]
234    fn test_build_gitee_permalink_with_multi_line_selection() {
235        let permalink = Gitee.build_permalink(
236            ParsedGitRemote {
237                owner: "zed-industries".into(),
238                repo: "zed".into(),
239            },
240            BuildPermalinkParams::new(
241                "e5fe811d7ad0fc26934edd76f891d20bdc3bb194",
242                &repo_path("crates/editor/src/git/permalink.rs"),
243                Some(23..47),
244            ),
245        );
246
247        let expected_url = "https://gitee.com/zed-industries/zed/blob/e5fe811d7ad0fc26934edd76f891d20bdc3bb194/crates/editor/src/git/permalink.rs#L24-48";
248        assert_eq!(permalink.to_string(), expected_url.to_string())
249    }
250}