refactor: put the dry-run and quiet checks on Ctx
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
use super::{Argv, Cmd};
|
use super::{Argv, Cmd};
|
||||||
|
use crate::ctx::Ctx;
|
||||||
|
|
||||||
// compose prints its own progress, which --quiet has to ask it to stop
|
// compose prints its own progress, which --quiet has to ask it to stop
|
||||||
fn compose(quiet: bool) -> Argv {
|
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
|
// docker compose run --rm, which runs the image's entrypoint and so fixes up the
|
||||||
// container user before handing over
|
// container user before handing over
|
||||||
pub struct Run {
|
pub struct Run {
|
||||||
@@ -19,20 +63,15 @@ pub struct Run {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl 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 {
|
Self {
|
||||||
service: service.to_string(),
|
service: service.to_string(),
|
||||||
inner,
|
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 {
|
impl Cmd for Run {
|
||||||
@@ -59,14 +98,6 @@ pub struct Ps {
|
|||||||
service: String,
|
service: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Ps {
|
|
||||||
pub fn id_of(service: &str) -> Self {
|
|
||||||
Self {
|
|
||||||
service: service.to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Cmd for Ps {
|
impl Cmd for Ps {
|
||||||
fn argv(&self) -> Argv {
|
fn argv(&self) -> Argv {
|
||||||
compose(false).arg("ps").arg("--quiet").arg(&self.service)
|
compose(false).arg("ps").arg("--quiet").arg(&self.service)
|
||||||
@@ -74,7 +105,7 @@ impl Cmd for Ps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct Up {
|
pub struct Up {
|
||||||
pub quiet: bool,
|
quiet: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Cmd for Up {
|
impl Cmd for Up {
|
||||||
@@ -84,8 +115,8 @@ impl Cmd for Up {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct Start<'a> {
|
pub struct Start<'a> {
|
||||||
pub service: &'a str,
|
service: &'a str,
|
||||||
pub quiet: bool,
|
quiet: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Cmd for Start<'_> {
|
impl Cmd for Start<'_> {
|
||||||
@@ -95,7 +126,7 @@ impl Cmd for Start<'_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct Stop {
|
pub struct Stop {
|
||||||
pub quiet: bool,
|
quiet: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Cmd for Stop {
|
impl Cmd for Stop {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ use std::{
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
pub use argv::Argv;
|
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 django::{Bash, Manage, Words};
|
||||||
pub use docker::{Cp, Exec};
|
pub use docker::{Cp, Exec};
|
||||||
pub use git::{CheckIgnore, ConfigGet, LsFiles, RevParse};
|
pub use git::{CheckIgnore, ConfigGet, LsFiles, RevParse};
|
||||||
@@ -43,9 +43,12 @@ pub trait Cmd {
|
|||||||
Exec::wrapping(container, self.argv())
|
Exec::wrapping(container, self.argv())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn in_service(&self, service: &str) -> Run {
|
// the context comes in here because compose narrates what it is doing, and
|
||||||
Run::wrapping(service, self.argv())
|
// 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
|
// terminals
|
||||||
fn run(&self, ctx: &Ctx) -> Result<()> {
|
fn run(&self, ctx: &Ctx) -> Result<()> {
|
||||||
self.argv().run(ctx)
|
self.argv().run(ctx)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use anyhow::{Result, anyhow};
|
|||||||
|
|
||||||
use crate::cmd::{Bash, Cmd, Manage, Words};
|
use crate::cmd::{Bash, Cmd, Manage, Words};
|
||||||
use crate::ctx::Ctx;
|
use crate::ctx::Ctx;
|
||||||
use crate::fsops::{create_dir, touch, write_new};
|
|
||||||
use crate::output::note;
|
use crate::output::note;
|
||||||
use crate::project::Project;
|
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");
|
let management_dir = app_dir.join("management");
|
||||||
|
|
||||||
if !management_dir.exists() {
|
if !management_dir.exists() {
|
||||||
create_dir(ctx, &management_dir)?;
|
ctx.fs().create_dir(&management_dir)?;
|
||||||
touch(ctx, &management_dir.join("__init__.py"))?;
|
ctx.fs().touch(&management_dir.join("__init__.py"))?;
|
||||||
|
|
||||||
note!(ctx, "created module {app_name}.management")
|
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");
|
let commands_dir = management_dir.join("commands");
|
||||||
|
|
||||||
if !commands_dir.exists() {
|
if !commands_dir.exists() {
|
||||||
create_dir(ctx, &commands_dir)?;
|
ctx.fs().create_dir(&commands_dir)?;
|
||||||
touch(ctx, &commands_dir.join("__init__.py"))?;
|
ctx.fs().touch(&commands_dir.join("__init__.py"))?;
|
||||||
|
|
||||||
note!(ctx, "created module {app_name}.management.commands")
|
note!(ctx, "created module {app_name}.management.commands")
|
||||||
};
|
};
|
||||||
|
|
||||||
write_new(
|
ctx.fs().write_new(
|
||||||
ctx,
|
|
||||||
&commands_dir.join(format!("{name}.py")),
|
&commands_dir.join(format!("{name}.py")),
|
||||||
DEBUG_TEMPLATE.as_bytes(),
|
DEBUG_TEMPLATE.as_bytes(),
|
||||||
)?;
|
)?;
|
||||||
@@ -72,22 +71,18 @@ fn is_module_name(name: &str) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn bash(ctx: &Ctx) -> Result<()> {
|
pub fn bash(ctx: &Ctx) -> Result<()> {
|
||||||
Bash.in_service(&service(ctx)?)
|
Bash.in_service(ctx, &service(ctx)?).replace(ctx)
|
||||||
.quiet(ctx.quiet)
|
|
||||||
.replace(ctx)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run(ctx: &Ctx, rest: &[String]) -> Result<()> {
|
pub fn run(ctx: &Ctx, rest: &[String]) -> Result<()> {
|
||||||
Words::new(rest)
|
Words::new(rest)
|
||||||
.in_service(&service(ctx)?)
|
.in_service(ctx, &service(ctx)?)
|
||||||
.quiet(ctx.quiet)
|
|
||||||
.replace(ctx)
|
.replace(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn manage(ctx: &Ctx, rest: &[String]) -> Result<()> {
|
pub fn manage(ctx: &Ctx, rest: &[String]) -> Result<()> {
|
||||||
Manage::new(rest)
|
Manage::new(rest)
|
||||||
.in_service(&service(ctx)?)
|
.in_service(ctx, &service(ctx)?)
|
||||||
.quiet(ctx.quiet)
|
|
||||||
.replace(ctx)
|
.replace(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,9 +11,7 @@ use anyhow::{Context, Result, anyhow, bail};
|
|||||||
|
|
||||||
use self::store::{Leads, Repo, ignored, leads, resolve, symlink_metadata_opt, tracked};
|
use self::store::{Leads, Repo, ignored, leads, resolve, symlink_metadata_opt, tracked};
|
||||||
use crate::ctx::Ctx;
|
use crate::ctx::Ctx;
|
||||||
use crate::fsops::{
|
use crate::fsops::suffixed;
|
||||||
ensure_private_parent, move_path, place_link, prune_empty, remove_file, rename, suffixed,
|
|
||||||
};
|
|
||||||
use crate::output::{line, note, warning};
|
use crate::output::{line, note, warning};
|
||||||
|
|
||||||
const BACKUP_SUFFIX: &str = ".ahab-bak";
|
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());
|
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) {
|
if let Err(e) = ctx.fs().move_path(&stored, &src) {
|
||||||
rename(ctx, &aside, &src).with_context(|| {
|
ctx.fs().rename(&aside, &src).with_context(|| {
|
||||||
format!(
|
format!(
|
||||||
"could not put the link at {} back after failing to restore it",
|
"could not put the link at {} back after failing to restore it",
|
||||||
src.display()
|
src.display()
|
||||||
@@ -150,8 +148,8 @@ fn restore_one(ctx: &Ctx, repo: &Repo, path: &Path, report: &Report) -> Result<(
|
|||||||
return Err(e);
|
return Err(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
remove_file(ctx, &aside)?;
|
ctx.fs().remove_file(&aside)?;
|
||||||
prune_empty(ctx, stored.parent(), &repo.base);
|
ctx.fs().prune_empty(stored.parent(), &repo.base);
|
||||||
|
|
||||||
report.line("restored", &rel);
|
report.line("restored", &rel);
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -295,7 +293,7 @@ fn link_one(ctx: &Ctx, repo: &Repo, path: &Path, force: bool, report: &Report) -
|
|||||||
stays_in_store(repo, &rel)?;
|
stays_in_store(repo, &rel)?;
|
||||||
// before anything is moved in, so the tree it lands in is never briefly
|
// before anything is moved in, so the tree it lands in is never briefly
|
||||||
// readable by anyone else
|
// readable by anyone else
|
||||||
ensure_private_parent(ctx, &repo.base, &target)?;
|
ctx.fs().ensure_private_parent(&repo.base, &target)?;
|
||||||
|
|
||||||
if tracked(ctx, repo, &rel)? {
|
if tracked(ctx, repo, &rel)? {
|
||||||
return Err(anyhow!(
|
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));
|
return Err(needs_force(&target));
|
||||||
}
|
}
|
||||||
|
|
||||||
place_link(ctx, &src, &target)?;
|
ctx.fs().place_link(&src, &target)?;
|
||||||
report.line("repointed", &rel);
|
report.line("repointed", &rel);
|
||||||
Ok(())
|
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));
|
report.line("saved", &suffixed(&rel, BACKUP_SUFFIX));
|
||||||
|
|
||||||
place_link(ctx, &src, &target)?;
|
ctx.fs().place_link(&src, &target)?;
|
||||||
report.line("linked", &rel);
|
report.line("linked", &rel);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -399,7 +397,7 @@ fn link_one(ctx: &Ctx, repo: &Repo, path: &Path, force: bool, report: &Report) -
|
|||||||
if !force {
|
if !force {
|
||||||
return Err(needs_force(&target));
|
return Err(needs_force(&target));
|
||||||
}
|
}
|
||||||
place_link(ctx, &src, &target)?;
|
ctx.fs().place_link(&src, &target)?;
|
||||||
report.line("linked", &rel);
|
report.line("linked", &rel);
|
||||||
Ok(())
|
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
|
// 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
|
// tree has lost it altogether, which is the one outcome worse than failing
|
||||||
fn move_and_link(ctx: &Ctx, src: &Path, target: &Path) -> Result<()> {
|
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) {
|
if let Err(e) = ctx.fs().place_link(src, target) {
|
||||||
move_path(ctx, target, src).with_context(|| {
|
ctx.fs().move_path(target, src).with_context(|| {
|
||||||
format!(
|
format!(
|
||||||
"could not put {} back after failing to link it to {}",
|
"could not put {} back after failing to link it to {}",
|
||||||
src.display(),
|
src.display(),
|
||||||
|
|||||||
@@ -11,11 +11,10 @@ use self::server::{Database, wait_until_ready, when_ready};
|
|||||||
use self::shape::{Dump, HEADER_LEN, Kind};
|
use self::shape::{Dump, HEADER_LEN, Kind};
|
||||||
use crate::cli::Format;
|
use crate::cli::Format;
|
||||||
use crate::cmd::{
|
use crate::cmd::{
|
||||||
Cmd, Cp, CreateDb, DropDb, Gunzip, Gzip, Head, PgDump, PgDumpAll, PgRestore, Psql, Rm, Start,
|
Cmd, Cp, CreateDb, DropDb, Gunzip, Gzip, Head, PgDump, PgDumpAll, PgRestore, Psql, Rm,
|
||||||
Stop, Up,
|
|
||||||
};
|
};
|
||||||
use crate::ctx::Ctx;
|
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};
|
use crate::output::{note, warning};
|
||||||
|
|
||||||
// unique per run: docker cp will not copy a directory over an existing path, and
|
// 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");
|
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
|
// 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
|
// 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<()> {
|
fn imported(ctx: &Ctx, db: &Database, dump: &Dump, file: &Path) -> Result<()> {
|
||||||
note!(ctx, "starting db container");
|
note!(ctx, "starting db container");
|
||||||
Start {
|
ctx.compose().start(&db.service).run(ctx)?;
|
||||||
service: &db.service,
|
|
||||||
quiet: ctx.quiet,
|
|
||||||
}
|
|
||||||
.run(ctx)?;
|
|
||||||
|
|
||||||
let remote = remote_dump();
|
let remote = remote_dump();
|
||||||
|
|
||||||
@@ -229,8 +224,8 @@ fn imported(ctx: &Ctx, db: &Database, dump: &Dump, file: &Path) -> Result<()> {
|
|||||||
restored?;
|
restored?;
|
||||||
|
|
||||||
note!(ctx, "restarting containers");
|
note!(ctx, "restarting containers");
|
||||||
Stop { quiet: ctx.quiet }.run(ctx)?;
|
ctx.compose().stop().run(ctx)?;
|
||||||
Up { quiet: ctx.quiet }.run(ctx)?;
|
ctx.compose().up().run(ctx)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -337,11 +332,11 @@ pub fn dump(ctx: &Ctx, file: &Path, format: Format, gzip: bool) -> Result<()> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if let Err(e) = dumped {
|
if let Err(e) = dumped {
|
||||||
let _ = remove_file(ctx, &partial);
|
let _ = ctx.fs().remove_file(&partial);
|
||||||
return Err(e);
|
return Err(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
rename(ctx, &partial, file)?;
|
ctx.fs().rename(&partial, file)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use std::time::{Duration, Instant};
|
|||||||
use anyhow::{Result, bail};
|
use anyhow::{Result, bail};
|
||||||
|
|
||||||
use super::shape::Kind;
|
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::ctx::Ctx;
|
||||||
use crate::project::Project;
|
use crate::project::Project;
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ impl Database {
|
|||||||
let service = compose.postgres()?;
|
let service = compose.postgres()?;
|
||||||
let (user, name) = compose.postgres_credentials(&service)?;
|
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 mut ids = listed.lines().map(str::trim).filter(|id| !id.is_empty());
|
||||||
|
|
||||||
let container = match (ids.next(), ids.next()) {
|
let container = match (ids.next(), ids.next()) {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use std::path::Path;
|
|||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
use crate::cmd::{Cmd, Ps};
|
use crate::cmd::Cmd;
|
||||||
use crate::commands::link;
|
use crate::commands::link;
|
||||||
use crate::ctx::Ctx;
|
use crate::ctx::Ctx;
|
||||||
use crate::output::line;
|
use crate::output::line;
|
||||||
@@ -62,7 +62,7 @@ fn role(ctx: &Ctx, role: &str, detected: Result<String>, project: &Project) {
|
|||||||
|
|
||||||
line!("{role}: {service} ({image})");
|
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) if id.trim().is_empty() => line!("\tcontainer: not running"),
|
||||||
Ok(id) => line!("\tcontainer: {}", short(id.trim())),
|
Ok(id) => line!("\tcontainer: {}", short(id.trim())),
|
||||||
Err(e) => line!("\tcontainer: {e:#}"),
|
Err(e) => line!("\tcontainer: {e:#}"),
|
||||||
|
|||||||
16
src/ctx.rs
16
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
|
// how ahab was invoked, handed to everything that runs commands or writes files
|
||||||
pub struct Ctx {
|
pub struct Ctx {
|
||||||
pub verbose: bool,
|
pub verbose: bool,
|
||||||
pub dry_run: bool,
|
pub dry_run: bool,
|
||||||
pub quiet: 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
286
src/fsops.rs
286
src/fsops.rs
@@ -16,68 +16,155 @@ pub fn suffixed(path: &Path, suffix: &str) -> PathBuf {
|
|||||||
PathBuf::from(out)
|
PathBuf::from(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
// every write to the working tree goes through this module, so a dry run is held
|
// the working tree, as this invocation is allowed to touch it. every write goes
|
||||||
// back in one place rather than at each call site
|
// through here, so what a dry run means is settled in one place rather than at
|
||||||
pub fn move_path(ctx: &Ctx, src: &Path, target: &Path) -> Result<()> {
|
// the top of each operation, where the next one added could simply not ask
|
||||||
if ctx.dry_run {
|
pub struct Fs<'a> {
|
||||||
return Ok(());
|
ctx: &'a Ctx,
|
||||||
}
|
|
||||||
|
|
||||||
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}")),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn rename(ctx: &Ctx, src: &Path, target: &Path) -> Result<()> {
|
impl<'a> Fs<'a> {
|
||||||
if ctx.dry_run {
|
pub(crate) fn new(ctx: &'a Ctx) -> Self {
|
||||||
return Ok(());
|
Self { ctx }
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(fs::rename(src, target)?)
|
// a dry run writes nothing and reports that nothing went wrong, which for
|
||||||
}
|
// every operation here is its empty answer
|
||||||
|
fn unless_planned<T: Default>(&self, act: impl FnOnce() -> Result<T>) -> Result<T> {
|
||||||
pub fn place_link(ctx: &Ctx, link_path: &Path, target: &Path) -> Result<()> {
|
match self.ctx.dry_run {
|
||||||
if ctx.dry_run {
|
true => Ok(T::default()),
|
||||||
return Ok(());
|
false => act(),
|
||||||
}
|
|
||||||
|
|
||||||
// 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()
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
|
|
||||||
Err(e) => return Err(e.into()),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
symlink(target, &tmp)?;
|
pub fn move_path(&self, src: &Path, target: &Path) -> Result<()> {
|
||||||
Ok(fs::rename(&tmp, link_path)?)
|
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
|
// 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
|
// 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,
|
// 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::File> {
|
pub fn create_private_new(path: &Path) -> Result<std::fs::File> {
|
||||||
std::fs::OpenOptions::new()
|
std::fs::OpenOptions::new()
|
||||||
.write(true)
|
.write(true)
|
||||||
@@ -87,40 +174,15 @@ pub fn create_private_new(path: &Path) -> Result<std::fs::File> {
|
|||||||
.with_context(|| format!("creating {}", path.display()))
|
.with_context(|| format!("creating {}", path.display()))
|
||||||
}
|
}
|
||||||
|
|
||||||
// the store holds what should not be reachable from the repository, so the
|
fn ensure_parent(target: &Path) -> Result<()> {
|
||||||
// directories it is kept in are the owner's alone. only the tree at or below
|
match target.parent() {
|
||||||
// `base` is created here; what is above it is the user's own business
|
Some(parent) if !parent.as_os_str().is_empty() => Ok(fs::create_dir_all(parent)?),
|
||||||
pub fn ensure_private_parent(ctx: &Ctx, base: &Path, target: &Path) -> Result<()> {
|
_ => Ok(()),
|
||||||
if ctx.dry_run {
|
|
||||||
return 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
|
// whatever is there, file, directory or symlink
|
||||||
fn remove_recursive(ctx: &Ctx, path: &Path) -> Result<()> {
|
fn remove_recursive(path: &Path) -> Result<()> {
|
||||||
if ctx.dry_run {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
if fs::symlink_metadata(path)?.is_dir() {
|
if fs::symlink_metadata(path)?.is_dir() {
|
||||||
fs::remove_dir_all(path)?;
|
fs::remove_dir_all(path)?;
|
||||||
} else {
|
} else {
|
||||||
@@ -130,70 +192,6 @@ fn remove_recursive(ctx: &Ctx, path: &Path) -> Result<()> {
|
|||||||
Ok(())
|
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<()> {
|
fn copy_recursive(src: &Path, target: &Path) -> Result<()> {
|
||||||
let meta = fs::symlink_metadata(src)?;
|
let meta = fs::symlink_metadata(src)?;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use anyhow::{Context, Result, anyhow};
|
use anyhow::{Context, Result, anyhow};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::cmd::{Cmd, Config};
|
use crate::cmd::Cmd;
|
||||||
use crate::ctx::Ctx;
|
use crate::ctx::Ctx;
|
||||||
|
|
||||||
const POSTGRES_IMAGES: [&str; 4] = ["postg", "timescale", "pgvector", "citus"];
|
const POSTGRES_IMAGES: [&str; 4] = ["postg", "timescale", "pgvector", "citus"];
|
||||||
@@ -13,7 +13,7 @@ pub struct Project {
|
|||||||
|
|
||||||
impl Project {
|
impl Project {
|
||||||
pub fn resolve(ctx: &Ctx) -> Result<Self> {
|
pub fn resolve(ctx: &Ctx) -> Result<Self> {
|
||||||
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 config: Value = serde_json::from_str(&json).context("parsing docker compose config")?;
|
||||||
let services = config
|
let services = config
|
||||||
.get("services")
|
.get("services")
|
||||||
|
|||||||
Reference in New Issue
Block a user