source/gitea: implement
This commit is contained in:
+29
-8
@@ -8,7 +8,9 @@ use std::collections::HashSet;
|
|||||||
|
|
||||||
pub trait KagamiSource {
|
pub trait KagamiSource {
|
||||||
const NAME: &'static str;
|
const NAME: &'static str;
|
||||||
const DEFAULT_URI_PREFIX: &'static str;
|
const DEFAULT_HOST: &'static str;
|
||||||
|
|
||||||
|
fn api_base(&self) -> String;
|
||||||
|
|
||||||
// Get Repository
|
// Get Repository
|
||||||
// - Use native API if available
|
// - Use native API if available
|
||||||
@@ -47,28 +49,47 @@ pub trait KagamiSource {
|
|||||||
) -> impl std::future::Future<Output = anyhow::Result<HashSet<RepositoryInfo>>> + Send;
|
) -> impl std::future::Future<Output = anyhow::Result<HashSet<RepositoryInfo>>> + Send;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
pub enum RepositoryVisibility {
|
pub enum RepositoryVisibility {
|
||||||
Public,
|
Public,
|
||||||
Internal,
|
Internal,
|
||||||
Private,
|
Private,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
pub enum RegexFilter {
|
pub enum RegexFilter {
|
||||||
Include(regex::Regex),
|
Include(String),
|
||||||
Exclude(regex::Regex),
|
Exclude(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
pub struct RepositoryFilter {
|
pub struct RepositoryFilter {
|
||||||
pub filter_regex: Vec<RegexFilter>,
|
pub filter_regex: Vec<RegexFilter>,
|
||||||
pub include_forks: bool,
|
pub include_forks: bool,
|
||||||
pub include_archived: 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 struct RepositoryInfo {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub full_name: String,
|
pub full_name: String,
|
||||||
|
|||||||
+230
-5
@@ -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 {
|
pub struct KagamiSourceGitea {
|
||||||
host: String,
|
host: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl KagamiSourceGitea {
|
impl KagamiSourceGitea {
|
||||||
pub fn new(host: &str) -> Self {
|
pub fn new(host: Option<&str>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
host: host.to_string(),
|
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)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user