bytes-radar 1.1.0

A tool for analyzing code statistics from remote repositories with hyper-fast performance
Documentation
use crate::net::traits::{GitProvider, ParsedRepository, ProviderConfig};
use async_trait::async_trait;
use reqwest::Client;
use serde::Deserialize;

#[derive(Deserialize)]
struct GitHubRepoInfo {
    default_branch: String,
}

pub struct GitHubProvider {
    token: Option<String>,
}

impl GitHubProvider {
    pub fn new() -> Self {
        Self { token: None }
    }
}

#[async_trait]
impl GitProvider for GitHubProvider {
    fn name(&self) -> &'static str {
        "github"
    }

    fn can_handle(&self, url: &str) -> bool {
        url.contains("github.com")
    }

    fn parse_url(&self, url: &str) -> Option<ParsedRepository> {
        if !self.can_handle(url) {
            return None;
        }

        let url = url.trim_end_matches('/');

        if url.contains("/tree/") {
            return self.parse_tree_url(url);
        }

        if url.contains("/commit/") {
            return self.parse_commit_url(url);
        }

        self.parse_basic_url(url)
    }

    fn build_download_urls(&self, parsed: &ParsedRepository) -> Vec<String> {
        let mut urls = Vec::new();

        if let Some(ref branch_or_commit) = parsed.branch_or_commit {
            if parsed.is_commit {
                urls.push(format!(
                    "https://github.com/{}/{}/archive/{}.tar.gz",
                    parsed.owner, parsed.repo, branch_or_commit
                ));
            } else {
                urls.push(format!(
                    "https://github.com/{}/{}/archive/refs/heads/{}.tar.gz",
                    parsed.owner, parsed.repo, branch_or_commit
                ));
                urls.push(format!(
                    "https://github.com/{}/{}/archive/refs/tags/{}.tar.gz",
                    parsed.owner, parsed.repo, branch_or_commit
                ));
            }
        }

        urls
    }

    async fn get_default_branch(
        &self,
        client: &Client,
        parsed: &ParsedRepository,
    ) -> Option<String> {
        #[cfg(not(target_arch = "wasm32"))]
        {
            let api_url = format!(
                "https://api.github.com/repos/{}/{}",
                parsed.owner, parsed.repo
            );

            let mut request = client.get(&api_url);

            if let Some(ref token) = self.token {
                request = request.header("Authorization", format!("token {}", token));
            }

            match request.send().await {
                Ok(response) => {
                    if response.status().is_success() {
                        match response.json::<GitHubRepoInfo>().await {
                            Ok(repo_info) => {
                                #[cfg(feature = "cli")]
                                log::debug!(
                                    "GitHub API: Found default branch '{}' for {}/{}",
                                    repo_info.default_branch,
                                    parsed.owner,
                                    parsed.repo
                                );
                                Some(repo_info.default_branch)
                            }
                            Err(_) => {
                                #[cfg(feature = "cli")]
                                log::debug!(
                                    "GitHub API: Failed to parse response for {}/{}",
                                    parsed.owner,
                                    parsed.repo
                                );
                                None
                            }
                        }
                    } else {
                        #[cfg(feature = "cli")]
                        log::debug!(
                            "GitHub API: Request failed with status {} for {}/{}",
                            response.status(),
                            parsed.owner,
                            parsed.repo
                        );
                        None
                    }
                }
                Err(_) => {
                    #[cfg(feature = "cli")]
                    log::debug!(
                        "GitHub API: Network error for {}/{}",
                        parsed.owner,
                        parsed.repo
                    );
                    None
                }
            }
        }

        #[cfg(target_arch = "wasm32")]
        None
    }

    fn apply_config(&mut self, config: &ProviderConfig) {
        self.token = config.credentials.get("token").cloned();
    }

    fn get_project_name(&self, url: &str) -> String {
        if let Some(parsed) = self.parse_url(url) {
            return parsed.project_name;
        }

        if let Some(filename) = url.split('/').next_back() {
            if filename.ends_with(".tar.gz") {
                return filename.trim_end_matches(".tar.gz").to_string();
            }
            if filename.ends_with(".tgz") {
                return filename.trim_end_matches(".tgz").to_string();
            }
            return filename.to_string();
        }

        "github-project".to_string()
    }
}

