use std::collections::HashSet; use anyhow::Context; use octocrab::{Octocrab, Page, models}; use crate::sources::{KagamiSource, RepositoryFilter, RepositoryInfo, RepositoryVisibility}; pub struct KagamiSourceGithub { host: Option, } impl KagamiSourceGithub { pub fn new(host: Option<&str>) -> Self { Self { host: host.map(|s| s.to_string()), } } } impl KagamiSource for KagamiSourceGithub { const NAME: &'static str = "github"; const DEFAULT_HOST: &'static str = "github.com"; fn api_base(&self) -> String { match self.host.as_deref() { Some(host) if host != "github.com" => format!("https://{}/api/v3", host), _ => "https://api.github.com".to_string(), } } async fn get_repositories_by_user( &self, owner: &str, token: Option<&str>, _filter: &RepositoryFilter, ) -> anyhow::Result> { let octocrab = { let mut builder = Octocrab::builder(); if let Some(t) = token { builder = builder.personal_token(t.to_owned()); } if self.host.is_some() { builder = builder .base_uri(self.api_base()) .context("failed to set octocrab base_uri")?; } builder.build().context("failed to build octocrab client")? }; let mut page: Option> = None; let mut repos = HashSet::new(); loop { let current: Page = match page { None => octocrab .get( &format!("/users/{}/repos", owner), Some(&[("per_page", "100")]), ) .await .context("failed to fetch user repositories")?, Some(ref p) => { match octocrab.get_page(&p.next).await { Ok(Some(next_page)) => next_page, Ok(None) => break, Err(_) => break, } } }; for r in ¤t.items { let ri = map_github_repo_to_info(r); repos.insert(ri); } if current.next.is_none() { break; } else { page = Some(current); } } Ok(repos) } async fn get_repositories_by_organization( &self, owner: &str, token: Option<&str>, // NOTE: GitHub nested teams doesn't remove the organization repos; this flag is unused. _recurse_groups: bool, _filter: &RepositoryFilter, ) -> anyhow::Result> { let octocrab = { let mut builder = Octocrab::builder(); if let Some(t) = token { builder = builder.personal_token(t.to_owned()); } if self.host.is_some() { builder = builder .base_uri(self.api_base()) .context("failed to set octocrab base_uri")?; } builder.build().context("failed to build octocrab client")? }; let mut repos = HashSet::new(); let mut page: Option> = None; loop { let current: Page = match page { None => octocrab .get( &format!("/orgs/{}/repos", owner), Some(&[("per_page", "100")]), ) .await .context("failed to fetch organization repositories")?, Some(ref p) => match octocrab.get_page(&p.next).await { Ok(Some(next_page)) => next_page, Ok(None) => break, Err(_) => break, }, }; for r in ¤t.items { let ri = map_github_repo_to_info(r); repos.insert(ri); } if current.next.is_none() { break; } else { page = Some(current); } } Ok(repos) } async fn get_repositories_by_stars( &self, owner: &str, token: Option<&str>, _filter: &RepositoryFilter, ) -> anyhow::Result> { let octocrab = { let mut builder = Octocrab::builder(); if let Some(t) = token { builder = builder.personal_token(t.to_owned()); } if self.host.is_some() { builder = builder .base_uri(self.api_base()) .context("failed to set octocrab base_uri")?; } builder.build().context("failed to build octocrab client")? }; let mut page: Option> = None; let mut repos = HashSet::new(); loop { let current: Page = match page { None => octocrab .get( &format!("/users/{}/starred", owner), Some(&[("per_page", "100")]), ) .await .context("failed to fetch starred repositories")?, Some(ref p) => match octocrab.get_page(&p.next).await { Ok(Some(next_page)) => next_page, Ok(None) => break, Err(_) => break, }, }; for r in ¤t.items { let ri = map_github_repo_to_info(r); repos.insert(ri); } if current.next.is_none() { break; } else { page = Some(current); } } Ok(repos) } } fn map_github_repo_to_info(r: &models::Repository) -> RepositoryInfo { RepositoryInfo { name: r.name.clone(), // TODO: don't unwrap with string default here url: r .html_url .clone() .map(|u| u.to_string()) .unwrap_or_default(), icon_url: r.owner.as_ref().map(|o| o.avatar_url.to_string()), is_fork: r.fork.unwrap_or(false), is_archived: r.archived.unwrap_or(false), visibility: if r.private.unwrap_or(false) { RepositoryVisibility::Private } else { RepositoryVisibility::Public }, stars: r.stargazers_count.unwrap_or(0), description: r.description.clone(), } }