diff --git a/Cargo.lock b/Cargo.lock index 4cd1006..b1a97a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -354,6 +354,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "difference" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" + [[package]] name = "digest" version = "0.10.7" @@ -870,6 +876,7 @@ dependencies = [ "clap", "config", "dialoguer", + "difference", "dirs-next", "home", "oauth2", @@ -879,6 +886,7 @@ dependencies = [ "serde_json", "serde_yaml", "tabled", + "term", "thiserror", "tiny_http", "tracing", @@ -1341,6 +1349,12 @@ dependencies = [ "untrusted", ] +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + [[package]] name = "ryu" version = "1.0.15" @@ -1572,6 +1586,17 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "term" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +dependencies = [ + "dirs-next", + "rustversion", + "winapi", +] + [[package]] name = "thiserror" version = "1.0.48" diff --git a/Cargo.toml b/Cargo.toml index 64817b1..c0122b2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ chrono = { version = "0.4.30", features = ["serde"] } clap = { version = "4.4.2", features = ["derive", "env"] } config = "0.13.3" dialoguer = { version = "0.10.4", features = ["fuzzy-select"] } +difference = "2.0" dirs-next = "2.0.0" home = "0.5.5" oauth2 = "4.4.2" @@ -21,6 +22,7 @@ serde = { version = "1.0.188", features = ["derive"] } serde_json = "1.0.105" serde_yaml = "0.9.25" tabled = "0.14.0" +term = "0.7.0" thiserror = "1.0.48" tiny_http = "0.12.0" tracing = "0.1.37" diff --git a/src/api/types.rs b/src/api/types.rs index f78140d..c868f2c 100644 --- a/src/api/types.rs +++ b/src/api/types.rs @@ -23,7 +23,7 @@ pub struct ListServicesResponse { pub services: Vec } -#[derive(Serialize, Deserialize, Debug, Tabled, Clone)] +#[derive(Serialize, Deserialize, Debug, Tabled, Clone, PartialEq)] pub struct Service { pub name: String, pub image: String, diff --git a/src/commands/services.rs b/src/commands/services.rs index 537cd90..4529053 100644 --- a/src/commands/services.rs +++ b/src/commands/services.rs @@ -1,6 +1,8 @@ use anyhow::{anyhow, Result}; use clap::{Parser, Subcommand}; use dialoguer::{FuzzySelect, Input}; +use difference::{Difference, Changeset}; +use std::io::Write; use std::path::Path; use super::CommandBase; use tabled::Table; @@ -53,11 +55,11 @@ pub enum Commands { pub struct Deploy { #[arg(help = "Name of the app to deploy")] name: String, - #[arg(long, help = "Environment to deploy to")] + #[arg(short, long, help = "Environment to deploy to")] env: String, #[arg(short, long, help = "The image to deploy, e.g. yourimage:v1")] image: Option, - #[arg(long, help = "(not implemented) Skip confirmation")] + #[arg(long, help = "Skip confirmation", default_missing_value("true"), default_value("false"), num_args(0..=1), require_equals(true))] no_confirm: Option, #[arg(short, long, help = "Port the application listens on")] port: Option, @@ -119,15 +121,57 @@ impl Deploy { }; if let Some(false) = self.no_confirm { - // TODO: show user what changed - let existing_svc_yaml = serde_yaml::to_string(&existing_svc)?; - println!("{}", existing_svc_yaml); + if existing_svc.is_some() && existing_svc.clone().unwrap() == new_svc { + println!("no changes detected"); + return Ok(()) + } + let existing_svc_yaml = if existing_svc.is_some() { + serde_yaml::to_string(&existing_svc)? + } else { + "".to_string() + }; + let new_svc_yaml = serde_yaml::to_string(&new_svc)?; + self.render_diff(existing_svc_yaml, new_svc_yaml)?; + let selection = FuzzySelect::with_theme(&dialoguer::theme::ColorfulTheme::default()) + .with_prompt("Do you want to apply the above changes?") + .items(&["no", "yes"]) + .default(0) + .interact() + .unwrap(); + if selection == 0 { + println!("Cancelling..."); + return Ok(()) + } } let result = base.api_client().deploy_service(token, &org_name, &self.env, new_svc)?; println!("Service {} deployed", result.name); Ok(()) } + + fn render_diff(&self, a: String, b: String) -> Result<()> { + let Changeset { diffs, .. } = Changeset::new(&a, &b, "\n"); + let mut t = term::stdout().unwrap(); + for i in 0..diffs.len() { + match diffs[i] { + Difference::Same(ref x) => { + t.reset().unwrap(); + writeln!(t, " {}", x)?; + } + Difference::Add(ref x) => { + t.fg(term::color::GREEN).unwrap(); + writeln!(t, "+{}", x)?; + } + Difference::Rem(ref x) => { + t.fg(term::color::RED).unwrap(); + writeln!(t, "-{}", x)?; + } + } + } + t.reset().unwrap(); + t.flush().unwrap(); + Ok(()) + } } #[derive(Parser, Debug)]