ghfetch 0.1.0

GitHub stats in the terminal, neofetch-style
use crate::api::client::GhClient;
use crate::cli::OrgOpts;
use crate::data::languages::LanguageBreakdown;
use crate::data::stats::AggregateStats;
use crate::data::user::RepoSummary;
use anyhow::Result;
use serde::Serialize;
use std::collections::HashMap;

#[derive(Debug, Serialize)]
pub struct OrgProfile {
    pub login: String,
    pub name: Option<String>,
    pub description: Option<String>,
    pub location: Option<String>,
    pub blog: Option<String>,
    pub twitter: Option<String>,
    pub public_repos: u32,
    pub followers: u32,
    pub created: String,
    pub url: String,
    pub stats: AggregateStats,
    pub languages: Option<LanguageBreakdown>,
    pub top_repos: Option<Vec<RepoSummary>>,
}

pub async fn fetch_org_profile(
    client: &GhClient,
    orgname: &str,
    opts: &OrgOpts,
) -> Result<OrgProfile> {
    let org = client.get_org(orgname).await?;

    if client.is_authenticated() {
        fetch_org_graphql(client, org, orgname, opts).await
    } else {
        fetch_org_rest(client, org, orgname, opts).await
    }
}

async fn fetch_org_graphql(
    client: &GhClient,
    org: crate::api::types::GitHubOrg,
    orgname: &str,
    opts: &OrgOpts,
) -> Result<OrgProfile> {
    let repos = client.get_org_repos_graphql(orgname).await?;

    let stats = crate::data::stats::aggregate_from_graphql(&repos);

    let languages = if opts.show_languages() {
        let lang_map: HashMap<String, Vec<(String, u64)>> = repos
            .iter()
            .map(|r| (r.name.clone(), r.languages.clone()))
            .collect();
        Some(crate::data::languages::aggregate_languages(
            &lang_map,
            opts.effective_lang_limit(),
        ))
    } else {
        None
    };

    let top_repos = if opts.show_repos() || opts.show_full_card() {
        let mut sorted = repos;
        sorted.sort_by(|a, b| b.stargazer_count.cmp(&a.stargazer_count));
        Some(
            sorted
                .into_iter()
                .take(opts.repo_limit)
                .map(|r| RepoSummary {
                    name: r.name,
                    stars: r.stargazer_count,
                    forks: r.fork_count,
                    language: r.languages.first().map(|(n, _)| n.clone()),
                    description: r.description,
                    is_private: r.is_private,
                })
                .collect(),
        )
    } else {
        None
    };

    Ok(OrgProfile {
        login: org.login,
        name: org.name,
        description: org.description,
        location: org.location,
        blog: org.blog,
        twitter: org.twitter_username,
        public_repos: org.public_repos,
        followers: org.followers,
        created: org.created_at.format("%b %Y").to_string(),
        url: org.html_url,
        stats,
        languages,
        top_repos,
    })
}

async fn fetch_org_rest(
    client: &GhClient,
    org: crate::api::types::GitHubOrg,
    orgname: &str,
    opts: &OrgOpts,
) -> Result<OrgProfile> {
    let repos = client.get_org_repos(orgname, 100).await?;

    let stats = crate::data::stats::aggregate_from_rest(&repos);

    let languages = if opts.show_languages() {
        let mut lang_map: HashMap<String, HashMap<String, u64>> = HashMap::new();
        let to_fetch: Vec<_> = repos.iter().take(30).collect();
        for repo in to_fetch {
            if let Ok(langs) = client.get_repo_languages(orgname, &repo.name).await {
                lang_map.insert(repo.name.clone(), langs);
            }
        }
        Some(crate::data::languages::aggregate_from_rest(
            &lang_map,
            opts.effective_lang_limit(),
        ))
    } else {
        None
    };

    let top_repos = if opts.show_repos() || opts.show_full_card() {
        let mut sorted = repos;
        sorted.sort_by(|a, b| b.stargazers_count.cmp(&a.stargazers_count));
        Some(
            sorted
                .into_iter()
                .take(opts.repo_limit)
                .map(|r| RepoSummary {
                    name: r.name,
                    stars: r.stargazers_count,
                    forks: r.forks_count,
                    language: r.language,
                    description: r.description,
                    is_private: false, // REST doesn't expose this for non-owned repos
                })
                .collect(),
        )
    } else {
        None
    };

    Ok(OrgProfile {
        login: org.login,
        name: org.name,
        description: org.description,
        location: org.location,
        blog: org.blog,
        twitter: org.twitter_username,
        public_repos: org.public_repos,
        followers: org.followers,
        created: org.created_at.format("%b %Y").to_string(),
        url: org.html_url,
        stats,
        languages,
        top_repos,
    })
}