use std::{ io::Read, ops::Deref, path::{Path, PathBuf}, sync::{Arc, Mutex}, }; use clap::{Parser, Subcommand}; use cuddle_please_frontend::{gatheres::ConfigArgs, PleaseConfig, PleaseConfigBuilder}; use cuddle_please_misc::{ get_most_significant_version, ConsoleUi, DynRemoteGitClient, DynUi, GiteaClient, GlobalArgs, LocalGitClient, NextVersion, RemoteGitEngine, StdinFn, VcsClient, }; use crate::{config_command::ConfigCommandHandler, release_command::ReleaseCommand}; #[derive(Parser)] #[command(author, version, about, long_about = None)] pub struct Command { #[command(flatten)] global: GlobalArgs, #[command(flatten)] config: ConfigArgs, #[command(subcommand)] commands: Option, #[clap(skip)] ui: DynUi, #[clap(skip)] stdin: StdinFn, } impl Default for Command { fn default() -> Self { Self::new() } } impl Command { pub fn new() -> Self { let args = std::env::args(); Self::new_from_args_with_stdin(Some(ConsoleUi::default()), args, || { let mut input = String::new(); std::io::stdin().read_to_string(&mut input)?; Ok(input) }) } pub fn new_from_args(ui: Option, i: I) -> Self where I: IntoIterator, T: Into + Clone, UIF: Into, { let mut s = Self::parse_from(i); if let Some(ui) = ui { s.ui = ui.into(); } s } pub fn new_from_args_with_stdin(ui: Option, i: I, input: F) -> Self where I: IntoIterator, T: Into + Clone, F: Fn() -> anyhow::Result + Send + Sync + 'static, UIF: Into, { let mut s = Self::parse_from(i); if let Some(ui) = ui { s.ui = ui.into(); } s.stdin = Some(Arc::new(Mutex::new(input))); s } pub fn execute(self, current_dir: Option<&Path>) -> anyhow::Result<()> { let config = self.build_config(current_dir)?; let git_client = self.get_git(config.get_source())?; let gitea_client = self.get_gitea_client(); match &self.commands { Some(Commands::Release {}) => { tracing::debug!("running command: release"); ReleaseCommand::new(config, git_client, gitea_client) .execute(self.global.dry_run)?; } Some(Commands::Config { command }) => { ConfigCommandHandler::new(self.ui, config).execute(command)?; } Some(Commands::Gitea { command }) => { let git_url = url::Url::parse(config.get_api_url())?; let mut url = String::new(); url.push_str(git_url.scheme()); url.push_str("://"); url.push_str(&git_url.host().unwrap().to_string()); if let Some(port) = git_url.port() { url.push_str(format!(":{port}").as_str()); } let client = GiteaClient::new(&url, self.global.token.as_deref()); match command { GiteaCommand::Connect {} => { client.connect(config.get_owner(), config.get_repository())?; self.ui.write_str_ln("connected succesfully go gitea"); } GiteaCommand::Tags { command } => match command { Some(GiteaTagsCommand::MostSignificant {}) => { let tags = client.get_tags(config.get_owner(), config.get_repository())?; match get_most_significant_version(tags.iter().collect()) { Some(tag) => { self.ui.write_str_ln(&format!( "found most significant tags: {}", tag.name )); } None => { self.ui.write_str_ln("found no tags with versioning schema"); } } } None => { let tags = client.get_tags(config.get_owner(), config.get_repository())?; self.ui.write_str_ln("got tags from gitea"); for tag in tags { self.ui.write_str_ln(&format!("- {}", tag.name)) } } }, GiteaCommand::SinceCommit { sha, branch } => { let commits = client.get_commits_since( config.get_owner(), config.get_repository(), Some(sha), branch, )?; self.ui.write_str_ln("got commits from gitea"); for commit in commits { self.ui.write_str_ln(&format!("- {}", commit.get_title())) } } GiteaCommand::CheckPr {} => { let pr = client.get_pull_request(config.get_owner(), config.get_repository())?; match pr { Some(index) => { self.ui.write_str_ln(&format!( "found cuddle-please (index={}) pr from gitea", index )); } None => { self.ui.write_str_ln("found no cuddle-please pr from gitea"); } } } } } Some(Commands::Doctor {}) => { match std::process::Command::new("git").arg("-v").output() { Ok(o) => { let stdout = std::str::from_utf8(&o.stdout).unwrap_or(""); self.ui.write_str_ln(&format!("OK: {}", stdout)); } Err(e) => { self.ui .write_str_ln(&format!("WARNING: git is not installed: {}", e)); } } } None => {} } Ok(()) } fn build_config(&self, current_dir: Option<&Path>) -> Result { let mut builder = &mut PleaseConfigBuilder::new(); if self.global.config_stdin { if let Some(stdin_fn) = self.stdin.clone() { let output = (stdin_fn.lock().unwrap().deref())(); builder = builder.with_stdin(output?); } } let current_dir = get_current_path(current_dir, self.config.source.clone())?; let config = builder .with_config_file(¤t_dir) .with_source(¤t_dir) .with_execution_env(std::env::vars()) .with_cli(self.config.clone()) .build()?; Ok(config) } fn get_git(&self, current_dir: &Path) -> anyhow::Result { if self.global.no_vcs { Ok(VcsClient::new_noop()) } else { VcsClient::new_git(current_dir) } } fn get_gitea_client(&self) -> DynRemoteGitClient { match self.global.engine { cuddle_please_misc::RemoteEngine::Local => Box::new(LocalGitClient::new()), cuddle_please_misc::RemoteEngine::Gitea => Box::new(GiteaClient::new( &self.config.api_url.clone().expect("api_url to be set"), self.global.token.as_deref(), )), } } } #[derive(Debug, Clone, Subcommand)] pub enum Commands { /// Config is mostly used for debugging the final config output Release {}, #[command(hide = true)] Config { #[command(subcommand)] command: ConfigCommand, }, #[command(hide = true)] Gitea { #[command(subcommand)] command: GiteaCommand, }, /// Helps you identify missing things from your execution environment for cuddle-please to function as intended Doctor {}, } #[derive(Subcommand, Debug, Clone)] pub enum ConfigCommand { /// List will list the final configuration List {}, } #[derive(Subcommand, Debug, Clone)] pub enum GiteaCommand { Connect {}, Tags { #[command(subcommand)] command: Option, }, SinceCommit { #[arg(long)] sha: String, #[arg(long)] branch: String, }, CheckPr {}, } #[derive(Subcommand, Debug, Clone)] pub enum GiteaTagsCommand { MostSignificant {}, } fn get_current_path( optional_current_dir: Option<&Path>, optional_source_path: Option, ) -> anyhow::Result { let path = optional_source_path .or_else(|| optional_current_dir.map(|p| p.to_path_buf())) // fall back on current env from environment .filter(|v| v.to_string_lossy() != "") // make sure we don't get empty values //.and_then(|p| p.canonicalize().ok()) // Make sure we get the absolute path //.context("could not find current dir, pass --source as a replacement")?; .unwrap_or(PathBuf::from(".")); if !path.exists() { anyhow::bail!("path doesn't exist {}", path.display()); } Ok(path) }