From 413f71fd33d2be8547b885d71b426464be54d1dd Mon Sep 17 00:00:00 2001 From: wenqian Date: Tue, 8 Aug 2023 22:29:54 +0800 Subject: [PATCH] feat: add run command (#25) --- src/batch.rs | 1 + src/cmd/app.rs | 3 + src/cmd/complete/mod.rs | 1 + src/cmd/complete/run.rs | 21 +++++ src/cmd/run/complete.rs | 2 + src/cmd/run/home.rs | 19 ++--- src/cmd/run/mod.rs | 1 + src/cmd/run/run.rs | 171 ++++++++++++++++++++++++++++++++++++++++ src/shell.rs | 132 +++++++++++++++++++++++++------ 9 files changed, 311 insertions(+), 40 deletions(-) create mode 100644 src/cmd/complete/run.rs create mode 100644 src/cmd/run/run.rs diff --git a/src/batch.rs b/src/batch.rs index e79e250..db3a8ab 100644 --- a/src/batch.rs +++ b/src/batch.rs @@ -164,6 +164,7 @@ where handler.join().unwrap(); } + println!(); println!( "{desc} done, with {} successed, {} failed", style(suc_count).green(), diff --git a/src/cmd/app.rs b/src/cmd/app.rs index b4d79ce..9080f19 100644 --- a/src/cmd/app.rs +++ b/src/cmd/app.rs @@ -17,6 +17,7 @@ use crate::cmd::run::rebase::RebaseArgs; use crate::cmd::run::release::ReleaseArgs; use crate::cmd::run::remove::RemoveArgs; use crate::cmd::run::reset::ResetArgs; +use crate::cmd::run::run::RunArgs; use crate::cmd::run::squash::SquashArgs; use crate::cmd::run::tag::TagArgs; use crate::cmd::run::update::UpdateArgs; @@ -51,6 +52,7 @@ pub enum Commands { Update(UpdateArgs), Clear(ClearArgs), Import(ImportArgs), + Run(RunArgs), } impl Run for App { @@ -79,6 +81,7 @@ impl Run for App { Commands::Update(args) => args.run(), Commands::Clear(args) => args.run(), Commands::Import(args) => args.run(), + Commands::Run(args) => args.run(), } } } diff --git a/src/cmd/complete/mod.rs b/src/cmd/complete/mod.rs index c43d6b3..1706947 100644 --- a/src/cmd/complete/mod.rs +++ b/src/cmd/complete/mod.rs @@ -4,6 +4,7 @@ pub mod home; pub mod owner; pub mod release; pub mod remote; +pub mod run; pub mod tag; pub struct Complete { diff --git a/src/cmd/complete/run.rs b/src/cmd/complete/run.rs new file mode 100644 index 0000000..17e6e72 --- /dev/null +++ b/src/cmd/complete/run.rs @@ -0,0 +1,21 @@ +use anyhow::Result; + +use crate::cmd::complete::home; +use crate::cmd::complete::Complete; +use crate::config; + +pub fn complete(args: &[&str]) -> Result { + if args.is_empty() { + return Ok(Complete::empty()); + } + if args.len() == 1 { + let mut names: Vec = config::base() + .workflows + .iter() + .map(|(key, _)| key.clone()) + .collect(); + names.sort(); + return Ok(Complete::from(names)); + } + home::complete(&args[1..]) +} diff --git a/src/cmd/run/complete.rs b/src/cmd/run/complete.rs index a8d3904..396d48a 100644 --- a/src/cmd/run/complete.rs +++ b/src/cmd/run/complete.rs @@ -9,6 +9,7 @@ use crate::cmd::complete::home; use crate::cmd::complete::owner; use crate::cmd::complete::release; use crate::cmd::complete::remote; +use crate::cmd::complete::run; use crate::cmd::complete::tag; use crate::cmd::complete::Complete; use crate::cmd::Run; @@ -58,6 +59,7 @@ impl CompleteArgs { "update" => no_complete, "clear" => owner::complete, "import" => owner::complete, + "run" => run::complete, } } diff --git a/src/cmd/run/home.rs b/src/cmd/run/home.rs index 6e1e095..22661c3 100644 --- a/src/cmd/run/home.rs +++ b/src/cmd/run/home.rs @@ -3,15 +3,15 @@ use std::io::ErrorKind; use std::path::PathBuf; use std::rc::Rc; -use anyhow::{bail, Context, Result}; +use anyhow::{Context, Result}; use clap::Args; use crate::cmd::Run; use crate::config::types::Remote; use crate::repo::database::Database; use crate::repo::types::{NameLevel, Repo}; -use crate::shell::Shell; -use crate::{api, info, shell}; +use crate::shell::{Shell, Workflow}; +use crate::{api, shell}; use crate::{config, confirm, utils}; /// Print the home path of a repo, recommand to use `zz` command instead. @@ -158,17 +158,8 @@ impl HomeArgs { if let Some(owner) = remote.owners.get(repo.owner.as_str()) { if let Some(workflow_names) = &owner.on_create { for workflow_name in workflow_names.iter() { - let maybe_workflow = config::base().workflows.get(workflow_name); - if let None = maybe_workflow { - bail!( - "Could not find workeflow {} for owner {}, please check your config", - workflow_name, - repo.owner.as_str(), - ); - } - let workflow = maybe_workflow.unwrap(); - info!("Execute on_create workflow {}", workflow_name); - shell::execute_workflow(workflow, repo)?; + let wf = Workflow::new(workflow_name)?; + wf.execute_repo(repo)?; } } } diff --git a/src/cmd/run/mod.rs b/src/cmd/run/mod.rs index a3191c8..34b29ff 100644 --- a/src/cmd/run/mod.rs +++ b/src/cmd/run/mod.rs @@ -14,6 +14,7 @@ pub mod rebase; pub mod release; pub mod remove; pub mod reset; +pub mod run; pub mod squash; pub mod tag; pub mod update; diff --git a/src/cmd/run/run.rs b/src/cmd/run/run.rs new file mode 100644 index 0000000..dca4172 --- /dev/null +++ b/src/cmd/run/run.rs @@ -0,0 +1,171 @@ +use std::collections::HashSet; +use std::path::PathBuf; +use std::rc::Rc; +use std::sync::Arc; + +use anyhow::{bail, Result}; +use clap::Args; +use console::style; + +use crate::batch::{self, Reporter, Task}; +use crate::cmd::Run; +use crate::repo::database::Database; +use crate::repo::types::{NameLevel, Repo}; +use crate::shell::Workflow; +use crate::{config, confirm, utils}; + +/// Run workflow +#[derive(Args)] +pub struct RunArgs { + /// The workflow name. + pub workflow: String, + + /// The remote name. + pub remote: Option, + + /// The repo query. + pub query: Option, + + /// If true, filter repos. + #[clap(long, short)] + pub filter: bool, + + /// If true, run workflow for current repo. + #[clap(long, short)] + pub current: bool, +} + +struct RunTask { + workflow: Arc, + name_level: Arc, + dir: PathBuf, + + remote: String, + owner: String, + name: String, + + show_name: String, +} + +impl Task<()> for RunTask { + fn run(&self, rp: &Reporter<()>) -> Result<()> { + self.workflow.execute_task( + &self.name_level, + rp, + &self.dir, + &self.remote, + &self.owner, + &self.name, + ) + } + + fn message_done(&self, result: &Result<()>) -> String { + match result { + Ok(_) => format!("Run workflow for {} done", self.show_name), + Err(_) => { + let msg = format!("Run workflow for {} failed", self.show_name); + format!("{}", style(msg).red()) + } + } + } +} + +impl Run for RunArgs { + fn run(&self) -> Result<()> { + let workflow = Workflow::new(self.workflow.as_str())?; + let workflow = Arc::new(workflow); + let (mut repos, level) = self.select_repos()?; + if repos.is_empty() { + println!("No repo to run"); + return Ok(()); + } + if repos.len() == 1 { + let repo = repos.into_iter().next().unwrap(); + confirm!( + "Run workflow {} for repo {}", + self.workflow, + repo.full_name() + ); + workflow.execute_repo(&repo)?; + return Ok(()); + } + if self.filter { + let items: Vec = repos.iter().map(|repo| repo.as_string(&level)).collect(); + let items = utils::edit_items(items)?; + let filter_set: HashSet = items.into_iter().collect(); + repos = repos + .into_iter() + .filter(|repo| filter_set.contains(&repo.as_string(&level))) + .collect(); + if repos.is_empty() { + println!("No repo to run after filter"); + return Ok(()); + } + } + + println!("About to run:"); + for repo in repos.iter() { + let name = repo.as_string(&level); + println!(" * {}", name); + } + let repo_word = if repos.len() == 1 { + String::from("1 repo") + } else { + format!("{} repos", repos.len()) + }; + confirm!( + "Do you want to run workflow {} for {}", + self.workflow, + repo_word + ); + + let level = Arc::new(level); + let mut tasks = Vec::with_capacity(repos.len()); + for repo in repos { + let dir = repo.get_path(); + let show_name = repo.as_string(&level); + tasks.push(RunTask { + workflow: workflow.clone(), + name_level: level.clone(), + dir, + remote: format!("{}", repo.remote), + owner: format!("{}", repo.owner), + name: format!("{}", repo.name), + show_name, + }) + } + + let desc = format!("Run workflow {}", self.workflow); + let _ = batch::run(desc.as_str(), tasks); + Ok(()) + } +} + +impl RunArgs { + fn select_repos(&self) -> Result<(Vec>, NameLevel)> { + let db = Database::read()?; + if self.current { + let repo = db.must_current()?; + return Ok((vec![repo], NameLevel::Name)); + } + if let None = &self.remote { + return Ok((db.list_all(), NameLevel::Full)); + } + let remote = self.remote.as_ref().unwrap().as_str(); + if let None = &self.query { + return Ok((db.list_by_remote(remote), NameLevel::Owner)); + } + let query = self.query.as_ref().unwrap().as_str(); + let query = query.trim_matches('/'); + if query.is_empty() { + bail!("Invalid query {}", self.query.as_ref().unwrap()); + } + if !query.contains("/") { + return Ok((db.list_by_owner(remote, query), NameLevel::Name)); + } + let remote = config::must_get_remote(remote)?; + let (owner, name) = utils::parse_query(&remote, query); + let repo = db.must_get(&remote.name, &owner, &name)?; + Ok((vec![repo], NameLevel::Name)) + } +} diff --git a/src/shell.rs b/src/shell.rs index 2e894bb..6e9aa4e 100644 --- a/src/shell.rs +++ b/src/shell.rs @@ -11,10 +11,11 @@ use console::{style, StyledObject}; use regex::{Captures, Regex}; use crate::api::types::Provider; +use crate::batch::Reporter; use crate::config::types::{Remote, WorkflowStep}; use crate::errors::SilentExit; -use crate::repo::types::Repo; -use crate::utils; +use crate::repo::types::{NameLevel, Repo}; +use crate::{config, utils}; use crate::{confirm, exec, info}; pub struct Shell { @@ -640,35 +641,114 @@ impl GitTag { } } -pub fn execute_workflow(steps: &Vec, repo: &Rc) -> Result<()> { - let dir = repo.get_path(); - for step in steps.iter() { - if let Some(run) = &step.run { - let script = run.replace("\n", ";"); - let mut cmd = Shell::sh(&script); - cmd.with_path(&dir); +pub struct Workflow { + pub name: String, + steps: &'static Vec, +} - cmd.with_env("REPO_NAME", repo.name.as_str()); - cmd.with_env("REPO_OWNER", repo.owner.as_str()); - cmd.with_env("REMOTE", repo.remote.as_str()); - cmd.with_env("REPO_LONG", repo.long_name()); - cmd.with_env("REPO_FULL", repo.full_name()); +impl Workflow { + pub fn new(name: impl AsRef) -> Result { + match config::base().workflows.get(name.as_ref()) { + Some(steps) => Ok(Workflow { + name: name.as_ref().to_string(), + steps, + }), + None => bail!("Could not find workeflow {}", name.as_ref()), + } + } - cmd.with_desc(format!("Run {}", step.name)); + pub fn execute_repo(&self, repo: &Rc) -> Result<()> { + info!("Execute workflow {} for {}", self.name, repo.full_name()); + let dir = repo.get_path(); + for step in self.steps.iter() { + if let Some(run) = &step.run { + let script = run.replace("\n", ";"); + let mut cmd = Shell::sh(&script); + cmd.with_path(&dir); - cmd.execute()?.check()?; - continue; - } - if let None = step.file { - continue; + cmd.with_env("REPO_NAME", repo.name.as_str()); + cmd.with_env("REPO_OWNER", repo.owner.as_str()); + cmd.with_env("REMOTE", repo.remote.as_str()); + cmd.with_env("REPO_LONG", repo.long_name()); + cmd.with_env("REPO_FULL", repo.full_name()); + + cmd.with_desc(format!("Run {}", step.name)); + + cmd.execute()?.check()?; + continue; + } + if let None = step.file { + continue; + } + + let content = step.file.as_ref().unwrap(); + let content = content.replace("\\t", "\t"); + + exec!("Create file {}", step.name); + let path = dir.join(&step.name); + utils::write_file(&path, content.as_bytes())?; } + Ok(()) + } + + pub fn execute_task( + &self, + name_level: &NameLevel, + rp: &Reporter, + dir: &PathBuf, + remote: S, + owner: S, + name: S, + ) -> Result<()> + where + S: AsRef, + { + let long_name = format!("{}/{}", owner.as_ref(), name.as_ref()); + let full_name = format!("{}:{}/{}", remote.as_ref(), owner.as_ref(), name.as_ref()); + let show_name = match name_level { + NameLevel::Full => full_name.as_str(), + NameLevel::Owner => long_name.as_str(), + NameLevel::Name => name.as_ref(), + }; + + for step in self.steps.iter() { + if let Some(run) = &step.run { + let script = run.replace("\n", ";"); + let mut cmd = Shell::sh(&script); + cmd.with_path(&dir); + + cmd.with_env("REPO_NAME", name.as_ref()); + cmd.with_env("REPO_OWNER", owner.as_ref()); + cmd.with_env("REMOTE", remote.as_ref()); + cmd.with_env("REPO_LONG", long_name.as_str()); + cmd.with_env("REPO_FULL", full_name.as_str()); - let content = step.file.as_ref().unwrap(); - let content = content.replace("\\t", "\t"); + cmd.set_mute(true).piped_stderr(); - exec!("Create file {}", step.name); - let path = dir.join(&step.name); - utils::write_file(&path, content.as_bytes())?; + rp.message(format!( + "Running step {} for {}", + style(&step.name).green(), + style(show_name).cyan() + )); + + cmd.execute()?.check()?; + continue; + } + if let None = step.file { + continue; + } + + let content = step.file.as_ref().unwrap(); + let content = content.replace("\\t", "\t"); + + rp.message(format!( + "Create file {} for {}", + style(&step.name).green(), + style(show_name).cyan() + )); + let path = dir.join(&step.name); + utils::write_file(&path, content.as_bytes())?; + } + Ok(()) } - Ok(()) }