Files
kagami/src/sources/github.rs
2026-01-16 14:05:08 +09:00

216 lines
6.6 KiB
Rust

use std::collections::HashSet;
use anyhow::Context;
use octocrab::{Octocrab, Page, models};
use crate::sources::{KagamiSource, RepositoryFilter, RepositoryInfo, RepositoryVisibility};
pub struct KagamiSourceGithub {
host: Option<String>,
}
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<HashSet<RepositoryInfo>> {
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<Page<models::Repository>> = None;
let mut repos = HashSet::new();
loop {
let current: Page<models::Repository> = 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 &current.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<HashSet<RepositoryInfo>> {
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<Page<models::Repository>> = None;
loop {
let current: Page<models::Repository> = 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 &current.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<HashSet<RepositoryInfo>> {
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<Page<models::Repository>> = None;
let mut repos = HashSet::new();
loop {
let current: Page<models::Repository> = 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 &current.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(),
}
}