From b40dfe6124ba96f1b9ee4242aecf1e21fb0796e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Jane=C5=BEi=C4=8D?= Date: Wed, 9 Sep 2026 12:32:55 +0000 Subject: [PATCH] refactor: put the dry-run and quiet checks on Ctx --- src/cmd/compose.rs | 73 +++++--- src/cmd/mod.rs | 9 +- src/commands/django.rs | 23 +-- src/commands/link.rs | 30 ++-- src/commands/postgres.rs | 21 +-- src/commands/postgres/server.rs | 4 +- src/commands/status.rs | 4 +- src/ctx.rs | 16 ++ src/fsops.rs | 286 ++++++++++++++++---------------- src/project.rs | 4 +- 10 files changed, 253 insertions(+), 217 deletions(-) diff --git a/src/cmd/compose.rs b/src/cmd/compose.rs index 2b7daae..8e81de3 100644 --- a/src/cmd/compose.rs +++ b/src/cmd/compose.rs @@ -1,4 +1,5 @@ use super::{Argv, Cmd}; +use crate::ctx::Ctx; // compose prints its own progress, which --quiet has to ask it to stop fn compose(quiet: bool) -> Argv { @@ -10,6 +11,49 @@ fn compose(quiet: bool) -> Argv { } } +// the compose commands, each already knowing whether -q asked compose to stop +// narrating. the flag was a field every construction site set by hand, and +// three of them did not, so `ahab -q` still had compose talking over the output +pub struct Compose<'a> { + ctx: &'a Ctx, +} + +impl<'a> Compose<'a> { + pub(crate) fn new(ctx: &'a Ctx) -> Self { + Self { ctx } + } + + pub fn up(&self) -> Up { + Up { + quiet: self.ctx.quiet, + } + } + + pub fn stop(&self) -> Stop { + Stop { + quiet: self.ctx.quiet, + } + } + + pub fn start(&self, service: &'a str) -> Start<'a> { + Start { + service, + quiet: self.ctx.quiet, + } + } + + // reading the project says nothing, so there is no progress to quieten + pub fn config(&self) -> Config { + Config + } + + pub fn ps(&self, service: &str) -> Ps { + Ps { + service: service.to_string(), + } + } +} + // docker compose run --rm, which runs the image's entrypoint and so fixes up the // container user before handing over pub struct Run { @@ -19,20 +63,15 @@ pub struct Run { } impl Run { - pub fn wrapping(service: &str, inner: Argv) -> Self { + // only reached through Cmd::in_service, which takes the context, so the + // progress flag cannot be left off by forgetting a builder call + pub(super) fn wrapping(ctx: &Ctx, service: &str, inner: Argv) -> Self { Self { service: service.to_string(), inner, - quiet: false, + quiet: ctx.quiet, } } - - // compose narrates the container it creates before handing over, which is - // progress along the way rather than what was asked for - pub fn quiet(mut self, quiet: bool) -> Self { - self.quiet = quiet; - self - } } impl Cmd for Run { @@ -59,14 +98,6 @@ pub struct Ps { service: String, } -impl Ps { - pub fn id_of(service: &str) -> Self { - Self { - service: service.to_string(), - } - } -} - impl Cmd for Ps { fn argv(&self) -> Argv { compose(false).arg("ps").arg("--quiet").arg(&self.service) @@ -74,7 +105,7 @@ impl Cmd for Ps { } pub struct Up { - pub quiet: bool, + quiet: bool, } impl Cmd for Up { @@ -84,8 +115,8 @@ impl Cmd for Up { } pub struct Start<'a> { - pub service: &'a str, - pub quiet: bool, + service: &'a str, + quiet: bool, } impl Cmd for Start<'_> { @@ -95,7 +126,7 @@ impl Cmd for Start<'_> { } pub struct Stop { - pub quiet: bool, + quiet: bool, } impl Cmd for Stop { diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs index a88486f..43c13f1 100644 --- a/src/cmd/mod.rs +++ b/src/cmd/mod.rs @@ -14,7 +14,7 @@ use std::{ use anyhow::Result; pub use argv::Argv; -pub use compose::{Config, Ps, Run, Start, Stop, Up}; +pub use compose::{Compose, Run}; pub use django::{Bash, Manage, Words}; pub use docker::{Cp, Exec}; pub use git::{CheckIgnore, ConfigGet, LsFiles, RevParse}; @@ -43,9 +43,12 @@ pub trait Cmd { Exec::wrapping(container, self.argv()) } - fn in_service(&self, service: &str) -> Run { - Run::wrapping(service, self.argv()) + // the context comes in here because compose narrates what it is doing, and + // whether it should is the caller's -q rather than this command's business + fn in_service(&self, ctx: &Ctx, service: &str) -> Run { + Run::wrapping(ctx, service, self.argv()) } + // terminals fn run(&self, ctx: &Ctx) -> Result<()> { self.argv().run(ctx) diff --git a/src/commands/django.rs b/src/commands/django.rs index e9795ca..2c4d699 100644 --- a/src/commands/django.rs +++ b/src/commands/django.rs @@ -4,7 +4,7 @@ use anyhow::{Result, anyhow}; use crate::cmd::{Bash, Cmd, Manage, Words}; use crate::ctx::Ctx; -use crate::fsops::{create_dir, touch, write_new}; + use crate::output::note; use crate::project::Project; @@ -40,8 +40,8 @@ pub fn make_command(ctx: &Ctx, app: &Path, name: &str) -> Result<()> { let management_dir = app_dir.join("management"); if !management_dir.exists() { - create_dir(ctx, &management_dir)?; - touch(ctx, &management_dir.join("__init__.py"))?; + ctx.fs().create_dir(&management_dir)?; + ctx.fs().touch(&management_dir.join("__init__.py"))?; note!(ctx, "created module {app_name}.management") }; @@ -49,14 +49,13 @@ pub fn make_command(ctx: &Ctx, app: &Path, name: &str) -> Result<()> { let commands_dir = management_dir.join("commands"); if !commands_dir.exists() { - create_dir(ctx, &commands_dir)?; - touch(ctx, &commands_dir.join("__init__.py"))?; + ctx.fs().create_dir(&commands_dir)?; + ctx.fs().touch(&commands_dir.join("__init__.py"))?; note!(ctx, "created module {app_name}.management.commands") }; - write_new( - ctx, + ctx.fs().write_new( &commands_dir.join(format!("{name}.py")), DEBUG_TEMPLATE.as_bytes(), )?; @@ -72,22 +71,18 @@ fn is_module_name(name: &str) -> bool { } pub fn bash(ctx: &Ctx) -> Result<()> { - Bash.in_service(&service(ctx)?) - .quiet(ctx.quiet) - .replace(ctx) + Bash.in_service(ctx, &service(ctx)?).replace(ctx) } pub fn run(ctx: &Ctx, rest: &[String]) -> Result<()> { Words::new(rest) - .in_service(&service(ctx)?) - .quiet(ctx.quiet) + .in_service(ctx, &service(ctx)?) .replace(ctx) } pub fn manage(ctx: &Ctx, rest: &[String]) -> Result<()> { Manage::new(rest) - .in_service(&service(ctx)?) - .quiet(ctx.quiet) + .in_service(ctx, &service(ctx)?) .replace(ctx) } diff --git a/src/commands/link.rs b/src/commands/link.rs index 5a725be..ea8a90b 100644 --- a/src/commands/link.rs +++ b/src/commands/link.rs @@ -11,9 +11,7 @@ use anyhow::{Context, Result, anyhow, bail}; use self::store::{Leads, Repo, ignored, leads, resolve, symlink_metadata_opt, tracked}; use crate::ctx::Ctx; -use crate::fsops::{ - ensure_private_parent, move_path, place_link, prune_empty, remove_file, rename, suffixed, -}; +use crate::fsops::suffixed; use crate::output::{line, note, warning}; const BACKUP_SUFFIX: &str = ".ahab-bak"; @@ -137,10 +135,10 @@ fn restore_one(ctx: &Ctx, repo: &Repo, path: &Path, report: &Report) -> Result<( bail!("{} is in the way; move it aside", aside.display()); } - rename(ctx, &src, &aside)?; + ctx.fs().rename(&src, &aside)?; - if let Err(e) = move_path(ctx, &stored, &src) { - rename(ctx, &aside, &src).with_context(|| { + if let Err(e) = ctx.fs().move_path(&stored, &src) { + ctx.fs().rename(&aside, &src).with_context(|| { format!( "could not put the link at {} back after failing to restore it", src.display() @@ -150,8 +148,8 @@ fn restore_one(ctx: &Ctx, repo: &Repo, path: &Path, report: &Report) -> Result<( return Err(e); } - remove_file(ctx, &aside)?; - prune_empty(ctx, stored.parent(), &repo.base); + ctx.fs().remove_file(&aside)?; + ctx.fs().prune_empty(stored.parent(), &repo.base); report.line("restored", &rel); Ok(()) @@ -295,7 +293,7 @@ fn link_one(ctx: &Ctx, repo: &Repo, path: &Path, force: bool, report: &Report) - stays_in_store(repo, &rel)?; // before anything is moved in, so the tree it lands in is never briefly // readable by anyone else - ensure_private_parent(ctx, &repo.base, &target)?; + ctx.fs().ensure_private_parent(&repo.base, &target)?; if tracked(ctx, repo, &rel)? { return Err(anyhow!( @@ -363,7 +361,7 @@ fn link_one(ctx: &Ctx, repo: &Repo, path: &Path, force: bool, report: &Report) - return Err(needs_force(&target)); } - place_link(ctx, &src, &target)?; + ctx.fs().place_link(&src, &target)?; report.line("repointed", &rel); Ok(()) } @@ -381,10 +379,10 @@ fn link_one(ctx: &Ctx, repo: &Repo, path: &Path, force: bool, report: &Report) - )); } - rename(ctx, &src, &backup)?; + ctx.fs().rename(&src, &backup)?; report.line("saved", &suffixed(&rel, BACKUP_SUFFIX)); - place_link(ctx, &src, &target)?; + ctx.fs().place_link(&src, &target)?; report.line("linked", &rel); Ok(()) } @@ -399,7 +397,7 @@ fn link_one(ctx: &Ctx, repo: &Repo, path: &Path, force: bool, report: &Report) - if !force { return Err(needs_force(&target)); } - place_link(ctx, &src, &target)?; + ctx.fs().place_link(&src, &target)?; report.line("linked", &rel); Ok(()) } @@ -416,10 +414,10 @@ fn link_one(ctx: &Ctx, repo: &Repo, path: &Path, force: bool, report: &Report) - // but no link placed, the store holds a path nothing points at and the working // tree has lost it altogether, which is the one outcome worse than failing fn move_and_link(ctx: &Ctx, src: &Path, target: &Path) -> Result<()> { - move_path(ctx, src, target)?; + ctx.fs().move_path(src, target)?; - if let Err(e) = place_link(ctx, src, target) { - move_path(ctx, target, src).with_context(|| { + if let Err(e) = ctx.fs().place_link(src, target) { + ctx.fs().move_path(target, src).with_context(|| { format!( "could not put {} back after failing to link it to {}", src.display(), diff --git a/src/commands/postgres.rs b/src/commands/postgres.rs index 735cf05..34fe179 100644 --- a/src/commands/postgres.rs +++ b/src/commands/postgres.rs @@ -11,11 +11,10 @@ use self::server::{Database, wait_until_ready, when_ready}; use self::shape::{Dump, HEADER_LEN, Kind}; use crate::cli::Format; use crate::cmd::{ - Cmd, Cp, CreateDb, DropDb, Gunzip, Gzip, Head, PgDump, PgDumpAll, PgRestore, Psql, Rm, Start, - Stop, Up, + Cmd, Cp, CreateDb, DropDb, Gunzip, Gzip, Head, PgDump, PgDumpAll, PgRestore, Psql, Rm, }; use crate::ctx::Ctx; -use crate::fsops::{create_private_new, remove_file, rename, suffixed}; +use crate::fsops::{create_private_new, suffixed}; use crate::output::{note, warning}; // unique per run: docker cp will not copy a directory over an existing path, and @@ -136,7 +135,7 @@ pub fn import(ctx: &Ctx, file: &Path) -> Result<()> { } note!(ctx, "stopping all containers"); - Stop { quiet: ctx.quiet }.run(ctx)?; + ctx.compose().stop().run(ctx)?; // everything from here runs with the project down, so an error leaves it // that way and the user has no reason to guess as much @@ -159,11 +158,7 @@ fn local_path_for_docker(file: &Path) -> Result<()> { fn imported(ctx: &Ctx, db: &Database, dump: &Dump, file: &Path) -> Result<()> { note!(ctx, "starting db container"); - Start { - service: &db.service, - quiet: ctx.quiet, - } - .run(ctx)?; + ctx.compose().start(&db.service).run(ctx)?; let remote = remote_dump(); @@ -229,8 +224,8 @@ fn imported(ctx: &Ctx, db: &Database, dump: &Dump, file: &Path) -> Result<()> { restored?; note!(ctx, "restarting containers"); - Stop { quiet: ctx.quiet }.run(ctx)?; - Up { quiet: ctx.quiet }.run(ctx)?; + ctx.compose().stop().run(ctx)?; + ctx.compose().up().run(ctx)?; Ok(()) } @@ -337,11 +332,11 @@ pub fn dump(ctx: &Ctx, file: &Path, format: Format, gzip: bool) -> Result<()> { }; if let Err(e) = dumped { - let _ = remove_file(ctx, &partial); + let _ = ctx.fs().remove_file(&partial); return Err(e); } - rename(ctx, &partial, file)?; + ctx.fs().rename(&partial, file)?; Ok(()) } diff --git a/src/commands/postgres/server.rs b/src/commands/postgres/server.rs index 0f92b73..84999ed 100644 --- a/src/commands/postgres/server.rs +++ b/src/commands/postgres/server.rs @@ -4,7 +4,7 @@ use std::time::{Duration, Instant}; use anyhow::{Result, bail}; use super::shape::Kind; -use crate::cmd::{Cmd, PgIsReady, PgRestore, Ps, Psql}; +use crate::cmd::{Cmd, PgIsReady, PgRestore, Psql}; use crate::ctx::Ctx; use crate::project::Project; @@ -26,7 +26,7 @@ impl Database { let service = compose.postgres()?; let (user, name) = compose.postgres_credentials(&service)?; - let listed = Ps::id_of(&service).capture(ctx)?; + let listed = ctx.compose().ps(&service).capture(ctx)?; let mut ids = listed.lines().map(str::trim).filter(|id| !id.is_empty()); let container = match (ids.next(), ids.next()) { diff --git a/src/commands/status.rs b/src/commands/status.rs index 05651e2..992f58d 100644 --- a/src/commands/status.rs +++ b/src/commands/status.rs @@ -2,7 +2,7 @@ use std::path::Path; use anyhow::Result; -use crate::cmd::{Cmd, Ps}; +use crate::cmd::Cmd; use crate::commands::link; use crate::ctx::Ctx; use crate::output::line; @@ -62,7 +62,7 @@ fn role(ctx: &Ctx, role: &str, detected: Result, project: &Project) { line!("{role}: {service} ({image})"); - match Ps::id_of(&service).capture(ctx) { + match ctx.compose().ps(&service).capture(ctx) { Ok(id) if id.trim().is_empty() => line!("\tcontainer: not running"), Ok(id) => line!("\tcontainer: {}", short(id.trim())), Err(e) => line!("\tcontainer: {e:#}"), diff --git a/src/ctx.rs b/src/ctx.rs index 1099bc6..4714ab5 100644 --- a/src/ctx.rs +++ b/src/ctx.rs @@ -1,6 +1,22 @@ +use crate::cmd::Compose; +use crate::fsops::Fs; + // how ahab was invoked, handed to everything that runs commands or writes files pub struct Ctx { pub verbose: bool, pub dry_run: bool, pub quiet: bool, } + +impl Ctx { + // the two things these flags actually decide, asked for by name rather than + // read field by field wherever they are needed: a dry run writes nothing, + // and --quiet stops compose narrating + pub fn fs(&self) -> Fs<'_> { + Fs::new(self) + } + + pub fn compose(&self) -> Compose<'_> { + Compose::new(self) + } +} diff --git a/src/fsops.rs b/src/fsops.rs index c97bd63..b4f0e58 100644 --- a/src/fsops.rs +++ b/src/fsops.rs @@ -16,68 +16,155 @@ pub fn suffixed(path: &Path, suffix: &str) -> PathBuf { PathBuf::from(out) } -// every write to the working tree goes through this module, so a dry run is held -// back in one place rather than at each call site -pub fn move_path(ctx: &Ctx, src: &Path, target: &Path) -> Result<()> { - if ctx.dry_run { - return Ok(()); - } - - ensure_parent(ctx, target)?; - - // rename cannot cross filesystems, and the store often is another one - match fs::rename(src, target) { - Ok(()) => Ok(()), - Err(rename_err) => match copy_recursive(src, target) { - Ok(()) => remove_recursive(ctx, src), - Err(copy_err) => Err(copy_err).with_context(|| format!("after {rename_err}")), - }, - } +// the working tree, as this invocation is allowed to touch it. every write goes +// through here, so what a dry run means is settled in one place rather than at +// the top of each operation, where the next one added could simply not ask +pub struct Fs<'a> { + ctx: &'a Ctx, } -pub fn rename(ctx: &Ctx, src: &Path, target: &Path) -> Result<()> { - if ctx.dry_run { - return Ok(()); +impl<'a> Fs<'a> { + pub(crate) fn new(ctx: &'a Ctx) -> Self { + Self { ctx } } - Ok(fs::rename(src, target)?) -} - -pub fn place_link(ctx: &Ctx, link_path: &Path, target: &Path) -> Result<()> { - if ctx.dry_run { - return Ok(()); - } - - // the parent can be missing when the store holds a path the repository does - // not have any more, and symlink would only report the bare ENOENT - ensure_parent(ctx, link_path)?; - - // symlink under a temp name and rename over the path: the rename is atomic - let tmp = suffixed(link_path, ".ahab-tmp"); - - // only ahab's own leftover is cleared away: anything else here belongs to - // the project, and silently unlinking it would lose it - match fs::symlink_metadata(&tmp) { - Ok(meta) if meta.is_symlink() => fs::remove_file(&tmp)?, - Ok(_) => { - return Err(anyhow!( - "{} is in the way and is not a symlink ahab left behind; \ - move it aside", - tmp.display() - )); + // a dry run writes nothing and reports that nothing went wrong, which for + // every operation here is its empty answer + fn unless_planned(&self, act: impl FnOnce() -> Result) -> Result { + match self.ctx.dry_run { + true => Ok(T::default()), + false => act(), } - Err(e) if e.kind() == std::io::ErrorKind::NotFound => {} - Err(e) => return Err(e.into()), } - symlink(target, &tmp)?; - Ok(fs::rename(&tmp, link_path)?) + pub fn move_path(&self, src: &Path, target: &Path) -> Result<()> { + self.unless_planned(|| { + ensure_parent(target)?; + + // rename cannot cross filesystems, and the store often is another one + match fs::rename(src, target) { + Ok(()) => Ok(()), + Err(rename_err) => match copy_recursive(src, target) { + Ok(()) => remove_recursive(src), + Err(copy_err) => Err(copy_err).with_context(|| format!("after {rename_err}")), + }, + } + }) + } + + pub fn rename(&self, src: &Path, target: &Path) -> Result<()> { + self.unless_planned(|| Ok(fs::rename(src, target)?)) + } + + pub fn place_link(&self, link_path: &Path, target: &Path) -> Result<()> { + self.unless_planned(|| { + // the parent can be missing when the store holds a path the + // repository does not have any more, and symlink would only report + // the bare ENOENT + ensure_parent(link_path)?; + + // symlink under a temp name and rename over the path: the rename is + // atomic + let tmp = suffixed(link_path, ".ahab-tmp"); + + // only ahab's own leftover is cleared away: anything else here + // belongs to the project, and silently unlinking it would lose it + match fs::symlink_metadata(&tmp) { + Ok(meta) if meta.is_symlink() => fs::remove_file(&tmp)?, + Ok(_) => { + return Err(anyhow!( + "{} is in the way and is not a symlink ahab left behind; \ + move it aside", + tmp.display() + )); + } + Err(e) if e.kind() == std::io::ErrorKind::NotFound => {} + Err(e) => return Err(e.into()), + } + + symlink(target, &tmp)?; + Ok(fs::rename(&tmp, link_path)?) + }) + } + + pub fn remove_file(&self, path: &Path) -> Result<()> { + self.unless_planned(|| Ok(fs::remove_file(path)?)) + } + + pub fn create_dir(&self, path: &Path) -> Result<()> { + self.unless_planned(|| Ok(fs::create_dir(path)?)) + } + + // created if it is missing, left as it is otherwise, which is what a caller + // making an empty __init__.py wants + pub fn touch(&self, path: &Path) -> Result<()> { + self.unless_planned(|| { + fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(false) + .open(path)?; + + Ok(()) + }) + } + + // fails rather than writing over a file that is already there + pub fn write_new(&self, path: &Path, contents: &[u8]) -> Result<()> { + self.unless_planned(|| { + let mut file = fs::File::create_new(path)?; + file.write_all(contents)?; + + Ok(()) + }) + } + + // the store holds what should not be reachable from the repository, so the + // directories it is kept in are the owner's alone. only the tree at or + // below `base` is created here; what is above it is the user's own business + pub fn ensure_private_parent(&self, base: &Path, target: &Path) -> Result<()> { + self.unless_planned(|| { + let Some(parent) = target.parent().filter(|dir| dir.starts_with(base)) else { + return Ok(()); + }; + + // a recursive create applies the mode to every directory it makes + std::fs::DirBuilder::new() + .recursive(true) + .mode(0o700) + .create(parent) + .with_context(|| format!("creating {}", parent.display())) + }) + } + + // a restored path can leave the store holding nothing but empty directories + pub fn prune_empty(&self, dir: Option<&Path>, stop: &Path) { + let pruned: Result<()> = self.unless_planned(|| { + let mut dir = dir; + + while let Some(path) = dir { + if path == stop || !path.starts_with(stop) || fs::remove_dir(path).is_err() { + break; + } + + dir = path.parent(); + } + + Ok(()) + }); + + // an empty directory left behind is untidy, not a failure + let _ = pruned; + } } // 0600 and only where nothing is yet. a dump is the whole database, and // pg_dumpall's is every role's password hash, so it is not the owner's to share // by default; create_new also refuses to follow a symlink planted at the path, -// which File::create would open and truncate +// which File::create would open and truncate. +// +// not on Fs: a dry run has no file to hand back, so its one caller asks for +// this only once it knows it is writing for real pub fn create_private_new(path: &Path) -> Result { std::fs::OpenOptions::new() .write(true) @@ -87,40 +174,15 @@ pub fn create_private_new(path: &Path) -> Result { .with_context(|| format!("creating {}", path.display())) } -// the store holds what should not be reachable from the repository, so the -// directories it is kept in are the owner's alone. only the tree at or below -// `base` is created here; what is above it is the user's own business -pub fn ensure_private_parent(ctx: &Ctx, base: &Path, target: &Path) -> Result<()> { - if ctx.dry_run { - return Ok(()); +fn ensure_parent(target: &Path) -> Result<()> { + match target.parent() { + Some(parent) if !parent.as_os_str().is_empty() => Ok(fs::create_dir_all(parent)?), + _ => Ok(()), } - - let Some(parent) = target.parent().filter(|dir| dir.starts_with(base)) else { - return Ok(()); - }; - - // a recursive create applies the mode to every directory it makes - std::fs::DirBuilder::new() - .recursive(true) - .mode(0o700) - .create(parent) - .with_context(|| format!("creating {}", parent.display())) -} - -pub fn remove_file(ctx: &Ctx, path: &Path) -> Result<()> { - if ctx.dry_run { - return Ok(()); - } - - Ok(fs::remove_file(path)?) } // whatever is there, file, directory or symlink -fn remove_recursive(ctx: &Ctx, path: &Path) -> Result<()> { - if ctx.dry_run { - return Ok(()); - } - +fn remove_recursive(path: &Path) -> Result<()> { if fs::symlink_metadata(path)?.is_dir() { fs::remove_dir_all(path)?; } else { @@ -130,70 +192,6 @@ fn remove_recursive(ctx: &Ctx, path: &Path) -> Result<()> { Ok(()) } -fn ensure_parent(ctx: &Ctx, target: &Path) -> Result<()> { - if ctx.dry_run { - return Ok(()); - } - - match target.parent() { - Some(parent) if !parent.as_os_str().is_empty() => Ok(fs::create_dir_all(parent)?), - _ => Ok(()), - } -} - -pub fn create_dir(ctx: &Ctx, path: &Path) -> Result<()> { - if ctx.dry_run { - return Ok(()); - } - - Ok(fs::create_dir(path)?) -} - -// created if it is missing, left as it is otherwise, which is what a caller -// making an empty __init__.py wants -pub fn touch(ctx: &Ctx, path: &Path) -> Result<()> { - if ctx.dry_run { - return Ok(()); - } - - fs::OpenOptions::new() - .write(true) - .create(true) - .truncate(false) - .open(path)?; - - Ok(()) -} - -// fails rather than writing over a file that is already there -pub fn write_new(ctx: &Ctx, path: &Path, contents: &[u8]) -> Result<()> { - if ctx.dry_run { - return Ok(()); - } - - let mut file = fs::File::create_new(path)?; - file.write_all(contents)?; - - Ok(()) -} - -// a restored path can leave the store holding nothing but empty directories -pub fn prune_empty(ctx: &Ctx, dir: Option<&Path>, stop: &Path) { - if ctx.dry_run { - return; - } - - let mut dir = dir; - - while let Some(path) = dir { - if path == stop || !path.starts_with(stop) || fs::remove_dir(path).is_err() { - return; - } - - dir = path.parent(); - } -} - fn copy_recursive(src: &Path, target: &Path) -> Result<()> { let meta = fs::symlink_metadata(src)?; diff --git a/src/project.rs b/src/project.rs index 124c936..b32efdb 100644 --- a/src/project.rs +++ b/src/project.rs @@ -1,7 +1,7 @@ use anyhow::{Context, Result, anyhow}; use serde_json::Value; -use crate::cmd::{Cmd, Config}; +use crate::cmd::Cmd; use crate::ctx::Ctx; const POSTGRES_IMAGES: [&str; 4] = ["postg", "timescale", "pgvector", "citus"]; @@ -13,7 +13,7 @@ pub struct Project { impl Project { pub fn resolve(ctx: &Ctx) -> Result { - let json = Config.capture(ctx)?; + let json = ctx.compose().config().capture(ctx)?; let config: Value = serde_json::from_str(&json).context("parsing docker compose config")?; let services = config .get("services")