source/gitea: implement

This commit is contained in:
2026-01-16 13:03:17 +09:00
parent 0b9a7debd3
commit 6ca2e8b2f8
2 changed files with 259 additions and 13 deletions

View File

@@ -8,7 +8,9 @@ use std::collections::HashSet;
pub trait KagamiSource {
const NAME: &'static str;
const DEFAULT_URI_PREFIX: &'static str;
const DEFAULT_HOST: &'static str;
fn api_base(&self) -> String;
// Get Repository
// - Use native API if available
@@ -47,28 +49,47 @@ pub trait KagamiSource {
) -> impl std::future::Future<Output = anyhow::Result<HashSet<RepositoryInfo>>> + Send;
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum RepositoryVisibility {
Public,
Internal,
Private,
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum RegexFilter {
Include(regex::Regex),
Exclude(regex::Regex),
Include(String),
Exclude(String),
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct RepositoryFilter {
pub filter_regex: Vec<RegexFilter>,
pub include_forks: bool,
pub include_archived: bool,
pub visibility: HashSet<RepositoryVisibility>,
pub min_stars: Option<u32>,
pub max_stars: Option<u32>,
pub include_public: bool,
pub include_internal: bool,
pub include_private: bool,
}
#[derive(Debug, Clone)]
impl Default for RepositoryFilter {
fn default() -> Self {
Self {
filter_regex: Vec::new(),
include_forks: true,
include_archived: true,
min_stars: None,
max_stars: None,
include_public: true,
include_internal: true,
include_private: true,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct RepositoryInfo {
pub name: String,
pub full_name: String,

View File

@@ -1,11 +1,236 @@
use std::collections::HashSet;
use anyhow::Context;
use crate::sources::{KagamiSource, RepositoryFilter, RepositoryInfo, RepositoryVisibility};
use gitea_sdk;
use gitea_sdk::Auth::Token;
use gitea_sdk::Client as GiteaClient;
pub struct KagamiSourceGitea {
host: String,
host: Option<String>,
}
impl KagamiSourceGitea {
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 KagamiSourceGitea {
const NAME: &'static str = "gitea";
const DEFAULT_HOST: &'static str = "gitea.com";
fn api_base(&self) -> String {
format!("https://{}", self.host.as_deref().unwrap_or(Self::DEFAULT_HOST))
}
async fn get_repositories_by_user(
&self,
owner: &str,
token: Option<&str>,
_filter: &RepositoryFilter,
) -> anyhow::Result<HashSet<RepositoryInfo>> {
let base = self.api_base();
let client: GiteaClient = match token {
Some(tok) if !tok.is_empty() => GiteaClient::new(&base, Token(tok.to_string())),
_ => GiteaClient::new(&base, gitea_sdk::Auth::<String>::None),
};
let mut page = 1i64;
let mut set = HashSet::new();
loop {
let repos_resp = client
.users(owner)
.list_repos()
.limit(100)
.page(page)
.send(&client)
.await
.context("failed to list gitea user repos")?;
if repos_resp.is_empty() {
break;
}
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
} else {
Some(r.avatar_url.clone())
},
is_fork: r.fork,
is_archived: r.archived,
visibility: if r.private {
RepositoryVisibility::Private
} else if r.internal {
RepositoryVisibility::Internal
} else {
RepositoryVisibility::Public
},
stars: r.stars_count as u32,
description: if r.description.is_empty() {
None
} else {
Some(r.description.clone())
},
};
set.insert(info);
}
if repos_resp.len() < 100 {
break;
}
page += 1;
}
Ok(set)
}
async fn get_repositories_by_organization(
&self,
owner: &str,
token: Option<&str>,
// NOTE: Gitea does not have nested organizations/groups like GitLab,
_recurse_groups: bool,
_filter: &RepositoryFilter,
) -> anyhow::Result<HashSet<RepositoryInfo>> {
let base = self.api_base();
let client: GiteaClient = match token {
Some(tok) if !tok.is_empty() => GiteaClient::new(&base, Token(tok.to_string())),
_ => GiteaClient::new(&base, gitea_sdk::Auth::<String>::None),
};
let mut page = 1i64;
let mut set = HashSet::new();
loop {
let repos_resp = client
.orgs(owner)
.list_repos()
.limit(100)
.page(page)
.send(&client)
.await
.context("failed to list gitea org repos")?;
if repos_resp.is_empty() {
break;
}
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
} else {
Some(r.avatar_url.clone())
},
is_fork: r.fork,
is_archived: r.archived,
visibility: if r.private {
RepositoryVisibility::Private
} else if r.internal {
RepositoryVisibility::Internal
} else {
RepositoryVisibility::Public
},
stars: r.stars_count as u32,
description: if r.description.is_empty() {
None
} else {
Some(r.description.clone())
},
};
set.insert(info);
}
if repos_resp.len() < 100 {
break;
}
page += 1;
}
Ok(set)
}
async fn get_repositories_by_stars(
&self,
owner: &str,
token: Option<&str>,
_filter: &RepositoryFilter,
) -> anyhow::Result<HashSet<RepositoryInfo>> {
let base = self.api_base();
let client: GiteaClient = match token {
Some(tok) if !tok.is_empty() => GiteaClient::new(&base, Token(tok.to_string())),
_ => GiteaClient::new(&base, gitea_sdk::Auth::<String>::None),
};
let mut page = 1u64;
let mut set = HashSet::new();
loop {
let repos_resp = client
.users(owner)
.list_starred()
.limit(100u64)
.page(page)
.send(&client)
.await
.context("failed to list gitea starred repos")?;
if repos_resp.is_empty() {
break;
}
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
} else {
Some(r.avatar_url.clone())
},
is_fork: r.fork,
is_archived: r.archived,
visibility: if r.private {
RepositoryVisibility::Private
} else if r.internal {
RepositoryVisibility::Internal
} else {
RepositoryVisibility::Public
},
stars: r.stars_count as u32,
description: if r.description.is_empty() {
None
} else {
Some(r.description.clone())
},
};
set.insert(info);
}
if repos_resp.len() < 100 {
break;
}
page += 1;
}
Ok(set)
}
}
}