From e97205c4bf9da2fb446b25a7a4dd6e02f747014e Mon Sep 17 00:00:00 2001 From: mace Date: Fri, 18 Sep 2026 17:59:55 +0200 Subject: [PATCH] Hide changed connection lines behind -v/--verbose set_local_db/set_dot_env diffs can contain secrets (connection strings), so printing them unconditionally on every up/down leaked credentials to stdout. Default output is now a line-count summary; pass -v/--verbose to see the actual before/after lines. --- README.md | 6 ++++- src/arguments.rs | 8 +++++++ src/config/parse.rs | 53 +++++++++++++++++++++++++++++++++++++++++++- src/main.rs | 54 ++++++++++++++++++++++++++++++++++++++++++--- 4 files changed, 116 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index cb05d7a..6836da6 100644 --- a/README.md +++ b/README.md @@ -35,13 +35,14 @@ cargo install --path . ## Usage ``` -dev_tools +dev_tools [-v|--verbose] ``` | Argument | Description | |---|---| | `project` | Project key (matches `_DIR` in the config, case-insensitive) | | `up` / `down` | Start or stop the environment | +| `-v`, `--verbose` | Print the actual before/after lines changed in the env files (values may include secrets, e.g. connection strings). Without it, only a line count is printed. | ### Examples @@ -51,6 +52,9 @@ dev_tools foo up # Stop it dev_tools foo down + +# See exactly which lines were changed +dev_tools foo up -v ``` ## What it does diff --git a/src/arguments.rs b/src/arguments.rs index 6a42897..df7af70 100644 --- a/src/arguments.rs +++ b/src/arguments.rs @@ -13,6 +13,13 @@ pub struct Arguments { #[arg(help = "Possible values are 'up' and 'down'")] pub action: String, + + #[arg( + short, + long, + help = "Show which lines were changed (values may include secrets)" + )] + pub verbose: bool, } impl Arguments { @@ -33,6 +40,7 @@ mod tests { Arguments { project: "test".to_string(), action: action.to_string(), + verbose: false, } } diff --git a/src/config/parse.rs b/src/config/parse.rs index 3e4455c..8f54482 100644 --- a/src/config/parse.rs +++ b/src/config/parse.rs @@ -13,10 +13,16 @@ pub struct FileChange { pub diffs: Vec<(String, String)>, } +impl FileChange { + fn no_changes_message(&self) -> String { + format!("[!] {}: no changes", self.path) + } +} + impl fmt::Display for FileChange { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if self.diffs.is_empty() { - return writeln!(f, "[!] {}: no changes", self.path); + return writeln!(f, "{}", self.no_changes_message()); } writeln!(f, "[!] {} changed:", self.path)?; for (before, after) in &self.diffs { @@ -26,6 +32,22 @@ impl fmt::Display for FileChange { } } +impl FileChange { + /// Line-count-only summary that never prints line contents, since those + /// can contain secrets (e.g. connection strings). Use `Display` (gated + /// behind `-v`/`--verbose` in `main`) to show the actual before/after lines. + pub fn summary(&self) -> String { + if self.diffs.is_empty() { + return format!("{}\n", self.no_changes_message()); + } + format!( + "[!] {} changed: {} line(s) (use -v to show)\n", + self.path, + self.diffs.len() + ) + } +} + /// writes global connections pub fn set_local_db(start: bool, config: &DevToolsConf) -> Result { let global_connections_env = format!( @@ -476,6 +498,35 @@ mod tests { ); } + #[test] + fn file_change_summary_no_diffs() { + let change = FileChange { + path: "some/path".to_string(), + diffs: vec![], + }; + assert_eq!(change.summary(), "[!] some/path: no changes\n"); + } + + #[test] + fn file_change_summary_hides_values() { + let change = FileChange { + path: "some/path".to_string(), + diffs: vec![ + ( + "connection='ci'".to_string(), + "connection='staging'".to_string(), + ), + ("a=1".to_string(), "#a=1".to_string()), + ], + }; + let summary = change.summary(); + assert_eq!( + summary, + "[!] some/path changed: 2 line(s) (use -v to show)\n" + ); + assert!(!summary.contains("connection")); + } + #[test] fn toggle_activate_removes_leading_hash() { assert_eq!(toggle("#value=1", true), "value=1"); diff --git a/src/main.rs b/src/main.rs index 8dc6b1e..4d43548 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,7 +4,7 @@ use env::config::{load_config, DevToolsConf}; use crate::{ arguments::Arguments, - config::parse::{set_dot_env, set_local_db}, + config::parse::{set_dot_env, set_local_db, FileChange}, git::command::{restore_connections_env, toggle_index}, }; use clap::Parser; @@ -15,6 +15,54 @@ mod container; mod env; mod git; +/// Line-count summary by default, since a full diff can contain secrets +/// (e.g. connection strings); `-v`/`--verbose` opts into the diff. +fn file_change_output(change: &FileChange, verbose: bool) -> String { + if verbose { + change.to_string() + } else { + change.summary() + } +} + +fn print_file_change(change: &FileChange, verbose: bool) { + print!("{}", file_change_output(change, verbose)); +} + +#[cfg(test)] +mod tests { + use super::*; + + fn change_with_diff() -> FileChange { + FileChange { + path: "some/path".to_string(), + diffs: vec![( + "connection='ci'".to_string(), + "connection='staging'".to_string(), + )], + } + } + + #[test] + fn file_change_output_hides_values_by_default() { + let output = file_change_output(&change_with_diff(), false); + assert_eq!( + output, + "[!] some/path changed: 1 line(s) (use -v to show)\n" + ); + assert!(!output.contains("connection")); + } + + #[test] + fn file_change_output_shows_values_when_verbose() { + let output = file_change_output(&change_with_diff(), true); + assert_eq!( + output, + "[!] some/path changed:\n connection='ci' -> connection='staging'\n" + ); + } +} + fn main() -> Result<()> { let args: Arguments = Arguments::parse(); args.validate()?; @@ -24,14 +72,14 @@ fn main() -> Result<()> { start_docker_compose(&config.container_service, &config.container_dir, start)?; if start { - print!("{}", set_local_db(start, &config)?); + print_file_change(&set_local_db(start, &config)?, args.verbose); } toggle_index(start, &config)?; if !start { restore_connections_env(&config)?; println!("[!] conf/global/connections.env restored to committed state."); } - print!("{}", set_dot_env(start, &config)?); + print_file_change(&set_dot_env(start, &config)?, args.verbose); println!( "[!] Don't forget your interpreter! Project directory: {}",