From 4d3a7b2032c01a0aa8d816d47666365a3773818f Mon Sep 17 00:00:00 2001 From: "Dotan J. Nahum" Date: Fri, 11 Nov 2022 10:10:39 +0200 Subject: [PATCH] feat: add (#10) * feat: add * lints --- Cargo.lock | 42 ++++++++ backpack/Cargo.toml | 1 + backpack/src/bin/bp.rs | 2 + backpack/src/bin/commands/add.rs | 75 ++++++++++++++ backpack/src/bin/commands/apply.rs | 7 ++ backpack/src/bin/commands/mod.rs | 1 + backpack/src/config.rs | 10 +- backpack/src/git.rs | 36 ++++++- backpack/src/run.rs | 6 +- backpack/src/templates.rs | 2 +- backpack/src/ui.rs | 78 +++++++++++++-- backpack/tests/vendors_test.rs | 3 + xtask/Cargo.toml | 1 + xtask/src/main.rs | 154 +++-------------------------- 14 files changed, 269 insertions(+), 149 deletions(-) create mode 100644 backpack/src/bin/commands/add.rs diff --git a/Cargo.lock b/Cargo.lock index bfaa92d..414f636 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -77,6 +77,7 @@ dependencies = [ "console", "content_inspector", "dirs", + "edit", "env_logger", "fs_extra", "git-url-parse", @@ -510,6 +511,22 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "453440c271cf5577fd2a40e4942540cb7d0d2f85e27c8d07dd0023c925a67541" +[[package]] +name = "edit" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c562aa71f7bc691fde4c6bf5f93ae5a5298b617c2eb44c76c87832299a17fbb4" +dependencies = [ + "tempfile", + "which", +] + +[[package]] +name = "either" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" + [[package]] name = "encode_unicode" version = "0.3.6" @@ -2552,6 +2569,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "which" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c831fbbee9e129a8cf93e7747a82da9d95ba8e16621cae60ec2cdc849bacb7b" +dependencies = [ + "either", + "libc", + "once_cell", +] + [[package]] name = "winapi" version = "0.3.9" @@ -2670,6 +2698,20 @@ dependencies = [ "fs_extra", "glob", "xshell", + "xtaskops", +] + +[[package]] +name = "xtaskops" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ed7f04c3669b6d39c662471a2be538809a8c0e2dfd82462fb952c27ee7c468" +dependencies = [ + "anyhow", + "dialoguer", + "duct", + "fs_extra", + "glob", ] [[package]] diff --git a/backpack/Cargo.toml b/backpack/Cargo.toml index f165f05..31a9e69 100644 --- a/backpack/Cargo.toml +++ b/backpack/Cargo.toml @@ -50,6 +50,7 @@ interactive-actions = "1" tera = "^1.17.0" tera-text-filters = "^1.0.0" content_inspector = "0.2.4" +edit = "*" [dev-dependencies] insta = { version = "1.17.1", features = ["backtrace", "redactions"] } diff --git a/backpack/src/bin/bp.rs b/backpack/src/bin/bp.rs index 7061960..d7b7475 100644 --- a/backpack/src/bin/bp.rs +++ b/backpack/src/bin/bp.rs @@ -18,6 +18,7 @@ fn main() { .subcommand(commands::new::command()) .subcommand(commands::apply::command()) .subcommand(commands::cache::command()) + .subcommand(commands::add::command()) .subcommand(commands::config::command()); let matches = app.clone().get_matches(); @@ -27,6 +28,7 @@ fn main() { ("new", subcommand_matches) => commands::new::run(&matches, subcommand_matches), ("apply", subcommand_matches) => commands::apply::run(&matches, subcommand_matches), ("cache", subcommand_matches) => commands::cache::run(&matches, subcommand_matches), + ("add", subcommand_matches) => commands::add::run(&matches, subcommand_matches), ("config", subcommand_matches) => commands::config::run(&matches, subcommand_matches), _ => unreachable!(), }, diff --git a/backpack/src/bin/commands/add.rs b/backpack/src/bin/commands/add.rs new file mode 100644 index 0000000..cabaf63 --- /dev/null +++ b/backpack/src/bin/commands/add.rs @@ -0,0 +1,75 @@ +use anyhow::Context; +use anyhow::Result as AnyResult; +use backpack::config::Project; +use backpack::data::CopyMode; +use backpack::git::GitCmd; +use backpack::git::GitProvider; +use backpack::{config::Config, ui::Prompt}; +use clap::{Arg, ArgMatches, Command}; + +pub fn command() -> Command<'static> { + Command::new("add") + .about("Add a repo as a project") + .arg( + Arg::new("git") + .short('g') + .long("git") + .help("prefer a git url") + .takes_value(false), + ) + .arg(Arg::new("repo")) +} + +pub fn run(_matches: &ArgMatches, subcommand_matches: &ArgMatches) -> AnyResult { + // get a repo + // arg given -> from arg + // arg not given -> git provider, cmd to extract current remote + // parse it to see that its valid + // initialize a Location, and then resolve it per usual. + // next, + // -> canonicalize to https and git, and ask which one + // if exists in config, say it and skip. + // + // show current projects, + // ask what to call it (populate with init-name) + // with selected, get the current config, load it, mutate, and store + // done + + // XXX fix this to fallback onto git, and then bail if none there too + let repo = subcommand_matches + .get_one::("repo") + .map_or_else(|| GitCmd::default().get_local_url(), |r| Ok(r.to_string()))?; + + // build Location + // git preference + // if user flag -> true + // otherwise web + + // load configuration + // show all projects + // ask how to call the new one + // store to config + // ask if to open + // open with an open crate + + let (config, _) = Config::load_or_default().context("could not load configuration")?; + + let prompt = &mut Prompt::build(&config, false, None); + prompt.show_projects(&CopyMode::All); + let name = prompt.ask_for_project_name(&repo)?; + + // add it to the configuration and save + let mut config = config.clone(); + if let Some(projects) = config.projects.as_mut() { + projects.insert(name.clone(), Project::from_link(&repo)); + } + if prompt.are_you_sure(&format!("Save '{}' ({}) to configuration?", name, &repo))? { + config.save()?; + prompt.say(&format!("Saved '{}' to global config.", name)); + } + prompt.suggest_edit( + "Would you like to add actions? (will open editor)", + Config::global_config_file()?.as_path(), + )?; + Ok(true) +} diff --git a/backpack/src/bin/commands/apply.rs b/backpack/src/bin/commands/apply.rs index e74f84e..c54c915 100644 --- a/backpack/src/bin/commands/apply.rs +++ b/backpack/src/bin/commands/apply.rs @@ -31,6 +31,13 @@ pub fn command() -> Command<'static> { .help("fetch resources without using the cache") .takes_value(false), ) + .arg( + Arg::new("config") + .short('c') + .long("config") + .help("use a specified configuration file") + .takes_value(true), + ) .arg(Arg::new("shortlink")) .arg(Arg::new("dest")) } diff --git a/backpack/src/bin/commands/mod.rs b/backpack/src/bin/commands/mod.rs index 6ccb6e5..fe342a0 100644 --- a/backpack/src/bin/commands/mod.rs +++ b/backpack/src/bin/commands/mod.rs @@ -1,3 +1,4 @@ +pub mod add; pub mod apply; pub mod cache; pub mod config; diff --git a/backpack/src/config.rs b/backpack/src/config.rs index 5deea69..0732c4c 100644 --- a/backpack/src/config.rs +++ b/backpack/src/config.rs @@ -272,7 +272,7 @@ impl Default for ProjectSourceKind { } } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct Project { #[serde(rename = "shortlink")] pub shortlink: String, @@ -296,6 +296,14 @@ pub struct Project { #[serde(rename = "swaps")] pub swaps: Option>, } +impl Project { + pub fn from_link(ln: &str) -> Self { + Self { + shortlink: ln.to_string(), + ..Default::default() + } + } +} #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RepoActionsConfig { diff --git a/backpack/src/git.rs b/backpack/src/git.rs index 67ca37c..a02f1c9 100644 --- a/backpack/src/git.rs +++ b/backpack/src/git.rs @@ -1,5 +1,5 @@ use crate::data::Location; -use anyhow::{Context, Result}; +use anyhow::{bail, Context, Result}; use std::process::Command; use tracing; @@ -24,6 +24,13 @@ pub trait GitProvider { /// /// This function will return an error if underlying remote resolving implementation failed. fn get_ref_or_default(&self, location: &Location) -> Result; + + /// Get the current repo's main remote url + /// + /// # Errors + /// + /// This function will return an error if underlying remote resolving implementation failed. + fn get_local_url(&self) -> Result; } #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -133,4 +140,31 @@ impl GitProvider for GitCmd { } Ok(()) } + + fn get_local_url(&self) -> anyhow::Result { + let process = Command::new("git") + .arg("remote") + .arg("get-url") + .arg("origin") + .output() + .with_context(|| "cannot run git remote on local repo".to_string())?; + + if !process.status.success() { + anyhow::bail!( + "cannot find local url: {}", + String::from_utf8_lossy(&process.stderr[..]) + ); + } + + let url = String::from_utf8_lossy(&process.stdout[..]); + let lines = url.lines().collect::>(); + if lines.len() != 1 { + // too many urls, cannot decide + bail!("repo has more than one remote URL") + } + match lines.first() { + Some(ln) => Ok((*ln).to_string()), + _ => bail!("no repo remote URL found"), + } + } } diff --git a/backpack/src/run.rs b/backpack/src/run.rs index ea5b865..ad668b9 100644 --- a/backpack/src/run.rs +++ b/backpack/src/run.rs @@ -74,7 +74,11 @@ impl Runner { // confirm if !opts.always_yes && should_confirm - && !prompt.are_you_sure(&shortlink, dest.as_deref())? + && !prompt.are_you_sure(&format!( + "Generate from '{}' into '{}'?", + shortlink, + dest.as_deref().unwrap_or("a default folder") + ))? { // bail out, user won't confirm return Ok(()); diff --git a/backpack/src/templates.rs b/backpack/src/templates.rs index a22108f..f551379 100644 --- a/backpack/src/templates.rs +++ b/backpack/src/templates.rs @@ -125,7 +125,7 @@ impl Swapper { .parent() .ok_or_else(|| anyhow::anyhow!("cannot get parent for {:?}", swapped))?; if !parent.exists() { - fs::create_dir_all(&parent)?; + fs::create_dir_all(parent)?; }; let content_swaps = self diff --git a/backpack/src/ui.rs b/backpack/src/ui.rs index 8e41648..aaef734 100644 --- a/backpack/src/ui.rs +++ b/backpack/src/ui.rs @@ -63,7 +63,15 @@ impl<'a> Prompt<'a> { } else { self.input_shortlink()? }; - Ok((shortlink, d.map(ToString::to_string), true)) + if let Some(d) = d { + Ok((shortlink, Some(d.to_string()), true)) + } else { + Ok(( + shortlink, + self.input_dest(opts.mode == CopyMode::Copy)?, + true, + )) + } } (Some(s), None) => Ok(( s.to_string(), @@ -123,6 +131,50 @@ impl<'a> Prompt<'a> { _ => Ok(None), } } + + /// Shows the list of projects + pub fn show_projects(&self, mode: &CopyMode) { + println!("Current projects:"); + match self.config.projects_for_selection(mode) { + projects if !projects.is_empty() => { + for (name, project) in projects { + println!( + "- {} ({})", + style(name).yellow(), + style(&project.shortlink).dim() + ); + } + } + _ => { + println!("You have no projects yet."); + } + }; + } + + /// Returns the input shortlink of this [`Prompt`]. + /// + /// # Errors + /// + /// This function will return an error if interaction is killed + pub fn ask_for_project_name(&mut self, repo: &str) -> AnyResult { + let question = Question::input("question") + .validate(|v, _| { + if v.is_empty() { + Err("cannot be empty".into()) + } else { + Ok(()) + } + }) + .message(format!("A name for '{}'?", repo)) + .build(); + let name = self + .prompt_one(question)? + .as_string() + .ok_or_else(|| anyhow::anyhow!("cannot parse input"))? + .to_string(); + Ok(name) + } + /// Returns the input shortlink of this [`Prompt`]. /// /// # Errors @@ -178,13 +230,9 @@ impl<'a> Prompt<'a> { /// # Errors /// /// This function will return an error if interaction is killed - pub fn are_you_sure(&mut self, shortlink: &str, dest: Option<&str>) -> AnyResult { + pub fn are_you_sure(&mut self, text: &str) -> AnyResult { let question = Question::confirm("question") - .message(format!( - "Generate from '{}' into '{}'?", - shortlink, - dest.unwrap_or("a default folder"), - )) + .message(text) .default(true) .build(); @@ -273,6 +321,22 @@ impl<'a> Prompt<'a> { } } + pub fn say(&self, text: &str) { + println!("{}", text); + } + + /// Ask if user wants to edit a file and open editor + /// + /// # Errors + /// + /// This function will return an error if IO failed + pub fn suggest_edit(&mut self, text: &str, path: &Path) -> AnyResult<()> { + if self.are_you_sure(text)? { + edit::edit_file(path)?; + } + Ok(()) + } + fn prompt_one>>(&mut self, question: I) -> AnyResult { match self.events { Some(ref mut events) => { diff --git a/backpack/tests/vendors_test.rs b/backpack/tests/vendors_test.rs index f5dc137..9854023 100644 --- a/backpack/tests/vendors_test.rs +++ b/backpack/tests/vendors_test.rs @@ -22,6 +22,9 @@ impl GitProvider for TestGitProvider { fn shallow_clone(&self, _location: &Location, _out: &str) -> anyhow::Result<()> { Ok(()) } + fn get_local_url(&self) -> anyhow::Result { + Ok("".to_string()) + } } #[test] diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index 5305cbb..63bbac1 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -14,3 +14,4 @@ duct = "0.13.5" glob = "0.3.0" fs_extra = "1" dialoguer = "^0.10.1" +xtaskops = "0.2.1" diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 31ed068..ed34c18 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -1,17 +1,9 @@ #![allow(dead_code)] -use anyhow::Result as AnyResult; + use clap::{AppSettings, Arg, Command}; -use dialoguer::{theme::ColorfulTheme, Confirm}; -use duct::cmd; -use fs_extra as fsx; -use fsx::dir::CopyOptions; -use glob::glob; -use std::{ - fs::create_dir_all, - path::{Path, PathBuf}, -}; +use xtaskops::ops; +use xtaskops::tasks; -const TEMPLATE_PROJECT_NAME: &str = "backpack"; fn main() -> Result<(), anyhow::Error> { let cli = Command::new("xtask") .setting(AppSettings::SubcommandRequiredElseHelp) @@ -24,141 +16,27 @@ fn main() -> Result<(), anyhow::Error> { .takes_value(false), ), ) - .subcommand(Command::new("vars")); - + .subcommand(Command::new("vars")) + .subcommand(Command::new("ci")) + .subcommand(Command::new("powerset")) + .subcommand(Command::new("bloat-deps")) + .subcommand(Command::new("bloat-time")) + .subcommand(Command::new("docs")); let matches = cli.get_matches(); - let root = root_dir(); - let project = root.join(TEMPLATE_PROJECT_NAME); + let root = ops::root_dir(); let res = match matches.subcommand() { - Some(("coverage", sm)) => { - remove_dir("coverage")?; - create_dir_all("coverage")?; - - println!("=== running coverage ==="); - cmd!("cargo", "test") - .env("CARGO_INCREMENTAL", "0") - .env("RUSTFLAGS", "-Cinstrument-coverage") - .env("LLVM_PROFILE_FILE", "cargo-test-%p-%m.profraw") - .run()?; - println!("ok."); - - println!("=== generating report ==="); - let devmode = sm.is_present("dev"); - let (fmt, file) = if devmode { - ("html", "coverage/html") - } else { - ("lcov", "coverage/tests.lcov") - }; - cmd!( - "grcov", - ".", - "--binary-path", - "./target/debug/deps", - "-s", - ".", - "-t", - fmt, - "--branch", - "--ignore-not-existing", - "--ignore", - "../*", - "--ignore", - "/*", - "--ignore", - "xtask/*", - "--ignore", - "*/src/tests/*", - "-o", - file, - ) - .run()?; - println!("ok."); - - println!("=== cleaning up ==="); - clean_files("**/*.profraw")?; - println!("ok."); - if devmode { - if confirm("open report folder?") { - cmd!("open", file).run()?; - } else { - println!("report location: {}", file); - } - } - - Ok(()) - } - /* - Some(("dual", _)) => { - if exists(project.join("src/bin")) - && !confirm("this will overwrite existing contents. continue?") - { - return Err(anyhow!("user aborted")); - } - - remove_file(project.join("src/main.rs"))?; - copy_contents(root.join("xtask/src/dual"), project, true)?; - - println!("scaffolded as a dual crate."); - - Ok(()) - } - */ + Some(("coverage", sm)) => tasks::coverage(sm.is_present("dev")), Some(("vars", _)) => { - println!("project root: {:?}", project); println!("root: {:?}", root); Ok(()) } + Some(("ci", _)) => tasks::ci(), + Some(("docs", _)) => tasks::docs(), + Some(("powerset", _)) => tasks::powerset(), + Some(("bloat-deps", _)) => tasks::bloat_deps(), + Some(("bloat-time", _)) => tasks::bloat_time(), _ => unreachable!("unreachable branch"), }; res } - -fn clean_files(pattern: &str) -> AnyResult<()> { - let files: Result, _> = glob(pattern)?.collect(); - files?.iter().try_for_each(remove_file) -} - -fn remove_file

(path: P) -> AnyResult<()> -where - P: AsRef, -{ - fsx::file::remove(path).map_err(anyhow::Error::msg) -} - -fn remove_dir

(path: P) -> AnyResult<()> -where - P: AsRef, -{ - fsx::dir::remove(path).map_err(anyhow::Error::msg) -} - -fn exists

(path: P) -> bool -where - P: AsRef, -{ - std::path::Path::exists(path.as_ref()) -} - -fn copy_contents(from: P, to: Q, overwrite: bool) -> AnyResult -where - P: AsRef, - Q: AsRef, -{ - let mut opts = CopyOptions::new(); - opts.content_only = true; - opts.overwrite = overwrite; - fsx::dir::copy(from, to, &opts).map_err(anyhow::Error::msg) -} - -fn confirm(question: &str) -> bool { - Confirm::with_theme(&ColorfulTheme::default()) - .with_prompt(question) - .interact() - .unwrap() -} -fn root_dir() -> PathBuf { - let mut xtask_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - xtask_dir.pop(); - xtask_dir -}