report which lines changed on up/down

set_local_db, toggle_after_line and set_dot_env now return a
FileChange (path + before/after line pairs) instead of (), and main
prints it after each toggle so 'up'/'down' shows exactly what the
program rewrote in each env file.
This commit is contained in:
2026-09-17 11:00:17 +02:00
parent 12b8701e00
commit 72e0c229f3
2 changed files with 289 additions and 21 deletions
+287 -19
View File
@@ -1,4 +1,5 @@
use std::{
fmt,
fs::{self, File},
io::{BufRead, BufReader},
};
@@ -9,8 +10,27 @@ use crate::env::config::DevToolsConf;
pub const GLOBAL_CONNECTION_PATH: &str = "conf/global/connections.env";
const LOCAL_CONNECTION_PATH: &str = "conf/local/.env";
/// The lines a toggle operation rewrote in a given file, as (before, after) pairs.
pub struct FileChange {
pub path: String,
pub diffs: Vec<(String, String)>,
}
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);
}
writeln!(f, "[!] {} changed:", self.path)?;
for (before, after) in &self.diffs {
writeln!(f, " {} -> {}", before, after)?;
}
Ok(())
}
}
/// writes global connections
pub fn set_local_db(start: bool, config: &DevToolsConf) -> Result<()> {
pub fn set_local_db(start: bool, config: &DevToolsConf) -> Result<FileChange> {
let global_connections_env = format!(
"{}{}",
config.project_dir_as_string(),
@@ -21,27 +41,39 @@ pub fn set_local_db(start: bool, config: &DevToolsConf) -> Result<()> {
let reader: BufReader<File> = BufReader::new(file);
let mut modified_lines: Vec<String> = Vec::new();
let mut diffs: Vec<(String, String)> = Vec::new();
for line in reader.lines() {
let line_content = line?;
let modified = if start && connections_env_comment(&line_content) {
format!("# {}", line_content)
let modified: Option<String> = if start && connections_env_comment(&line_content) {
Some(format!("# {}", line_content))
} else if !start && should_commented_out(&line_content) {
line_content[2..].to_string()
Some(line_content[2..].to_string())
} else {
line_content
None
};
modified_lines.push(modified);
match modified {
Some(new_line) => {
diffs.push((line_content, new_line.clone()));
modified_lines.push(new_line);
}
None => modified_lines.push(line_content),
}
}
fs::write(global_connections_env, modified_lines.join("\n"))
fs::write(&global_connections_env, modified_lines.join("\n"))
.context("Cannot write global connections env")?;
Ok(())
Ok(FileChange {
path: global_connections_env,
diffs,
})
}
pub fn set_dot_env(start: bool, config: &DevToolsConf) -> Result<()> {
toggle_after_line("#docker", 4, start, config)?;
toggle_after_line("#cidb", 2, !start, config)?;
Ok(())
pub fn set_dot_env(start: bool, config: &DevToolsConf) -> Result<FileChange> {
let mut docker_change = toggle_after_line("#docker", 4, start, config)?;
let cidb_change = toggle_after_line("#cidb", 2, !start, config)?;
docker_change.diffs.extend(cidb_change.diffs);
Ok(docker_change)
}
const CI_DB_MASTER_IP: &str = "ci.db.master.ip";
@@ -85,7 +117,7 @@ fn toggle_after_line(
num_lines: usize,
activate: bool,
config: &DevToolsConf,
) -> Result<()> {
) -> Result<FileChange> {
let local_connection_env = format!(
"{}{}",
config.project_dir_as_string(),
@@ -96,6 +128,7 @@ fn toggle_after_line(
let reader: BufReader<File> = BufReader::new(file);
let mut modified_lines: Vec<String> = Vec::new();
let mut diffs: Vec<(String, String)> = Vec::new();
let mut relevant = false;
let mut counter: usize = 0;
@@ -111,21 +144,34 @@ fn toggle_after_line(
continue;
}
if relevant {
modified_lines.push(toggle(&line_content, activate));
let modified: Option<String> = if relevant {
counter += 1;
let new_line = toggle(&line_content, activate);
(new_line != line_content).then_some(new_line)
} else if let Some(key) = connection_key(&line_content) {
let value = if activate { "staging" } else { "ci" };
modified_lines.push(format!("{}='{}'", key, value));
let new_line = format!("{}='{}'", key, value);
(new_line != line_content).then_some(new_line)
} else {
modified_lines.push(line_content);
None
};
match modified {
Some(new_line) => {
diffs.push((line_content, new_line.clone()));
modified_lines.push(new_line);
}
None => modified_lines.push(line_content),
}
}
fs::write(local_connection_env, modified_lines.join("\n"))
fs::write(&local_connection_env, modified_lines.join("\n"))
.context("Cannot write local connection env")?;
Ok(())
Ok(FileChange {
path: local_connection_env,
diffs,
})
}
/// Returns the `connection` key as it appears on the given line (either the
@@ -156,6 +202,228 @@ pub(crate) fn toggle(line: &str, activate: bool) -> String {
#[cfg(test)]
mod tests {
use super::*;
use std::ops::Deref;
use std::path::{Path, PathBuf};
/// An isolated `conf/global`/`conf/local` tree under the OS temp dir, for
/// exercising `set_local_db`/`set_dot_env` against real files without
/// touching this repo's own `conf/` directory. Namespaced by process id
/// so concurrent `cargo test` runs can't collide on the same path, and
/// removed on drop so no stale directories accumulate.
struct TempProjectDir(PathBuf);
impl TempProjectDir {
fn new(name: &str) -> Self {
let dir = std::env::temp_dir().join(format!(
"dev_tools_parse_test_{}_{}",
std::process::id(),
name
));
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(dir.join("conf/global")).unwrap();
fs::create_dir_all(dir.join("conf/local")).unwrap();
Self(dir)
}
}
impl Deref for TempProjectDir {
type Target = Path;
fn deref(&self) -> &Path {
&self.0
}
}
impl Drop for TempProjectDir {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.0);
}
}
fn config_for(dir: &Path) -> DevToolsConf {
DevToolsConf {
container_service: String::new(),
container_dir: String::new(),
project_directory: PathBuf::from(format!("{}/", dir.display())),
}
}
#[test]
fn set_local_db_records_diffs_when_commenting_out() {
let dir = TempProjectDir::new("comment_out");
fs::write(
dir.join(GLOBAL_CONNECTION_PATH),
"ci.db.master.ip=127.0.0.1\nci.db.master.port=5432\nci.db.master.user=admin\nRCC_CI_DB_MASTER_IP=127.0.0.1",
)
.unwrap();
let change = set_local_db(true, &config_for(&dir)).unwrap();
assert_eq!(
change.diffs,
vec![
(
"ci.db.master.ip=127.0.0.1".to_string(),
"# ci.db.master.ip=127.0.0.1".to_string()
),
(
"ci.db.master.port=5432".to_string(),
"# ci.db.master.port=5432".to_string()
),
(
"RCC_CI_DB_MASTER_IP=127.0.0.1".to_string(),
"# RCC_CI_DB_MASTER_IP=127.0.0.1".to_string()
),
]
);
}
#[test]
fn set_local_db_records_diffs_when_uncommenting() {
let dir = TempProjectDir::new("uncomment");
fs::write(
dir.join(GLOBAL_CONNECTION_PATH),
"# ci.db.master.ip=127.0.0.1\n# ci.db.master.port=5432\nci.db.master.user=admin",
)
.unwrap();
let change = set_local_db(false, &config_for(&dir)).unwrap();
assert_eq!(
change.diffs,
vec![
(
"# ci.db.master.ip=127.0.0.1".to_string(),
"ci.db.master.ip=127.0.0.1".to_string()
),
(
"# ci.db.master.port=5432".to_string(),
"ci.db.master.port=5432".to_string()
),
]
);
}
#[test]
fn toggle_after_line_records_diffs_for_toggled_section() {
let dir = TempProjectDir::new("toggle_section");
fs::write(
dir.join(LOCAL_CONNECTION_PATH),
"#docker\nDOCKER_HOST=tcp://localhost:2375\n#DOCKER_TLS_VERIFY=1\n#DOCKER_CERT_PATH=/certs\nDOCKER_API_VERSION=1.41\nother=1",
)
.unwrap();
let change = toggle_after_line("#docker", 4, true, &config_for(&dir)).unwrap();
assert_eq!(
change.diffs,
vec![
(
"#DOCKER_TLS_VERIFY=1".to_string(),
"DOCKER_TLS_VERIFY=1".to_string()
),
(
"#DOCKER_CERT_PATH=/certs".to_string(),
"DOCKER_CERT_PATH=/certs".to_string()
),
]
);
}
#[test]
fn toggle_after_line_records_diffs_for_both_connection_keys() {
let dir = TempProjectDir::new("connection_keys");
fs::write(
dir.join(LOCAL_CONNECTION_PATH),
"#docker\nline1\nline2\nline3\nline4\nconnection='staging'\nRCC_CONNECTION='staging'",
)
.unwrap();
let change = toggle_after_line("#cidb", 2, false, &config_for(&dir)).unwrap();
assert_eq!(
change.diffs,
vec![
(
"connection='staging'".to_string(),
"connection='ci'".to_string()
),
(
"RCC_CONNECTION='staging'".to_string(),
"RCC_CONNECTION='ci'".to_string()
),
]
);
}
#[test]
fn set_dot_env_merges_diffs_from_both_sections() {
let dir = TempProjectDir::new("merge_sections");
fs::write(
dir.join(LOCAL_CONNECTION_PATH),
"#docker\nDOCKER_HOST=tcp://localhost:2375\n#DOCKER_TLS_VERIFY=1\n#DOCKER_CERT_PATH=/certs\nDOCKER_API_VERSION=1.41\n#cidb\nci.db.master.ip=127.0.0.1\nci.db.master.port=5432\nconnection='staging'\nRCC_CONNECTION='staging'",
)
.unwrap();
let change = set_dot_env(true, &config_for(&dir)).unwrap();
assert!(change.path.ends_with(LOCAL_CONNECTION_PATH));
assert_eq!(change.diffs.len(), 6);
for expected in [
("#DOCKER_TLS_VERIFY=1", "DOCKER_TLS_VERIFY=1"),
("#DOCKER_CERT_PATH=/certs", "DOCKER_CERT_PATH=/certs"),
("ci.db.master.ip=127.0.0.1", "#ci.db.master.ip=127.0.0.1"),
("ci.db.master.port=5432", "#ci.db.master.port=5432"),
("connection='staging'", "connection='ci'"),
("RCC_CONNECTION='staging'", "RCC_CONNECTION='ci'"),
] {
assert!(
change
.diffs
.contains(&(expected.0.to_string(), expected.1.to_string())),
"missing diff {:?}",
expected
);
}
}
#[test]
fn file_change_display_no_diffs() {
let change = FileChange {
path: "some/path".to_string(),
diffs: vec![],
};
assert_eq!(format!("{}", change), "[!] some/path: no changes\n");
}
#[test]
fn file_change_display_with_diffs() {
let change = FileChange {
path: "some/path".to_string(),
diffs: vec![(
"connection='ci'".to_string(),
"connection='staging'".to_string(),
)],
};
assert_eq!(
format!("{}", change),
"[!] some/path changed:\n connection='ci' -> connection='staging'\n"
);
}
#[test]
fn file_change_display_with_multiple_diffs() {
let change = FileChange {
path: "some/path".to_string(),
diffs: vec![
("a=1".to_string(), "#a=1".to_string()),
("b=2".to_string(), "#b=2".to_string()),
],
};
assert_eq!(
format!("{}", change),
"[!] some/path changed:\n a=1 -> #a=1\n b=2 -> #b=2\n"
);
}
#[test]
fn toggle_activate_removes_leading_hash() {
+2 -2
View File
@@ -24,13 +24,13 @@ fn main() -> Result<()> {
start_docker_compose(&config.container_service, &config.container_dir, start)?;
if args.legacy {
println!("[!] Legacy mode active.");
set_local_db(start, &config)?;
print!("{}", set_local_db(start, &config)?);
toggle_index(start, &config)?;
} else {
println!("[!] Legacy mode deactivated.");
}
set_dot_env(start, &config)?;
print!("{}", set_dot_env(start, &config)?);
println!(
"[!] Don't forget your interpreter! Project directory: {}",