impl GitHubProvider {
    fn parse_tree_url(&self, url: &str) -> Option<ParsedRepository> {
        let parts: Vec<&str> = url.split('/').collect();
        if let Some(tree_pos) = parts.iter().position(|&x| x == "tree") {
            if tree_pos + 1 < parts.len() && tree_pos >= 2 {
                let owner = parts[tree_pos - 2].to_string();
                let repo = parts[tree_pos - 1].to_string();
                let branch = parts[tree_pos + 1].to_string();

                return Some(
                    ParsedRepository::new(owner, repo)
                        .with_branch(branch)
                        .with_host("github.com".to_string()),
                );
            }
        }
        None
    }

    fn parse_commit_url(&self, url: &str) -> Option<ParsedRepository> {
        let parts: Vec<&str> = url.split('/').collect();
        if let Some(commit_pos) = parts.iter().position(|&x| x == "commit") {
            if commit_pos + 1 < parts.len() && commit_pos >= 2 {
                let owner = parts[commit_pos - 2].to_string();
                let repo = parts[commit_pos - 1].to_string();
                let commit = parts[commit_pos + 1].to_string();

                return Some(
                    ParsedRepository::new(owner, repo)
                        .with_commit(commit)
                        .with_host("github.com".to_string()),
                );
            }
        }
        None
    }

    fn parse_basic_url(&self, url: &str) -> Option<ParsedRepository> {
        let parts: Vec<&str> = url.split('/').collect();
        if let Some(github_pos) = parts.iter().position(|&x| x == "github.com") {
            if github_pos + 2 < parts.len() {
                let owner = parts[github_pos + 1].to_string();
                let repo = parts[github_pos + 2].to_string();

                return Some(
                    ParsedRepository::new(owner, repo).with_host("github.com".to_string()),
                );
            }
        }

        if let Some(stripped) = url.strip_prefix("https://github.com/") {
            let parts: Vec<&str> = stripped.split('/').collect();
            if parts.len() >= 2 {
                let owner = parts[0].to_string();
                let repo = parts[1].to_string();

                return Some(
                    ParsedRepository::new(owner, repo).with_host("github.com".to_string()),
                );
            }
        }

        None
    }
}

impl Default for GitHubProvider {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_can_handle() {
        let provider = GitHubProvider::new();
        assert!(provider.can_handle("https://github.com/user/repo"));
        assert!(provider.can_handle("https://github.com/user/repo/tree/main"));
        assert!(!provider.can_handle("https://gitlab.com/user/repo"));
    }

    #[test]
    fn test_parse_basic_url() {
        let provider = GitHubProvider::new();

        let parsed = provider.parse_url("https://github.com/user/repo").unwrap();
        assert_eq!(parsed.owner, "user");
        assert_eq!(parsed.repo, "repo");
        assert_eq!(parsed.project_name, "repo@main");
        assert_eq!(parsed.branch_or_commit, None);
        assert!(!parsed.is_commit);
        assert_eq!(parsed.host.as_ref().unwrap(), "github.com");
    }

    #[test]
    fn test_parse_tree_url() {
        let provider = GitHubProvider::new();

        let parsed = provider
            .parse_url("https://github.com/user/repo/tree/develop")
            .unwrap();
        assert_eq!(parsed.owner, "user");
        assert_eq!(parsed.repo, "repo");
        assert_eq!(parsed.project_name, "repo@develop");
        assert_eq!(parsed.branch_or_commit, Some("develop".to_string()));
        assert!(!parsed.is_commit);
    }

    #[test]
    fn test_parse_commit_url() {
        let provider = GitHubProvider::new();

        let parsed = provider
            .parse_url("https://github.com/user/repo/commit/abc1234567890")
            .unwrap();
        assert_eq!(parsed.owner, "user");
        assert_eq!(parsed.repo, "repo");
        assert_eq!(parsed.project_name, "repo@abc1234");
        assert_eq!(parsed.branch_or_commit, Some("abc1234567890".to_string()));
        assert!(parsed.is_commit);
    }

    #[test]
    fn test_build_download_urls() {
        let provider = GitHubProvider::new();

        let parsed = ParsedRepository::new("user".to_string(), "repo".to_string())
            .with_branch("main".to_string());

        let urls = provider.build_download_urls(&parsed);
        assert!(urls
            .contains(&"https://github.com/user/repo/archive/refs/heads/main.tar.gz".to_string()));
        assert!(urls
            .contains(&"https://github.com/user/repo/archive/refs/tags/main.tar.gz".to_string()));
    }
}