632 lines · 19.3 KB · Rust
RawDownload
use std::str::FromStr;use std::sync::{Arc, LazyLock}; use anyhow::{Context as _, Result, bail};use async_trait::async_trait;use futures::AsyncReadExt;use gpui::SharedString;use http_client::{AsyncBody, HttpClient, HttpRequestExt, Request};use regex::Regex;use serde::Deserialize;use url::Url;use urlencoding::encode; use git::{    BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, ParsedGitRemote,    PullRequest, RemoteUrl,}; use crate::get_host_from_git_remote_url; fn pull_request_number_regex() -> &'static Regex {    static PULL_REQUEST_NUMBER_REGEX: LazyLock<Regex> =        LazyLock::new(|| Regex::new(r"\(#(\d+)\)$").unwrap());    &PULL_REQUEST_NUMBER_REGEX} #[derive(Debug, Deserialize)]struct CommitDetails {    #[expect(        unused,        reason = "This field was found to be unused with serde library bump; it's left as is due to insufficient context on PO's side, but it *may* be fine to remove"    )]    commit: Commit,    author: Option<User>,} #[derive(Debug, Deserialize)]struct Commit {    #[expect(        unused,        reason = "This field was found to be unused with serde library bump; it's left as is due to insufficient context on PO's side, but it *may* be fine to remove"    )]    author: Author,} #[derive(Debug, Deserialize)]struct Author {    #[expect(        unused,        reason = "This field was found to be unused with serde library bump; it's left as is due to insufficient context on PO's side, but it *may* be fine to remove"    )]    email: String,} #[derive(Debug, Deserialize)]struct User {    #[expect(        unused,        reason = "This field was found to be unused with serde library bump; it's left as is due to insufficient context on PO's side, but it *may* be fine to remove"    )]    pub id: u64,    pub avatar_url: String,} #[derive(Debug)]pub struct Github {    name: String,    base_url: Url,} fn build_cdn_avatar_url(email: &str) -> Result<Url> {    let email = email.trim_start_matches('<').trim_end_matches('>');    Url::parse(&format!(        "https://avatars.githubusercontent.com/u/e?email={}&s=128",        encode(email)    ))    .context("failed to construct avatar URL")} impl Github {    pub fn new(name: impl Into<String>, base_url: Url) -> Self {        Self {            name: name.into(),            base_url,        }    }     pub fn public_instance() -> Self {        Self::new("GitHub", Url::parse("https://github.com").unwrap())    }     pub fn from_remote_url(remote_url: &str) -> Result<Self> {        let host = get_host_from_git_remote_url(remote_url)?;        if host == "github.com" {            bail!("the GitHub instance is not self-hosted");        }         // TODO: detecting self hosted instances by checking whether "github" is in the url or not        // is not very reliable. See https://github.com/zed-industries/zed/issues/26393 for more        // information.        if !host.contains("github") {            bail!("not a GitHub URL");        }         Ok(Self::new(            "GitHub Self-Hosted",            Url::parse(&format!("https://{}", host))?,        ))    }     async fn fetch_github_commit_author(        &self,        repo_owner: &str,        repo: &str,        commit: &str,        client: &Arc<dyn HttpClient>,    ) -> Result<Option<User>> {        let Some(host) = self.base_url.host_str() else {            bail!("failed to get host from github base url");        };        let url = format!("https://api.{host}/repos/{repo_owner}/{repo}/commits/{commit}");         let mut request = Request::get(&url)            .header("Content-Type", "application/json")            .follow_redirects(http_client::RedirectPolicy::FollowAll);         if let Ok(github_token) = std::env::var("GITHUB_TOKEN") {            request = request.header("Authorization", format!("Bearer {}", github_token));        }         let mut response = client            .send(request.body(AsyncBody::default())?)            .await            .with_context(|| format!("error fetching GitHub commit details at {:?}", url))?;         let mut body = Vec::new();        response.body_mut().read_to_end(&mut body).await?;         if response.status().is_client_error() {            let text = String::from_utf8_lossy(body.as_slice());            bail!(                "status error {}, response: {text:?}",                response.status().as_u16()            );        }         let body_str = std::str::from_utf8(&body)?;         serde_json::from_str::<CommitDetails>(body_str)            .map(|commit| commit.author)            .context("failed to deserialize GitHub commit details")    }} #[async_trait]impl GitHostingProvider for Github {    fn name(&self) -> String {        self.name.clone()    }     fn base_url(&self) -> Url {        self.base_url.clone()    }     fn supports_avatars(&self) -> bool {        // Avatars are not supported for self-hosted GitHub instances        // See tracking issue: https://github.com/zed-industries/zed/issues/11043        &self.name == "GitHub"    }     fn format_line_number(&self, line: u32) -> String {        format!("L{line}")    }     fn format_line_numbers(&self, start_line: u32, end_line: u32) -> String {        format!("L{start_line}-L{end_line}")    }     fn parse_remote_url(&self, url: &str) -> Option<ParsedGitRemote> {        let url = RemoteUrl::from_str(url).ok()?;         let host = url.host_str()?;        if host != self.base_url.host_str()? {            return None;        }         let mut path_segments = url.path_segments()?;        let mut owner = path_segments.next()?;        if owner.is_empty() {            owner = path_segments.next()?;        }         let repo = path_segments.next()?.trim_end_matches(".git");         Some(ParsedGitRemote {            owner: owner.into(),            repo: repo.into(),        })    }     fn build_commit_permalink(        &self,        remote: &ParsedGitRemote,        params: BuildCommitPermalinkParams,    ) -> Url {        let BuildCommitPermalinkParams { sha } = params;        let ParsedGitRemote { owner, repo } = remote;         self.base_url()            .join(&format!("{owner}/{repo}/commit/{sha}"))            .unwrap()    }     fn build_permalink(&self, remote: ParsedGitRemote, params: BuildPermalinkParams) -> Url {        let ParsedGitRemote { owner, repo } = remote;        let BuildPermalinkParams {            sha,            path,            selection,        } = params;         let mut permalink = self            .base_url()            .join(&format!("{owner}/{repo}/blob/{sha}/{path}"))            .unwrap();        if path.ends_with(".md") {            permalink.set_query(Some("plain=1"));        }        permalink.set_fragment(            selection                .map(|selection| self.line_fragment(&selection))                .as_deref(),        );        permalink    }     fn build_create_pull_request_url(        &self,        remote: &ParsedGitRemote,        source_branch: &str,    ) -> Option<Url> {        let ParsedGitRemote { owner, repo } = remote;        let encoded_source = encode(source_branch);         self.base_url()            .join(&format!("{owner}/{repo}/pull/new/{encoded_source}"))            .ok()    }     fn extract_pull_request(&self, remote: &ParsedGitRemote, message: &str) -> Option<PullRequest> {        let line = message.lines().next()?;        let capture = pull_request_number_regex().captures(line)?;        let number = capture.get(1)?.as_str().parse::<u32>().ok()?;         let mut url = self.base_url();        let path = format!("/{}/{}/pull/{}", remote.owner, remote.repo, number);        url.set_path(&path);         Some(PullRequest { number, url })    }     async fn commit_author_avatar_url(        &self,        repo_owner: &str,        repo: &str,        commit: SharedString,        author_email: Option<SharedString>,        http_client: Arc<dyn HttpClient>,    ) -> Result<Option<Url>> {        if let Some(email) = author_email {            return Ok(Some(build_cdn_avatar_url(&email)?));        }         let commit = commit.to_string();        let avatar_url = self            .fetch_github_commit_author(repo_owner, repo, &commit, &http_client)            .await?            .map(|author| -> Result<Url, url::ParseError> {                let mut url = Url::parse(&author.avatar_url)?;                url.set_query(Some("size=128"));                Ok(url)            })            .transpose()?;        Ok(avatar_url)    }} #[cfg(test)]mod tests {    use git::repository::repo_path;    use indoc::indoc;    use pretty_assertions::assert_eq;     use super::*;     #[test]    fn test_remote_url_with_root_slash() {        let remote_url = "git@github.com:/zed-industries/zed";        let parsed_remote = Github::public_instance()            .parse_remote_url(remote_url)            .unwrap();         assert_eq!(            parsed_remote,            ParsedGitRemote {                owner: "zed-industries".into(),                repo: "zed".into(),            }        );    }     #[test]    fn test_invalid_self_hosted_remote_url() {        let remote_url = "git@github.com:zed-industries/zed.git";        let github = Github::from_remote_url(remote_url);        assert!(github.is_err());    }     #[test]    fn test_from_remote_url_ssh() {        let remote_url = "git@github.my-enterprise.com:zed-industries/zed.git";        let github = Github::from_remote_url(remote_url).unwrap();         assert!(!github.supports_avatars());        assert_eq!(github.name, "GitHub Self-Hosted".to_string());        assert_eq!(            github.base_url,            Url::parse("https://github.my-enterprise.com").unwrap()        );    }     #[test]    fn test_from_remote_url_https() {        let remote_url = "https://github.my-enterprise.com/zed-industries/zed.git";        let github = Github::from_remote_url(remote_url).unwrap();         assert!(!github.supports_avatars());        assert_eq!(github.name, "GitHub Self-Hosted".to_string());        assert_eq!(            github.base_url,            Url::parse("https://github.my-enterprise.com").unwrap()        );    }     #[test]    fn test_parse_remote_url_given_self_hosted_ssh_url() {        let remote_url = "git@github.my-enterprise.com:zed-industries/zed.git";        let parsed_remote = Github::from_remote_url(remote_url)            .unwrap()            .parse_remote_url(remote_url)            .unwrap();         assert_eq!(            parsed_remote,            ParsedGitRemote {                owner: "zed-industries".into(),                repo: "zed".into(),            }        );    }     #[test]    fn test_parse_remote_url_given_self_hosted_https_url_with_subgroup() {        let remote_url = "https://github.my-enterprise.com/zed-industries/zed.git";        let parsed_remote = Github::from_remote_url(remote_url)            .unwrap()            .parse_remote_url(remote_url)            .unwrap();         assert_eq!(            parsed_remote,            ParsedGitRemote {                owner: "zed-industries".into(),                repo: "zed".into(),            }        );    }     #[test]    fn test_parse_remote_url_given_ssh_url() {        let parsed_remote = Github::public_instance()            .parse_remote_url("git@github.com:zed-industries/zed.git")            .unwrap();         assert_eq!(            parsed_remote,            ParsedGitRemote {                owner: "zed-industries".into(),                repo: "zed".into(),            }        );    }     #[test]    fn test_parse_remote_url_given_https_url() {        let parsed_remote = Github::public_instance()            .parse_remote_url("https://github.com/zed-industries/zed.git")            .unwrap();         assert_eq!(            parsed_remote,            ParsedGitRemote {                owner: "zed-industries".into(),                repo: "zed".into(),            }        );    }     #[test]    fn test_parse_remote_url_given_https_url_with_username() {        let parsed_remote = Github::public_instance()            .parse_remote_url("https://jlannister@github.com/some-org/some-repo.git")            .unwrap();         assert_eq!(            parsed_remote,            ParsedGitRemote {                owner: "some-org".into(),                repo: "some-repo".into(),            }        );    }     #[test]    fn test_build_github_permalink_from_ssh_url() {        let remote = ParsedGitRemote {            owner: "zed-industries".into(),            repo: "zed".into(),        };        let permalink = Github::public_instance().build_permalink(            remote,            BuildPermalinkParams::new(                "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",                &repo_path("crates/editor/src/git/permalink.rs"),                None,            ),        );         let expected_url = "https://github.com/zed-industries/zed/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs";        assert_eq!(permalink.to_string(), expected_url.to_string())    }     #[test]    fn test_build_github_permalink() {        let permalink = Github::public_instance().build_permalink(            ParsedGitRemote {                owner: "zed-industries".into(),                repo: "zed".into(),            },            BuildPermalinkParams::new(                "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",                &repo_path("crates/zed/src/main.rs"),                None,            ),        );         let expected_url = "https://github.com/zed-industries/zed/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs";        assert_eq!(permalink.to_string(), expected_url.to_string())    }     #[test]    fn test_build_github_permalink_with_single_line_selection() {        let permalink = Github::public_instance().build_permalink(            ParsedGitRemote {                owner: "zed-industries".into(),                repo: "zed".into(),            },            BuildPermalinkParams::new(                "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",                &repo_path("crates/editor/src/git/permalink.rs"),                Some(6..6),            ),        );         let expected_url = "https://github.com/zed-industries/zed/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs#L7";        assert_eq!(permalink.to_string(), expected_url.to_string())    }     #[test]    fn test_build_github_permalink_with_multi_line_selection() {        let permalink = Github::public_instance().build_permalink(            ParsedGitRemote {                owner: "zed-industries".into(),                repo: "zed".into(),            },            BuildPermalinkParams::new(                "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",                &repo_path("crates/editor/src/git/permalink.rs"),                Some(23..47),            ),        );         let expected_url = "https://github.com/zed-industries/zed/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs#L24-L48";        assert_eq!(permalink.to_string(), expected_url.to_string())    }     #[test]    fn test_build_github_create_pr_url() {        let remote = ParsedGitRemote {            owner: "zed-industries".into(),

Lines 1–500 of 632.