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.
This commit is contained in:
2026-09-18 17:59:55 +02:00
parent 6b6c031242
commit e97205c4bf
4 changed files with 116 additions and 5 deletions
+5 -1
View File
@@ -35,13 +35,14 @@ cargo install --path .
## Usage ## Usage
``` ```
dev_tools <project> <up|down> dev_tools <project> <up|down> [-v|--verbose]
``` ```
| Argument | Description | | Argument | Description |
|---|---| |---|---|
| `project` | Project key (matches `<PROJECT>_DIR` in the config, case-insensitive) | | `project` | Project key (matches `<PROJECT>_DIR` in the config, case-insensitive) |
| `up` / `down` | Start or stop the environment | | `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 ### Examples
@@ -51,6 +52,9 @@ dev_tools foo up
# Stop it # Stop it
dev_tools foo down dev_tools foo down
# See exactly which lines were changed
dev_tools foo up -v
``` ```
## What it does ## What it does
+8
View File
@@ -13,6 +13,13 @@ pub struct Arguments {
#[arg(help = "Possible values are 'up' and 'down'")] #[arg(help = "Possible values are 'up' and 'down'")]
pub action: String, pub action: String,
#[arg(
short,
long,
help = "Show which lines were changed (values may include secrets)"
)]
pub verbose: bool,
} }
impl Arguments { impl Arguments {
@@ -33,6 +40,7 @@ mod tests {
Arguments { Arguments {
project: "test".to_string(), project: "test".to_string(),
action: action.to_string(), action: action.to_string(),
verbose: false,
} }
} }
+52 -1
View File
@@ -13,10 +13,16 @@ pub struct FileChange {
pub diffs: Vec<(String, String)>, pub diffs: Vec<(String, String)>,
} }
impl FileChange {
fn no_changes_message(&self) -> String {
format!("[!] {}: no changes", self.path)
}
}
impl fmt::Display for FileChange { impl fmt::Display for FileChange {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.diffs.is_empty() { if self.diffs.is_empty() {
return writeln!(f, "[!] {}: no changes", self.path); return writeln!(f, "{}", self.no_changes_message());
} }
writeln!(f, "[!] {} changed:", self.path)?; writeln!(f, "[!] {} changed:", self.path)?;
for (before, after) in &self.diffs { 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 /// writes global connections
pub fn set_local_db(start: bool, config: &DevToolsConf) -> Result<FileChange> { pub fn set_local_db(start: bool, config: &DevToolsConf) -> Result<FileChange> {
let global_connections_env = format!( 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] #[test]
fn toggle_activate_removes_leading_hash() { fn toggle_activate_removes_leading_hash() {
assert_eq!(toggle("#value=1", true), "value=1"); assert_eq!(toggle("#value=1", true), "value=1");
+51 -3
View File
@@ -4,7 +4,7 @@ use env::config::{load_config, DevToolsConf};
use crate::{ use crate::{
arguments::Arguments, 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}, git::command::{restore_connections_env, toggle_index},
}; };
use clap::Parser; use clap::Parser;
@@ -15,6 +15,54 @@ mod container;
mod env; mod env;
mod git; 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<()> { fn main() -> Result<()> {
let args: Arguments = Arguments::parse(); let args: Arguments = Arguments::parse();
args.validate()?; args.validate()?;
@@ -24,14 +72,14 @@ fn main() -> Result<()> {
start_docker_compose(&config.container_service, &config.container_dir, start)?; start_docker_compose(&config.container_service, &config.container_dir, start)?;
if start { if start {
print!("{}", set_local_db(start, &config)?); print_file_change(&set_local_db(start, &config)?, args.verbose);
} }
toggle_index(start, &config)?; toggle_index(start, &config)?;
if !start { if !start {
restore_connections_env(&config)?; restore_connections_env(&config)?;
println!("[!] conf/global/connections.env restored to committed state."); 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!( println!(
"[!] Don't forget your interpreter! Project directory: {}", "[!] Don't forget your interpreter! Project directory: {}",