Compare commits
7
Commits
481e3a1dcc
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e97205c4bf | ||
|
|
6b6c031242 | ||
|
|
9f4a0dc446 | ||
|
|
3873987c19 | ||
|
|
36752606a6 | ||
|
|
72e0c229f3 | ||
|
|
12b8701e00 |
@@ -35,14 +35,14 @@ cargo install --path .
|
|||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```
|
```
|
||||||
dev_tools <project> <up|down> [--legacy]
|
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 |
|
||||||
| `-l`, `--legacy` | Enable legacy mode: also toggles `conf/global/connections.env` and runs `git update-index` |
|
| `-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
|
||||||
|
|
||||||
@@ -53,15 +53,15 @@ dev_tools foo up
|
|||||||
# Stop it
|
# Stop it
|
||||||
dev_tools foo down
|
dev_tools foo down
|
||||||
|
|
||||||
# Start with legacy env handling
|
# See exactly which lines were changed
|
||||||
dev_tools foo up --legacy
|
dev_tools foo up -v
|
||||||
```
|
```
|
||||||
|
|
||||||
## What it does
|
## What it does
|
||||||
|
|
||||||
1. Starts or stops the Docker Compose service defined by `DOCKER_SERVICE` / `DOCKER_DIR`.
|
1. Starts or stops the Docker Compose service defined by `DOCKER_SERVICE` / `DOCKER_DIR`.
|
||||||
2. Toggles database connection entries in `<PROJECT_DIR>/conf/local/.env` between `staging` and CI values.
|
2. `conf/global/connections.env`: `up` toggles it to the CI credentials and marks it `git update-index --assume-unchanged` to avoid accidental commits; `down` unmarks it and restores it to its committed state by reading the committed blob with `git cat-file` (rather than computing the reverse toggle or running `git checkout`, which would fire the repo's `post-checkout` hook), so it's guaranteed byte-for-byte identical to HEAD.
|
||||||
3. With `--legacy`: additionally modifies `conf/global/connections.env` and marks it with `git update-index --assume-unchanged` to avoid accidental commits.
|
3. Toggles database connection entries in `<PROJECT_DIR>/conf/local/.env`: `up` switches the connection to `ci`, `down` switches it to `staging`.
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
|
|||||||
+5
-5
@@ -15,11 +15,11 @@ pub struct Arguments {
|
|||||||
pub action: String,
|
pub action: String,
|
||||||
|
|
||||||
#[arg(
|
#[arg(
|
||||||
short = 'l',
|
short,
|
||||||
long = "legacy",
|
long,
|
||||||
help = "Legacy mode for old env reading."
|
help = "Show which lines were changed (values may include secrets)"
|
||||||
)]
|
)]
|
||||||
pub legacy: bool,
|
pub verbose: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Arguments {
|
impl Arguments {
|
||||||
@@ -40,7 +40,7 @@ mod tests {
|
|||||||
Arguments {
|
Arguments {
|
||||||
project: "test".to_string(),
|
project: "test".to_string(),
|
||||||
action: action.to_string(),
|
action: action.to_string(),
|
||||||
legacy: false,
|
verbose: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+519
-61
@@ -1,7 +1,5 @@
|
|||||||
use std::{
|
use std::fmt;
|
||||||
fs::{self, File},
|
use std::fs;
|
||||||
io::{BufRead, BufReader},
|
|
||||||
};
|
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
|
|
||||||
@@ -9,99 +7,215 @@ use crate::env::config::DevToolsConf;
|
|||||||
pub const GLOBAL_CONNECTION_PATH: &str = "conf/global/connections.env";
|
pub const GLOBAL_CONNECTION_PATH: &str = "conf/global/connections.env";
|
||||||
const LOCAL_CONNECTION_PATH: &str = "conf/local/.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 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, "{}", self.no_changes_message());
|
||||||
|
}
|
||||||
|
writeln!(f, "[!] {} changed:", self.path)?;
|
||||||
|
for (before, after) in &self.diffs {
|
||||||
|
writeln!(f, " {} -> {}", before, after)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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<()> {
|
pub fn set_local_db(start: bool, config: &DevToolsConf) -> Result<FileChange> {
|
||||||
let global_connections_env = format!(
|
let global_connections_env = format!(
|
||||||
"{}{}",
|
"{}{}",
|
||||||
config.project_dir_as_string(),
|
config.project_dir_as_string(),
|
||||||
GLOBAL_CONNECTION_PATH
|
GLOBAL_CONNECTION_PATH
|
||||||
);
|
);
|
||||||
let file: File = File::open(&global_connections_env)
|
let content = fs::read_to_string(&global_connections_env)
|
||||||
.with_context(|| format!("could not read file `{}`", global_connections_env))?;
|
.with_context(|| format!("could not read file `{}`", global_connections_env))?;
|
||||||
let reader: BufReader<File> = BufReader::new(file);
|
let trailing_newline = content.ends_with('\n');
|
||||||
|
|
||||||
let mut modified_lines: Vec<String> = Vec::new();
|
let mut modified_lines: Vec<String> = Vec::new();
|
||||||
for line in reader.lines() {
|
let mut diffs: Vec<(String, String)> = Vec::new();
|
||||||
let line_content = line?;
|
for line_content in content.lines() {
|
||||||
let modified = if start && connections_env_comment(&line_content) {
|
let modified: Option<String> = if start && connections_env_comment(line_content) {
|
||||||
format!("# {}", line_content)
|
Some(format!("# {}", line_content))
|
||||||
} else if !start && should_commented_out(&line_content) {
|
} else if !start && should_commented_out(line_content) {
|
||||||
line_content[2..].to_string()
|
Some(line_content[2..].to_string())
|
||||||
} else {
|
} else {
|
||||||
line_content
|
None
|
||||||
};
|
};
|
||||||
modified_lines.push(modified);
|
|
||||||
|
match modified {
|
||||||
|
Some(new_line) => {
|
||||||
|
diffs.push((line_content.to_string(), new_line.clone()));
|
||||||
|
modified_lines.push(new_line);
|
||||||
|
}
|
||||||
|
None => modified_lines.push(line_content.to_string()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fs::write(global_connections_env, modified_lines.join("\n"))
|
let mut output = modified_lines.join("\n");
|
||||||
.context("Cannot write global connections env")?;
|
if trailing_newline {
|
||||||
Ok(())
|
output.push('\n');
|
||||||
|
}
|
||||||
|
fs::write(&global_connections_env, output).context("Cannot write global connections env")?;
|
||||||
|
Ok(FileChange {
|
||||||
|
path: global_connections_env,
|
||||||
|
diffs,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_dot_env(start: bool, config: &DevToolsConf) -> Result<()> {
|
const DOCKER_SECTION: &str = "#docker";
|
||||||
toggle_after_line("#docker", 4, start, config)?;
|
const DOCKER_SECTION_LINES: usize = 4;
|
||||||
toggle_after_line("#cidb", 2, !start, config)?;
|
const CIDB_SECTION: &str = "#cidb";
|
||||||
Ok(())
|
const CIDB_SECTION_LINES: usize = 2;
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn connections_env_comment(line: &str) -> bool {
|
/// Toggles the `#docker` and `#cidb` sections and the `connection` line of
|
||||||
line.starts_with("ci.db.master.ip=") || line.starts_with("ci.db.master.port=")
|
/// `conf/local/.env` in a single read/write pass. `#docker` follows `start`
|
||||||
}
|
/// directly; `#cidb` and the connection line follow its inverse (`up` means
|
||||||
|
/// the local docker db is reachable, so `#cidb`'s CI credentials switch off
|
||||||
pub(crate) fn should_commented_out(line: &str) -> bool {
|
/// and the connection moves to `ci`; `down` is the reverse, ending on
|
||||||
line.starts_with("# ci.db.master.ip=") || line.starts_with("# ci.db.master.port=")
|
/// `staging`). Every line outside both sections is checked against
|
||||||
}
|
/// `connection_key` exactly once, so it can't be toggled twice.
|
||||||
|
pub fn set_dot_env(start: bool, config: &DevToolsConf) -> Result<FileChange> {
|
||||||
fn toggle_after_line(
|
|
||||||
target_line_prefix: &str,
|
|
||||||
num_lines: usize,
|
|
||||||
activate: bool,
|
|
||||||
config: &DevToolsConf,
|
|
||||||
) -> Result<()> {
|
|
||||||
let local_connection_env = format!(
|
let local_connection_env = format!(
|
||||||
"{}{}",
|
"{}{}",
|
||||||
config.project_dir_as_string(),
|
config.project_dir_as_string(),
|
||||||
LOCAL_CONNECTION_PATH
|
LOCAL_CONNECTION_PATH
|
||||||
);
|
);
|
||||||
let file: File = File::open(&local_connection_env)
|
let content = fs::read_to_string(&local_connection_env)
|
||||||
.with_context(|| format!("could not read file `{}`", local_connection_env))?;
|
.with_context(|| format!("could not read file `{}`", local_connection_env))?;
|
||||||
let reader: BufReader<File> = BufReader::new(file);
|
let trailing_newline = content.ends_with('\n');
|
||||||
|
|
||||||
let mut modified_lines: Vec<String> = Vec::new();
|
let mut modified_lines: Vec<String> = Vec::new();
|
||||||
let mut relevant = false;
|
let mut diffs: Vec<(String, String)> = Vec::new();
|
||||||
let mut counter: usize = 0;
|
let mut section: Option<(bool, usize)> = None;
|
||||||
|
|
||||||
for line in reader.lines() {
|
for line_content in content.lines() {
|
||||||
let line_content = line?;
|
let trimmed = line_content.trim_start();
|
||||||
|
|
||||||
if counter == num_lines {
|
if let Some((_, remaining)) = section {
|
||||||
relevant = false;
|
if remaining == 0 {
|
||||||
|
section = None;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if line_content.trim_start().starts_with(target_line_prefix) {
|
|
||||||
relevant = true;
|
if trimmed.starts_with(DOCKER_SECTION) {
|
||||||
modified_lines.push(line_content);
|
section = Some((start, DOCKER_SECTION_LINES));
|
||||||
|
modified_lines.push(line_content.to_string());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if trimmed.starts_with(CIDB_SECTION) {
|
||||||
|
section = Some((!start, CIDB_SECTION_LINES));
|
||||||
|
modified_lines.push(line_content.to_string());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if relevant {
|
let modified: Option<String> = if let Some((activate, remaining)) = &mut section {
|
||||||
modified_lines.push(toggle(&line_content, activate));
|
let new_line = toggle(line_content, *activate);
|
||||||
counter += 1;
|
*remaining -= 1;
|
||||||
|
(new_line != line_content).then_some(new_line)
|
||||||
} else {
|
} else {
|
||||||
if !activate && line_content.trim_start().starts_with("connection=") {
|
connection_key(line_content).and_then(|key| {
|
||||||
modified_lines.push("connection='ci'".to_string());
|
let value = if start { "ci" } else { "staging" };
|
||||||
continue;
|
let new_line = format!("{}='{}'", key, value);
|
||||||
} else if activate && line_content.trim_start().starts_with("connection=") {
|
(new_line != line_content).then_some(new_line)
|
||||||
modified_lines.push("connection='staging'".to_string());
|
})
|
||||||
continue;
|
};
|
||||||
|
|
||||||
|
match modified {
|
||||||
|
Some(new_line) => {
|
||||||
|
diffs.push((line_content.to_string(), new_line.clone()));
|
||||||
|
modified_lines.push(new_line);
|
||||||
}
|
}
|
||||||
modified_lines.push(line_content);
|
None => modified_lines.push(line_content.to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fs::write(local_connection_env, modified_lines.join("\n"))
|
let mut output = modified_lines.join("\n");
|
||||||
.context("Cannot write local connection env")?;
|
if trailing_newline {
|
||||||
|
output.push('\n');
|
||||||
|
}
|
||||||
|
fs::write(&local_connection_env, output).context("Cannot write local connection env")?;
|
||||||
|
|
||||||
Ok(())
|
Ok(FileChange {
|
||||||
|
path: local_connection_env,
|
||||||
|
diffs,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const CI_DB_MASTER_IP: &str = "ci.db.master.ip";
|
||||||
|
const CI_DB_MASTER_PORT: &str = "ci.db.master.port";
|
||||||
|
const CONNECTION: &str = "connection";
|
||||||
|
|
||||||
|
const RCC_CI_DB_MASTER_IP: &str = "RCC_CI_DB_MASTER_IP";
|
||||||
|
const RCC_CI_DB_MASTER_PORT: &str = "RCC_CI_DB_MASTER_PORT";
|
||||||
|
const RCC_CONNECTION: &str = "RCC_CONNECTION";
|
||||||
|
|
||||||
|
/// Converts a legacy dotted key (`ci.db.master.ip`) into its `RCC_`-prefixed
|
||||||
|
/// replacement (`RCC_CI_DB_MASTER_IP`), as used by the new env file format.
|
||||||
|
/// Test-only: keeps the `RCC_*` consts above honest (see `rcc_key_matches_const`)
|
||||||
|
/// — the hot paths below use the consts directly to avoid allocating per line.
|
||||||
|
#[cfg(test)]
|
||||||
|
fn rcc_key(dotted_key: &str) -> String {
|
||||||
|
format!("RCC_{}", dotted_key.to_uppercase().replace('.', "_"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn starts_with_env_key(line: &str, key: &str) -> bool {
|
||||||
|
line.strip_prefix(key)
|
||||||
|
.is_some_and(|rest| rest.starts_with('='))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn connections_env_comment(line: &str) -> bool {
|
||||||
|
starts_with_env_key(line, CI_DB_MASTER_IP)
|
||||||
|
|| starts_with_env_key(line, RCC_CI_DB_MASTER_IP)
|
||||||
|
|| starts_with_env_key(line, CI_DB_MASTER_PORT)
|
||||||
|
|| starts_with_env_key(line, RCC_CI_DB_MASTER_PORT)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn should_commented_out(line: &str) -> bool {
|
||||||
|
match line.strip_prefix("# ") {
|
||||||
|
Some(rest) => connections_env_comment(rest),
|
||||||
|
None => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the `connection` key as it appears on the given line (either the
|
||||||
|
/// legacy `connection` or the new `RCC_CONNECTION`), if the line sets it.
|
||||||
|
fn connection_key(line: &str) -> Option<&'static str> {
|
||||||
|
let trimmed = line.trim_start();
|
||||||
|
if starts_with_env_key(trimmed, CONNECTION) {
|
||||||
|
Some(CONNECTION)
|
||||||
|
} else if starts_with_env_key(trimmed, RCC_CONNECTION) {
|
||||||
|
Some(RCC_CONNECTION)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn toggle(line: &str, activate: bool) -> String {
|
pub(crate) fn toggle(line: &str, activate: bool) -> String {
|
||||||
@@ -119,6 +233,299 @@ pub(crate) fn toggle(line: &str, activate: bool) -> String {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
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_preserves_trailing_newline() {
|
||||||
|
let dir = TempProjectDir::new("trailing_newline_global");
|
||||||
|
let path = dir.join(GLOBAL_CONNECTION_PATH);
|
||||||
|
fs::write(&path, "ci.db.master.user=admin\n").unwrap();
|
||||||
|
|
||||||
|
set_local_db(true, &config_for(&dir)).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
fs::read_to_string(&path).unwrap(),
|
||||||
|
"ci.db.master.user=admin\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_local_db_preserves_missing_trailing_newline() {
|
||||||
|
let dir = TempProjectDir::new("no_trailing_newline_global");
|
||||||
|
let path = dir.join(GLOBAL_CONNECTION_PATH);
|
||||||
|
fs::write(&path, "ci.db.master.user=admin").unwrap();
|
||||||
|
|
||||||
|
set_local_db(true, &config_for(&dir)).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
fs::read_to_string(&path).unwrap(),
|
||||||
|
"ci.db.master.user=admin"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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 set_dot_env_keeps_trailing_newline_when_modifying() {
|
||||||
|
let dir = TempProjectDir::new("trailing_newline_local_modify");
|
||||||
|
let path = dir.join(LOCAL_CONNECTION_PATH);
|
||||||
|
fs::write(&path, "connection='ci'\n").unwrap();
|
||||||
|
|
||||||
|
set_dot_env(false, &config_for(&dir)).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(fs::read_to_string(&path).unwrap(), "connection='staging'\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_dot_env_preserves_trailing_newline_when_unmodified() {
|
||||||
|
let dir = TempProjectDir::new("trailing_newline_local_noop");
|
||||||
|
let path = dir.join(LOCAL_CONNECTION_PATH);
|
||||||
|
fs::write(&path, "OTHER_KEY=value\n").unwrap();
|
||||||
|
|
||||||
|
set_dot_env(true, &config_for(&dir)).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(fs::read_to_string(&path).unwrap(), "OTHER_KEY=value\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_dot_env_preserves_missing_trailing_newline() {
|
||||||
|
let dir = TempProjectDir::new("no_trailing_newline_local");
|
||||||
|
let path = dir.join(LOCAL_CONNECTION_PATH);
|
||||||
|
fs::write(&path, "OTHER_KEY=value").unwrap();
|
||||||
|
|
||||||
|
set_dot_env(true, &config_for(&dir)).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(fs::read_to_string(&path).unwrap(), "OTHER_KEY=value");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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 set_dot_env_merges_diffs_from_both_sections_on_stop() {
|
||||||
|
let dir = TempProjectDir::new("merge_sections_stop");
|
||||||
|
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\n#ci.db.master.ip=127.0.0.1\n#ci.db.master.port=5432\nconnection='ci'\nRCC_CONNECTION='ci'",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let change = set_dot_env(false, &config_for(&dir)).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(change.diffs.len(), 6);
|
||||||
|
for expected in [
|
||||||
|
(
|
||||||
|
"DOCKER_HOST=tcp://localhost:2375",
|
||||||
|
"#DOCKER_HOST=tcp://localhost:2375",
|
||||||
|
),
|
||||||
|
("DOCKER_API_VERSION=1.41", "#DOCKER_API_VERSION=1.41"),
|
||||||
|
("#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='ci'", "connection='staging'"),
|
||||||
|
("RCC_CONNECTION='ci'", "RCC_CONNECTION='staging'"),
|
||||||
|
] {
|
||||||
|
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 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() {
|
||||||
@@ -169,4 +576,55 @@ mod tests {
|
|||||||
fn should_commented_out_ignores_uncommented() {
|
fn should_commented_out_ignores_uncommented() {
|
||||||
assert!(!should_commented_out("ci.db.master.ip=127.0.0.1"));
|
assert!(!should_commented_out("ci.db.master.ip=127.0.0.1"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rcc_key_converts_dotted_key() {
|
||||||
|
assert_eq!(rcc_key("ci.db.master.ip"), "RCC_CI_DB_MASTER_IP");
|
||||||
|
assert_eq!(rcc_key("connection"), "RCC_CONNECTION");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rcc_key_matches_const() {
|
||||||
|
assert_eq!(rcc_key(CI_DB_MASTER_IP), RCC_CI_DB_MASTER_IP);
|
||||||
|
assert_eq!(rcc_key(CI_DB_MASTER_PORT), RCC_CI_DB_MASTER_PORT);
|
||||||
|
assert_eq!(rcc_key(CONNECTION), RCC_CONNECTION);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn connections_env_comment_matches_rcc_ip() {
|
||||||
|
assert!(connections_env_comment("RCC_CI_DB_MASTER_IP=127.0.0.1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn connections_env_comment_matches_rcc_port() {
|
||||||
|
assert!(connections_env_comment("RCC_CI_DB_MASTER_PORT=5432"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_commented_out_matches_rcc_ip() {
|
||||||
|
assert!(should_commented_out("# RCC_CI_DB_MASTER_IP=127.0.0.1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_commented_out_matches_rcc_port() {
|
||||||
|
assert!(should_commented_out("# RCC_CI_DB_MASTER_PORT=5432"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn connection_key_matches_legacy() {
|
||||||
|
assert_eq!(connection_key("connection='ci'"), Some("connection"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn connection_key_matches_rcc() {
|
||||||
|
assert_eq!(
|
||||||
|
connection_key("RCC_CONNECTION='ci'"),
|
||||||
|
Some("RCC_CONNECTION")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn connection_key_ignores_other() {
|
||||||
|
assert_eq!(connection_key("connection_pool=5"), None);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+197
-11
@@ -1,9 +1,25 @@
|
|||||||
|
use std::fs;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::{ensure, Context, Result};
|
||||||
|
|
||||||
use crate::{config::parse::GLOBAL_CONNECTION_PATH, env::config::DevToolsConf};
|
use crate::{config::parse::GLOBAL_CONNECTION_PATH, env::config::DevToolsConf};
|
||||||
|
|
||||||
|
/// Runs `git <args>` in `dir` and errors if the process didn't exit
|
||||||
|
/// successfully, so a failed checkout/update-index can't be mistaken for one
|
||||||
|
/// that succeeded.
|
||||||
|
fn run_git(dir: &str, args: &[&str]) -> Result<()> {
|
||||||
|
let status = Command::new("git").current_dir(dir).args(args).status()?;
|
||||||
|
|
||||||
|
ensure!(
|
||||||
|
status.success(),
|
||||||
|
"git {} failed with {status}",
|
||||||
|
args.join(" ")
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn toggle_index(start: bool, config: &DevToolsConf) -> Result<()> {
|
pub fn toggle_index(start: bool, config: &DevToolsConf) -> Result<()> {
|
||||||
let change: &str = match start {
|
let change: &str = match start {
|
||||||
true => "--assume-unchanged",
|
true => "--assume-unchanged",
|
||||||
@@ -15,17 +31,187 @@ pub fn toggle_index(start: bool, config: &DevToolsConf) -> Result<()> {
|
|||||||
GLOBAL_CONNECTION_PATH
|
GLOBAL_CONNECTION_PATH
|
||||||
);
|
);
|
||||||
|
|
||||||
Command::new("git")
|
run_git(
|
||||||
.current_dir(config.project_dir_as_string())
|
&config.project_dir_as_string(),
|
||||||
.args(["update-index", change, &project_path])
|
&["update-index", change, &project_path],
|
||||||
.status()?;
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if !start {
|
/// Restores `connections.env` to its committed state, used on `down` instead
|
||||||
Command::new("git")
|
/// of computing the reverse toggle so the file ends up byte-for-byte
|
||||||
.current_dir(config.project_dir_as_string())
|
/// identical to HEAD, with no risk of a parsing mismatch. Reads the
|
||||||
.args(["checkout", &project_path])
|
/// committed blob with `git cat-file` and writes it directly rather than
|
||||||
.status()?;
|
/// running `git checkout`, since `checkout` fires `post-checkout` even for a
|
||||||
}
|
/// single-file checkout and `cat-file` (plumbing, no worktree update) never
|
||||||
|
/// triggers hooks.
|
||||||
|
pub fn restore_connections_env(config: &DevToolsConf) -> Result<()> {
|
||||||
|
let project_dir = config.project_dir_as_string();
|
||||||
|
let project_path = format!("{project_dir}{GLOBAL_CONNECTION_PATH}");
|
||||||
|
|
||||||
|
let output = Command::new("git")
|
||||||
|
.current_dir(&project_dir)
|
||||||
|
.args(["cat-file", "-p", &format!("HEAD:{GLOBAL_CONNECTION_PATH}")])
|
||||||
|
.output()?;
|
||||||
|
|
||||||
|
ensure!(
|
||||||
|
output.status.success(),
|
||||||
|
"git cat-file -p HEAD:{GLOBAL_CONNECTION_PATH} failed with {}",
|
||||||
|
output.status
|
||||||
|
);
|
||||||
|
|
||||||
|
fs::write(&project_path, output.stdout).context("Cannot write connections.env")?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::fs;
|
||||||
|
use std::ops::Deref;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
/// An isolated git repository under the OS temp dir, for exercising
|
||||||
|
/// `toggle_index`/`restore_connections_env` against a real `git`
|
||||||
|
/// process without touching this repo's own history. Namespaced by
|
||||||
|
/// process id so concurrent `cargo test` runs can't collide, and
|
||||||
|
/// removed on drop so no stale directories accumulate.
|
||||||
|
struct TempGitRepo(PathBuf);
|
||||||
|
|
||||||
|
impl TempGitRepo {
|
||||||
|
fn new(name: &str) -> Self {
|
||||||
|
let dir = std::env::temp_dir().join(format!(
|
||||||
|
"dev_tools_git_test_{}_{}",
|
||||||
|
std::process::id(),
|
||||||
|
name
|
||||||
|
));
|
||||||
|
let _ = fs::remove_dir_all(&dir);
|
||||||
|
fs::create_dir_all(dir.join("conf/global")).unwrap();
|
||||||
|
|
||||||
|
let run = |args: &[&str]| {
|
||||||
|
let status = Command::new("git")
|
||||||
|
.current_dir(&dir)
|
||||||
|
.args(args)
|
||||||
|
.status()
|
||||||
|
.unwrap();
|
||||||
|
assert!(status.success(), "git {args:?} failed");
|
||||||
|
};
|
||||||
|
run(&["init", "-q"]);
|
||||||
|
run(&["config", "user.email", "test@example.com"]);
|
||||||
|
run(&["config", "user.name", "test"]);
|
||||||
|
|
||||||
|
Self(dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn commit_connections_env(&self, content: &str) {
|
||||||
|
fs::write(self.join(GLOBAL_CONNECTION_PATH), content).unwrap();
|
||||||
|
let run = |args: &[&str]| {
|
||||||
|
let status = Command::new("git")
|
||||||
|
.current_dir(&self.0)
|
||||||
|
.args(args)
|
||||||
|
.status()
|
||||||
|
.unwrap();
|
||||||
|
assert!(status.success(), "git {args:?} failed");
|
||||||
|
};
|
||||||
|
run(&["add", GLOBAL_CONNECTION_PATH]);
|
||||||
|
run(&["commit", "-q", "-m", "connections.env"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for TempGitRepo {
|
||||||
|
type Target = Path;
|
||||||
|
fn deref(&self) -> &Path {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for TempGitRepo {
|
||||||
|
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 restore_connections_env_reverts_uncommitted_changes() {
|
||||||
|
let repo = TempGitRepo::new("restore_ok");
|
||||||
|
repo.commit_connections_env("ci.db.user=\"m.rothenhaeusler\"\n");
|
||||||
|
fs::write(repo.join(GLOBAL_CONNECTION_PATH), "corrupted content\n").unwrap();
|
||||||
|
|
||||||
|
restore_connections_env(&config_for(&repo)).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
fs::read_to_string(repo.join(GLOBAL_CONNECTION_PATH)).unwrap(),
|
||||||
|
"ci.db.user=\"m.rothenhaeusler\"\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn restore_connections_env_does_not_trigger_post_checkout_hook() {
|
||||||
|
let repo = TempGitRepo::new("no_hook");
|
||||||
|
repo.commit_connections_env("ci.db.user=\"m.rothenhaeusler\"\n");
|
||||||
|
fs::write(repo.join(GLOBAL_CONNECTION_PATH), "corrupted content\n").unwrap();
|
||||||
|
|
||||||
|
let marker = repo.join("hook-ran");
|
||||||
|
let hooks_dir = repo.join(".git/hooks");
|
||||||
|
fs::create_dir_all(&hooks_dir).unwrap();
|
||||||
|
let hook_path = hooks_dir.join("post-checkout");
|
||||||
|
fs::write(
|
||||||
|
&hook_path,
|
||||||
|
format!("#!/bin/sh\ntouch {}\n", marker.display()),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let mut perms = fs::metadata(&hook_path).unwrap().permissions();
|
||||||
|
std::os::unix::fs::PermissionsExt::set_mode(&mut perms, 0o755);
|
||||||
|
fs::set_permissions(&hook_path, perms).unwrap();
|
||||||
|
|
||||||
|
restore_connections_env(&config_for(&repo)).unwrap();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
!marker.exists(),
|
||||||
|
"restore_connections_env must not trigger post-checkout hooks"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn restore_connections_env_fails_when_not_a_git_repo() {
|
||||||
|
let dir = std::env::temp_dir().join(format!(
|
||||||
|
"dev_tools_git_test_{}_not_a_repo",
|
||||||
|
std::process::id()
|
||||||
|
));
|
||||||
|
let _ = fs::remove_dir_all(&dir);
|
||||||
|
fs::create_dir_all(dir.join("conf/global")).unwrap();
|
||||||
|
fs::write(dir.join(GLOBAL_CONNECTION_PATH), "content\n").unwrap();
|
||||||
|
|
||||||
|
let result = restore_connections_env(&config_for(&dir));
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
let _ = fs::remove_dir_all(&dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn toggle_index_marks_file_assume_unchanged() {
|
||||||
|
let repo = TempGitRepo::new("toggle_index");
|
||||||
|
repo.commit_connections_env("content\n");
|
||||||
|
|
||||||
|
toggle_index(true, &config_for(&repo)).unwrap();
|
||||||
|
|
||||||
|
let output = Command::new("git")
|
||||||
|
.current_dir(&*repo)
|
||||||
|
.args(["ls-files", "-v", GLOBAL_CONNECTION_PATH])
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
let listing = String::from_utf8(output.stdout).unwrap();
|
||||||
|
assert!(
|
||||||
|
listing.starts_with('h'),
|
||||||
|
"expected lower-case assume-unchanged flag, got: {listing}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+59
-10
@@ -4,8 +4,8 @@ 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::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()?;
|
||||||
@@ -22,15 +70,16 @@ fn main() -> Result<()> {
|
|||||||
let start: bool = args.action == "up";
|
let start: bool = args.action == "up";
|
||||||
|
|
||||||
start_docker_compose(&config.container_service, &config.container_dir, start)?;
|
start_docker_compose(&config.container_service, &config.container_dir, start)?;
|
||||||
if args.legacy {
|
|
||||||
println!("[!] Legacy mode active.");
|
|
||||||
set_local_db(start, &config)?;
|
|
||||||
toggle_index(start, &config)?;
|
|
||||||
} else {
|
|
||||||
println!("[!] Legacy mode deactivated.");
|
|
||||||
}
|
|
||||||
|
|
||||||
set_dot_env(start, &config)?;
|
if start {
|
||||||
|
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_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: {}",
|
||||||
|
|||||||
Reference in New Issue
Block a user