commands/list: init
This commit is contained in:
3
src/commands.rs
Normal file
3
src/commands.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
mod list;
|
||||
|
||||
pub use list::list;
|
||||
255
src/commands/list.rs
Normal file
255
src/commands/list.rs
Normal file
@@ -0,0 +1,255 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::{
|
||||
config::{Config, RepositoryFilterConfig, SourceConfig, SourceHostConfig, SourceHostType},
|
||||
sources::{KagamiSource, KagamiSourceGitea, KagamiSourceGithub, RepositoryInfo},
|
||||
};
|
||||
|
||||
// TODO: implement json output option
|
||||
pub async fn list(config: &Config) -> anyhow::Result<()> {
|
||||
let mut to_clone: Vec<(String, RepositoryInfo)> = Vec::new();
|
||||
|
||||
for (source_name, source_cfg) in config.sources.iter() {
|
||||
match source_cfg {
|
||||
SourceConfig::User {
|
||||
source_host,
|
||||
name,
|
||||
repository_filters,
|
||||
clone,
|
||||
..
|
||||
} => {
|
||||
if !*clone {
|
||||
continue;
|
||||
}
|
||||
let host_cfg = match resolve_host_cfg(config, source_host) {
|
||||
Ok(h) => h,
|
||||
Err(e) => {
|
||||
eprintln!("warning: {} for source '{}'", e, source_name);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let provider_results =
|
||||
fetch_for_user(&host_cfg, source_host, name, repository_filters).await;
|
||||
match provider_results {
|
||||
Ok(set) => {
|
||||
for ri in set.into_iter() {
|
||||
to_clone.push((source_name.clone(), ri));
|
||||
}
|
||||
}
|
||||
Err(e) => eprintln!("failed to fetch from source {}: {}", source_name, e),
|
||||
}
|
||||
}
|
||||
SourceConfig::Organization {
|
||||
source_host,
|
||||
name,
|
||||
recurse_groups,
|
||||
repository_filters,
|
||||
clone,
|
||||
..
|
||||
} => {
|
||||
if !*clone {
|
||||
continue;
|
||||
}
|
||||
let host_cfg = match resolve_host_cfg(config, source_host) {
|
||||
Ok(h) => h,
|
||||
Err(e) => {
|
||||
eprintln!("warning: {} for source '{}'", e, source_name);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let provider_results = fetch_for_org(
|
||||
&host_cfg,
|
||||
source_host,
|
||||
name,
|
||||
*recurse_groups,
|
||||
repository_filters,
|
||||
)
|
||||
.await;
|
||||
match provider_results {
|
||||
Ok(set) => {
|
||||
for ri in set.into_iter() {
|
||||
to_clone.push((source_name.clone(), ri));
|
||||
}
|
||||
}
|
||||
Err(e) => eprintln!("failed to fetch from source {}: {}", source_name, e),
|
||||
}
|
||||
}
|
||||
SourceConfig::UserStars {
|
||||
source_host,
|
||||
name,
|
||||
repository_filters,
|
||||
clone,
|
||||
..
|
||||
} => {
|
||||
if !*clone {
|
||||
continue;
|
||||
}
|
||||
let host_cfg = match resolve_host_cfg(config, source_host) {
|
||||
Ok(h) => h,
|
||||
Err(e) => {
|
||||
eprintln!("warning: {} for source '{}'", e, source_name);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let provider_results =
|
||||
fetch_for_stars(&host_cfg, source_host, name, repository_filters).await;
|
||||
match provider_results {
|
||||
Ok(set) => {
|
||||
for ri in set.into_iter() {
|
||||
to_clone.push((source_name.clone(), ri));
|
||||
}
|
||||
}
|
||||
Err(e) => eprintln!("failed to fetch from source {}: {}", source_name, e),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
use std::collections::HashSet as StdHashSet;
|
||||
let mut uniq = StdHashSet::new();
|
||||
println!("Repositories to be cloned ({}):", to_clone.len());
|
||||
for (src, ri) in to_clone.into_iter() {
|
||||
if uniq.insert(ri.clone()) {
|
||||
println!("[{}] {} -> {}", src, ri.name, ri.url);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn fetch_for_user(
|
||||
host_cfg: &SourceHostConfig,
|
||||
host_key: &str,
|
||||
owner: &str,
|
||||
repo_filters: &RepositoryFilterConfig,
|
||||
) -> anyhow::Result<HashSet<RepositoryInfo>> {
|
||||
match host_cfg._type {
|
||||
SourceHostType::Github => {
|
||||
let host = host_cfg.host.as_deref();
|
||||
let token = host_cfg.get_token()?;
|
||||
let provider = KagamiSourceGithub::new(host);
|
||||
provider
|
||||
.get_repositories_by_user(owner, token.as_deref(), repo_filters)
|
||||
.await
|
||||
}
|
||||
SourceHostType::Gitea => {
|
||||
let host = host_cfg.host.as_deref();
|
||||
let token = host_cfg.get_token()?;
|
||||
let provider = KagamiSourceGitea::new(host);
|
||||
provider
|
||||
.get_repositories_by_user(owner, token.as_deref(), repo_filters)
|
||||
.await
|
||||
}
|
||||
SourceHostType::Git => Ok(HashSet::new()),
|
||||
_ => anyhow::bail!(
|
||||
"source host type for '{}' not implemented in list",
|
||||
host_key
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
async fn fetch_for_org(
|
||||
host_cfg: &SourceHostConfig,
|
||||
host_key: &str,
|
||||
owner: &str,
|
||||
recurse: bool,
|
||||
repo_filters: &RepositoryFilterConfig,
|
||||
) -> anyhow::Result<HashSet<RepositoryInfo>> {
|
||||
match host_cfg._type {
|
||||
SourceHostType::Github => {
|
||||
let host = host_cfg.host.as_deref();
|
||||
let token = host_cfg.get_token()?;
|
||||
let provider = KagamiSourceGithub::new(host);
|
||||
provider
|
||||
.get_repositories_by_organization(owner, token.as_deref(), recurse, repo_filters)
|
||||
.await
|
||||
}
|
||||
SourceHostType::Gitea => {
|
||||
let host = host_cfg.host.as_deref();
|
||||
let token = host_cfg.get_token()?;
|
||||
let provider = KagamiSourceGitea::new(host);
|
||||
provider
|
||||
.get_repositories_by_organization(owner, token.as_deref(), recurse, repo_filters)
|
||||
.await
|
||||
}
|
||||
SourceHostType::Git => Ok(HashSet::new()),
|
||||
_ => anyhow::bail!(
|
||||
"source host type for '{}' not implemented in list",
|
||||
host_key
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
async fn fetch_for_stars(
|
||||
host_cfg: &SourceHostConfig,
|
||||
host_key: &str,
|
||||
owner: &str,
|
||||
repo_filters: &RepositoryFilterConfig,
|
||||
) -> anyhow::Result<HashSet<RepositoryInfo>> {
|
||||
match host_cfg._type {
|
||||
SourceHostType::Github => {
|
||||
let host = host_cfg.host.as_deref();
|
||||
let token = host_cfg.get_token()?;
|
||||
let provider = KagamiSourceGithub::new(host);
|
||||
provider
|
||||
.get_repositories_by_stars(owner, token.as_deref(), repo_filters)
|
||||
.await
|
||||
}
|
||||
SourceHostType::Gitea => {
|
||||
let host = host_cfg.host.as_deref();
|
||||
let token = host_cfg.get_token()?;
|
||||
let provider = KagamiSourceGitea::new(host);
|
||||
provider
|
||||
.get_repositories_by_stars(owner, token.as_deref(), repo_filters)
|
||||
.await
|
||||
}
|
||||
SourceHostType::Git => Ok(HashSet::new()),
|
||||
_ => anyhow::bail!(
|
||||
"source host type for '{}' not implemented in list",
|
||||
host_key
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_host_cfg(config: &Config, key: &str) -> anyhow::Result<SourceHostConfig> {
|
||||
if let Some(h) = config.source_hosts.get(key) {
|
||||
return Ok(h.clone());
|
||||
}
|
||||
|
||||
match key.to_lowercase().as_str() {
|
||||
"github" => Ok(SourceHostConfig {
|
||||
_type: SourceHostType::Github,
|
||||
host: None,
|
||||
token_file: None,
|
||||
token: None,
|
||||
}),
|
||||
"gitlab" => Ok(SourceHostConfig {
|
||||
_type: SourceHostType::Gitlab,
|
||||
host: None,
|
||||
token_file: None,
|
||||
token: None,
|
||||
}),
|
||||
"gitea" => Ok(SourceHostConfig {
|
||||
_type: SourceHostType::Gitea,
|
||||
host: None,
|
||||
token_file: None,
|
||||
token: None,
|
||||
}),
|
||||
"codeberg" | "forgejo" => Ok(SourceHostConfig {
|
||||
_type: SourceHostType::Forgejo,
|
||||
host: None,
|
||||
token_file: None,
|
||||
token: None,
|
||||
}),
|
||||
"git" => Ok(SourceHostConfig {
|
||||
_type: SourceHostType::Git,
|
||||
host: None,
|
||||
token_file: None,
|
||||
token: None,
|
||||
}),
|
||||
other => anyhow::bail!("unknown source_host '{}' and no default available", other),
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
use anyhow::Context;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
@@ -18,6 +19,21 @@ pub struct SourceHostConfig {
|
||||
pub token: Option<String>,
|
||||
}
|
||||
|
||||
impl SourceHostConfig {
|
||||
pub fn get_token(&self) -> anyhow::Result<Option<String>> {
|
||||
if let Some(ref t) = self.token {
|
||||
return Ok(Some(t.clone()));
|
||||
}
|
||||
|
||||
if let Some(ref p) = self.token_file {
|
||||
let s = std::fs::read_to_string(p).context("failed to read token file")?;
|
||||
return Ok(Some(s.trim().to_string()));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum SourceHostType {
|
||||
@@ -164,7 +180,7 @@ pub enum RegexFilter {
|
||||
Exclude(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct IssueFilterConfig {
|
||||
// - Filter by label(s)
|
||||
// - Filter by assignee(s)
|
||||
@@ -172,14 +188,7 @@ pub struct IssueFilterConfig {
|
||||
// - Filter by state (open, closed, all)
|
||||
}
|
||||
|
||||
impl Default for IssueFilterConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct CloneFilterConfig {
|
||||
// - Use native API if available
|
||||
// - Which branches to get
|
||||
@@ -188,10 +197,3 @@ pub struct CloneFilterConfig {
|
||||
// - Get Releases
|
||||
// - Get Pull Requests
|
||||
}
|
||||
|
||||
impl Default for CloneFilterConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
24
src/main.rs
24
src/main.rs
@@ -1,3 +1,4 @@
|
||||
pub mod commands;
|
||||
pub mod config;
|
||||
pub mod sources;
|
||||
|
||||
@@ -8,6 +9,7 @@ use anyhow::Context;
|
||||
use clap::{CommandFactory, Parser};
|
||||
use clap_complete::{Shell, generate};
|
||||
|
||||
use crate::commands::*;
|
||||
use config::Config;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
@@ -32,6 +34,9 @@ enum Command {
|
||||
/// Validate the configuration provided configuration file
|
||||
ValidateConfig,
|
||||
|
||||
/// List out the repositories that would be processed
|
||||
List,
|
||||
|
||||
/// Generate shell completions
|
||||
#[command(hide = true)]
|
||||
GenerateCompletions(GenerateCompletionArgs),
|
||||
@@ -47,9 +52,10 @@ struct GenerateCompletionArgs {
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let args = Args::parse();
|
||||
|
||||
if let Command::GenerateCompletions(args) = args.command {
|
||||
// Handle completions early if requested
|
||||
if let Command::GenerateCompletions(gen_args) = args.command.clone() {
|
||||
generate(
|
||||
args.shell,
|
||||
gen_args.shell,
|
||||
&mut Args::command(),
|
||||
"kagami",
|
||||
&mut std::io::stdout(),
|
||||
@@ -58,8 +64,18 @@ async fn main() -> anyhow::Result<()> {
|
||||
}
|
||||
|
||||
let raw_config = fs::read_to_string(&args.config).context("failed to read config file")?;
|
||||
let _config: Config =
|
||||
let config: Config =
|
||||
serde_json::from_str(&raw_config).context("failed to parse config file")?;
|
||||
|
||||
unimplemented!()
|
||||
match args.command {
|
||||
Command::ValidateConfig => {
|
||||
println!("configuration parsed successfully");
|
||||
return Ok(());
|
||||
}
|
||||
Command::List => list(&config).await,
|
||||
_ => {
|
||||
// other commands not implemented yet
|
||||
unimplemented!();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,16 @@ pub mod gitea;
|
||||
pub mod github;
|
||||
pub mod gitlab;
|
||||
|
||||
// pub use forgejo::KagamiSourceForgejo;
|
||||
pub use git::KagamiSourceGit;
|
||||
pub use gitea::KagamiSourceGitea;
|
||||
pub use github::KagamiSourceGithub;
|
||||
// pub use gitlab::KagamiSourceGitLab;
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::config::RepositoryFilterConfig;
|
||||
|
||||
pub trait KagamiSource {
|
||||
const NAME: &'static str;
|
||||
const DEFAULT_HOST: &'static str;
|
||||
@@ -30,7 +38,7 @@ pub trait KagamiSource {
|
||||
&self,
|
||||
owner: &str,
|
||||
token: Option<&str>,
|
||||
filter: &RepositoryFilter,
|
||||
filter: &RepositoryFilterConfig,
|
||||
) -> impl std::future::Future<Output = anyhow::Result<HashSet<RepositoryInfo>>> + Send;
|
||||
|
||||
fn get_repositories_by_organization(
|
||||
@@ -38,14 +46,14 @@ pub trait KagamiSource {
|
||||
owner: &str,
|
||||
token: Option<&str>,
|
||||
recurse_groups: bool,
|
||||
filter: &RepositoryFilter,
|
||||
filter: &RepositoryFilterConfig,
|
||||
) -> impl std::future::Future<Output = anyhow::Result<HashSet<RepositoryInfo>>> + Send;
|
||||
|
||||
fn get_repositories_by_stars(
|
||||
&self,
|
||||
owner: &str,
|
||||
token: Option<&str>,
|
||||
filter: &RepositoryFilter,
|
||||
filter: &RepositoryFilterConfig,
|
||||
) -> impl std::future::Future<Output = anyhow::Result<HashSet<RepositoryInfo>>> + Send;
|
||||
}
|
||||
|
||||
@@ -56,39 +64,6 @@ pub enum RepositoryVisibility {
|
||||
Private,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum RegexFilter {
|
||||
Include(String),
|
||||
Exclude(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct RepositoryFilter {
|
||||
pub filter_regex: Vec<RegexFilter>,
|
||||
pub include_forks: bool,
|
||||
pub include_archived: bool,
|
||||
pub min_stars: Option<u32>,
|
||||
pub max_stars: Option<u32>,
|
||||
pub include_public: bool,
|
||||
pub include_internal: bool,
|
||||
pub include_private: bool,
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::sources::{KagamiSource, RepositoryFilter, RepositoryInfo};
|
||||
use crate::{config::RepositoryFilterConfig, sources::{KagamiSource, RepositoryInfo}};
|
||||
|
||||
pub struct KagamiSourceGit {
|
||||
host: String,
|
||||
@@ -26,7 +26,7 @@ impl KagamiSource for KagamiSourceGit {
|
||||
&self,
|
||||
_owner: &str,
|
||||
_token: Option<&str>,
|
||||
_filter: &RepositoryFilter,
|
||||
_filter: &RepositoryFilterConfig,
|
||||
) -> anyhow::Result<HashSet<RepositoryInfo>> {
|
||||
Ok(HashSet::new())
|
||||
}
|
||||
@@ -36,7 +36,7 @@ impl KagamiSource for KagamiSourceGit {
|
||||
_owner: &str,
|
||||
_token: Option<&str>,
|
||||
_recurse_groups: bool,
|
||||
_filter: &RepositoryFilter,
|
||||
_filter: &RepositoryFilterConfig,
|
||||
) -> anyhow::Result<HashSet<RepositoryInfo>> {
|
||||
Ok(HashSet::new())
|
||||
}
|
||||
@@ -45,7 +45,7 @@ impl KagamiSource for KagamiSourceGit {
|
||||
&self,
|
||||
_owner: &str,
|
||||
_token: Option<&str>,
|
||||
_filter: &RepositoryFilter,
|
||||
_filter: &RepositoryFilterConfig,
|
||||
) -> anyhow::Result<HashSet<RepositoryInfo>> {
|
||||
Ok(HashSet::new())
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@ use std::collections::HashSet;
|
||||
|
||||
use anyhow::Context;
|
||||
|
||||
use crate::sources::{KagamiSource, RepositoryFilter, RepositoryInfo, RepositoryVisibility};
|
||||
use crate::config::RepositoryFilterConfig;
|
||||
use crate::sources::{KagamiSource, RepositoryInfo, RepositoryVisibility};
|
||||
|
||||
use gitea_sdk;
|
||||
use gitea_sdk::Auth::Token;
|
||||
@@ -35,7 +36,7 @@ impl KagamiSource for KagamiSourceGitea {
|
||||
&self,
|
||||
owner: &str,
|
||||
token: Option<&str>,
|
||||
_filter: &RepositoryFilter,
|
||||
_filter: &RepositoryFilterConfig,
|
||||
) -> anyhow::Result<HashSet<RepositoryInfo>> {
|
||||
let base = self.api_base();
|
||||
|
||||
@@ -104,7 +105,7 @@ impl KagamiSource for KagamiSourceGitea {
|
||||
token: Option<&str>,
|
||||
// NOTE: Gitea does not have nested organizations/groups like GitLab,
|
||||
_recurse_groups: bool,
|
||||
_filter: &RepositoryFilter,
|
||||
_filter: &RepositoryFilterConfig,
|
||||
) -> anyhow::Result<HashSet<RepositoryInfo>> {
|
||||
let base = self.api_base();
|
||||
|
||||
@@ -171,7 +172,7 @@ impl KagamiSource for KagamiSourceGitea {
|
||||
&self,
|
||||
owner: &str,
|
||||
token: Option<&str>,
|
||||
_filter: &RepositoryFilter,
|
||||
_filter: &RepositoryFilterConfig,
|
||||
) -> anyhow::Result<HashSet<RepositoryInfo>> {
|
||||
let base = self.api_base();
|
||||
|
||||
|
||||
@@ -3,7 +3,10 @@ use std::collections::HashSet;
|
||||
use anyhow::Context;
|
||||
use octocrab::{Octocrab, Page, models};
|
||||
|
||||
use crate::sources::{KagamiSource, RepositoryFilter, RepositoryInfo, RepositoryVisibility};
|
||||
use crate::{
|
||||
config::RepositoryFilterConfig,
|
||||
sources::{KagamiSource, RepositoryInfo, RepositoryVisibility},
|
||||
};
|
||||
|
||||
pub struct KagamiSourceGithub {
|
||||
host: Option<String>,
|
||||
@@ -32,7 +35,7 @@ impl KagamiSource for KagamiSourceGithub {
|
||||
&self,
|
||||
owner: &str,
|
||||
token: Option<&str>,
|
||||
_filter: &RepositoryFilter,
|
||||
_filter: &RepositoryFilterConfig,
|
||||
) -> anyhow::Result<HashSet<RepositoryInfo>> {
|
||||
let octocrab = {
|
||||
let mut builder = Octocrab::builder();
|
||||
@@ -59,13 +62,11 @@ impl KagamiSource for KagamiSourceGithub {
|
||||
)
|
||||
.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,
|
||||
}
|
||||
}
|
||||
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 {
|
||||
@@ -89,7 +90,7 @@ impl KagamiSource for KagamiSourceGithub {
|
||||
token: Option<&str>,
|
||||
// NOTE: GitHub nested teams doesn't remove the organization repos; this flag is unused.
|
||||
_recurse_groups: bool,
|
||||
_filter: &RepositoryFilter,
|
||||
_filter: &RepositoryFilterConfig,
|
||||
) -> anyhow::Result<HashSet<RepositoryInfo>> {
|
||||
let octocrab = {
|
||||
let mut builder = Octocrab::builder();
|
||||
@@ -142,7 +143,7 @@ impl KagamiSource for KagamiSourceGithub {
|
||||
&self,
|
||||
owner: &str,
|
||||
token: Option<&str>,
|
||||
_filter: &RepositoryFilter,
|
||||
_filter: &RepositoryFilterConfig,
|
||||
) -> anyhow::Result<HashSet<RepositoryInfo>> {
|
||||
let octocrab = {
|
||||
let mut builder = Octocrab::builder();
|
||||
|
||||
Reference in New Issue
Block a user