From eb61f4cd054caa54e0e3f8eb0bad31b467d07f00 Mon Sep 17 00:00:00 2001 From: h7x4 Date: Fri, 16 Jan 2026 14:03:41 +0900 Subject: [PATCH] source/github: implement --- Cargo.lock | 229 +----------------------------------------- Cargo.toml | 1 - src/sources.rs | 1 - src/sources/gitea.rs | 10 +- src/sources/github.rs | 214 ++++++++++++++++++++++++++++++++++++++- 5 files changed, 217 insertions(+), 238 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 29b2857..e6f078d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -144,28 +144,6 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" -[[package]] -name = "aws-lc-rs" -version = "1.15.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e84ce723ab67259cfeb9877c6a639ee9eb7a27b28123abd71db7f0d5d0cc9d86" -dependencies = [ - "aws-lc-sys", - "zeroize", -] - -[[package]] -name = "aws-lc-sys" -version = "0.36.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a442ece363113bd4bd4c8b18977a7798dd4d3c3383f34fb61936960e8f4ad8" -dependencies = [ - "cc", - "cmake", - "dunce", - "fs_extra", -] - [[package]] name = "backtrace" version = "0.3.76" @@ -294,17 +272,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd4932aefd12402b36c60956a4fe0035421f544799057659ff86f923657aada3" dependencies = [ "find-msvc-tools", - "jobserver", - "libc", "shlex", ] -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - [[package]] name = "cfg-if" version = "1.0.0" @@ -386,15 +356,6 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbd0f76e066e64fdc5631e3bb46381254deab9ef1158292f27c8c57e3bf3fe59" -[[package]] -name = "cmake" -version = "0.1.57" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" -dependencies = [ - "cc", -] - [[package]] name = "colorchoice" version = "1.0.3" @@ -851,7 +812,7 @@ dependencies = [ "base64ct", "bytes", "futures", - "reqwest 0.12.28", + "reqwest", "serde", "serde_json", "soft_assert", @@ -871,12 +832,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "fs_extra" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" - [[package]] name = "futures" version = "0.3.31" @@ -1018,7 +973,7 @@ checksum = "d86c6c1d8f0dfd7780151092ae4cbd84699678bb7640654dee2297c830a014a3" dependencies = [ "base64", "build-it", - "reqwest 0.12.28", + "reqwest", "serde", "serde_json", ] @@ -1042,7 +997,7 @@ dependencies = [ "itertools", "log", "percent-encoding", - "reqwest 0.12.28", + "reqwest", "serde", "serde_json", "serde_urlencoded", @@ -2480,38 +2435,6 @@ dependencies = [ "jiff-tzdb", ] -[[package]] -name = "jni" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" -dependencies = [ - "cesu8", - "cfg-if", - "combine", - "jni-sys", - "log", - "thiserror 1.0.69", - "walkdir", - "windows-sys 0.45.0", -] - -[[package]] -name = "jni-sys" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" - -[[package]] -name = "jobserver" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" -dependencies = [ - "getrandom 0.3.2", - "libc", -] - [[package]] name = "js-sys" version = "0.3.85" @@ -2558,7 +2481,6 @@ dependencies = [ "gix", "octocrab", "regex", - "reqwest 0.13.1", "sd-notify", "serde", "serde_json", @@ -3113,7 +3035,6 @@ version = "0.11.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" dependencies = [ - "aws-lc-rs", "bytes", "getrandom 0.3.2", "lru-slab", @@ -3302,46 +3223,6 @@ dependencies = [ "webpki-roots", ] -[[package]] -name = "reqwest" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04e9018c9d814e5f30cc16a0f03271aeab3571e609612d9fe78c1aa8d11c2f62" -dependencies = [ - "base64", - "bytes", - "encoding_rs", - "futures-core", - "h2", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", - "js-sys", - "log", - "mime", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls", - "rustls-pki-types", - "rustls-platform-verifier", - "serde", - "serde_json", - "sync_wrapper", - "tokio", - "tokio-rustls", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - [[package]] name = "rfc6979" version = "0.4.0" @@ -3439,7 +3320,6 @@ version = "0.23.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" dependencies = [ - "aws-lc-rs", "log", "once_cell", "ring", @@ -3471,40 +3351,12 @@ dependencies = [ "zeroize", ] -[[package]] -name = "rustls-platform-verifier" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" -dependencies = [ - "core-foundation 0.10.1", - "core-foundation-sys", - "jni", - "log", - "once_cell", - "rustls", - "rustls-native-certs", - "rustls-platform-verifier-android", - "rustls-webpki", - "security-framework 3.5.1", - "security-framework-sys", - "webpki-root-certs", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustls-platform-verifier-android" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" - [[package]] name = "rustls-webpki" version = "0.103.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" dependencies = [ - "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -4418,15 +4270,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "webpki-root-certs" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36a29fc0408b113f68cf32637857ab740edfafdf460c326cd2afaa2d84cc05dc" -dependencies = [ - "rustls-pki-types", -] - [[package]] name = "webpki-roots" version = "1.0.5" @@ -4537,15 +4380,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - [[package]] name = "windows-sys" version = "0.52.0" @@ -4582,21 +4416,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - [[package]] name = "windows-targets" version = "0.52.6" @@ -4630,12 +4449,6 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -4648,12 +4461,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -4666,12 +4473,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -4696,12 +4497,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -4714,12 +4509,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -4732,12 +4521,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -4750,12 +4533,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" diff --git a/Cargo.toml b/Cargo.toml index a640e38..9398732 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,6 @@ gitlab = "0.1807.0" gix = "0.70.0" octocrab = "0.49.5" regex = "1.12.2" -reqwest = { version = "0.13.1", features = ["json"] } sd-notify = "0.4.5" serde = "1.0.228" serde_json = "1.0.149" diff --git a/src/sources.rs b/src/sources.rs index 8cf0b1e..bc009fc 100644 --- a/src/sources.rs +++ b/src/sources.rs @@ -92,7 +92,6 @@ impl Default for RepositoryFilter { #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct RepositoryInfo { pub name: String, - pub full_name: String, pub url: String, pub icon_url: Option, pub is_fork: bool, diff --git a/src/sources/gitea.rs b/src/sources/gitea.rs index 4f3dd41..23996ae 100644 --- a/src/sources/gitea.rs +++ b/src/sources/gitea.rs @@ -25,7 +25,10 @@ impl KagamiSource for KagamiSourceGitea { const DEFAULT_HOST: &'static str = "gitea.com"; fn api_base(&self) -> String { - format!("https://{}", self.host.as_deref().unwrap_or(Self::DEFAULT_HOST)) + format!( + "https://{}", + self.host.as_deref().unwrap_or(Self::DEFAULT_HOST) + ) } async fn get_repositories_by_user( @@ -61,7 +64,6 @@ impl KagamiSource for KagamiSourceGitea { for r in repos_resp.iter() { let info = RepositoryInfo { name: r.name.clone(), - full_name: r.full_name.clone(), url: r.html_url.clone(), icon_url: if r.avatar_url.is_empty() { None @@ -106,7 +108,7 @@ impl KagamiSource for KagamiSourceGitea { ) -> anyhow::Result> { let base = self.api_base(); - let client: GiteaClient = match token { + let client: GiteaClient = match token { Some(tok) if !tok.is_empty() => GiteaClient::new(&base, Token(tok.to_string())), _ => GiteaClient::new(&base, gitea_sdk::Auth::::None), }; @@ -131,7 +133,6 @@ impl KagamiSource for KagamiSourceGitea { for r in repos_resp.iter() { let info = RepositoryInfo { name: r.name.clone(), - full_name: r.full_name.clone(), url: r.html_url.clone(), icon_url: if r.avatar_url.is_empty() { None @@ -199,7 +200,6 @@ impl KagamiSource for KagamiSourceGitea { for r in repos_resp.iter() { let info = RepositoryInfo { name: r.name.clone(), - full_name: r.full_name.clone(), url: r.html_url.clone(), icon_url: if r.avatar_url.is_empty() { None diff --git a/src/sources/github.rs b/src/sources/github.rs index e82f327..5869e40 100644 --- a/src/sources/github.rs +++ b/src/sources/github.rs @@ -1,11 +1,215 @@ +use std::collections::HashSet; + +use anyhow::Context; +use octocrab::{Octocrab, Page, models}; + +use crate::sources::{KagamiSource, RepositoryFilter, RepositoryInfo, RepositoryVisibility}; + pub struct KagamiSourceGithub { - host: String, + host: Option, } impl KagamiSourceGithub { - pub fn new(host: &str) -> Self { - Self { - host: host.to_string(), + 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(), } - } }