refactor: one helper each for the repeated blocks

This commit is contained in:
2026-09-09 12:42:23 +00:00
parent 5dbe99a304
commit c55273d80c
6 changed files with 104 additions and 58 deletions

View File

@@ -13,47 +13,59 @@ use self::store::{Leads, Repo, ignored, leads, resolve, symlink_metadata_opt, tr
use crate::cli::link as cli;
use crate::ctx::Ctx;
use crate::fsops::suffixed;
use crate::output::{line, note, warning};
use crate::output::{line, note, plural, warning};
const BACKUP_SUFFIX: &str = ".ahab-bak";
// where the link waits while the payload comes back out of the store
const RESTORING_SUFFIX: &str = ".ahab-restoring";
// move untracked paths out of the repo and symlink them back
pub fn add(ctx: &Ctx, args: &cli::Add) -> Result<()> {
let (paths, force) = (args.paths.as_slice(), args.force);
let repo = Repo::discover(ctx, args.store.root())?;
let report = Report::new(&repo);
if let [path] = paths {
return link_one(ctx, &repo, path, force, &report);
// one path's failure is its own; several are counted, so a run over a list says
// what happened to each and then how the run as a whole went. a single path has
// nothing to count, so its error is simply the command's
fn each(paths: &[PathBuf], mut act: impl FnMut(&Path) -> Result<()>) -> Result<()> {
if let [only] = paths {
return act(only);
}
let mut failed = 0;
for path in paths {
if let Err(e) = link_one(ctx, &repo, path, force, &report) {
if let Err(e) = act(path) {
warning!("{e:#}");
failed += 1;
}
}
if failed > 0 {
return Err(anyhow!("{failed} of {} paths failed", paths.len()));
match failed {
0 => Ok(()),
failed => Err(anyhow!("{failed} of {} paths failed", paths.len())),
}
Ok(())
}
// move untracked paths out of the repo and symlink them back
pub fn add(ctx: &Ctx, args: &cli::Add) -> Result<()> {
let repo = Repo::discover(ctx, args.store.root())?;
let report = Report::new(&repo);
each(&args.paths, |path| {
link_one(ctx, &repo, path, args.force, &report)
})
}
// move paths in the store back into the repo, the inverse of add
pub fn restore(ctx: &Ctx, args: &cli::Restore) -> Result<()> {
let (paths, all) = (args.paths.as_slice(), args.all);
let repo = Repo::discover(ctx, args.store.root())?;
let report = Report::new(&repo);
// clap requires one or the other and refuses both, so only the two real
// cases are left here
let stored = match all {
let stored = match args.all {
true => stored_paths(&repo, &repo.store)?,
false => paths.iter().cloned().map(|p| (p, Stored::Linked)).collect(),
false => args
.paths
.iter()
.cloned()
.map(|path| (path, Stored::Linked))
.collect(),
};
// only a linked path can be moved back; the store can hold orphans too
@@ -70,7 +82,7 @@ pub fn restore(ctx: &Ctx, args: &cli::Restore) -> Result<()> {
n => note!(
ctx,
"nothing to restore: {n} path{} in the store {} not linked, see `ahab link list`",
if n == 1 { "" } else { "s" },
plural(n),
if n == 1 { "is" } else { "are" }
),
}
@@ -82,26 +94,11 @@ pub fn restore(ctx: &Ctx, args: &cli::Restore) -> Result<()> {
note!(
ctx,
"leaving {skipped} path{} that the store holds but nothing links to",
if skipped == 1 { "" } else { "s" }
plural(skipped)
);
}
if let [path] = linked.as_slice() {
return restore_one(ctx, &repo, path, &report);
}
let mut failed = 0;
for path in &linked {
if let Err(e) = restore_one(ctx, &repo, path, &report) {
warning!("{e:#}");
failed += 1;
}
}
if failed > 0 {
return Err(anyhow!("{failed} of {} paths failed", linked.len()));
}
Ok(())
each(&linked, |path| restore_one(ctx, &repo, path, &report))
}
fn restore_one(ctx: &Ctx, repo: &Repo, path: &Path, report: &Report) -> Result<()> {
@@ -247,12 +244,18 @@ pub fn list(ctx: &Ctx, args: &cli::List) -> Result<()> {
Stored::Taken => "shadowed",
};
line!("\t{:<11}{}", format!("{verb}:"), rel.display());
entry(verb, rel);
}
Ok(())
}
// one line of what the store did with a path, in the column width `status`
// prints too: the two used to set it separately and had to be kept in step
fn entry(verb: &str, path: &Path) {
line!("\t{:<11}{}", format!("{verb}:"), path.display());
}
struct Report {
store: PathBuf,
named: Cell<bool>,
@@ -272,7 +275,7 @@ impl Report {
line!("store: {}", self.store.display());
}
line!("\t{:<11}{}", format!("{verb}:"), path.display());
entry(verb, path);
}
}

View File

@@ -16,7 +16,7 @@ use crate::cmd::{
};
use crate::ctx::Ctx;
use crate::fsops::{create_private_new, suffixed};
use crate::output::{note, warning};
use crate::output::{note, plural, warning};
// unique per run: docker cp will not copy a directory over an existing path, and
// quietly leaves whatever was there for pg_restore to read instead
@@ -36,6 +36,13 @@ fn partial_path(file: &Path) -> PathBuf {
suffixed(file, &format!(".{}.partial", std::process::id()))
}
// the database was dropped before the restore was attempted, so a restore that
// failed leaves nothing behind: said the same way wherever it happens, since
// what the user has to do about it does not depend on which tool it was
fn left_empty(tool: &str) -> anyhow::Error {
anyhow::anyhow!("{tool} failed, the database is left empty")
}
// pg_dumpall recreates roles the cluster already has, so this is the expected
// complaint rather than a failure
fn is_existing_role_error(line: &str) -> bool {
@@ -75,17 +82,17 @@ fn restore_cluster(ctx: &Ctx, db: &Database, restore: &dyn Cmd, file: &Path) ->
note!(
ctx,
"left {existing} existing role{} alone",
if existing == 1 { "" } else { "s" }
plural(existing)
);
}
if !out.status.success() {
bail!("psql failed, the database is left empty");
return Err(left_empty("psql"));
}
if failed > 0 {
bail!(
"psql reported {failed} error{}, so the cluster restored only in part",
if failed == 1 { "" } else { "s" }
plural(failed)
);
}
@@ -99,7 +106,7 @@ fn gzip_kind(ctx: &Ctx, db: &Database, file: &Path) -> Result<Kind> {
wait_until_ready(ctx, db)?;
let out = Gunzip
.pipe(&Head::bytes(HEADER_LEN))
.pipe(&Head::bytes(HEADER_LEN))?
// head closes the pipe once it has its bytes, which kills gunzip
.allow_early_close()
.in_container(&db.container)
@@ -257,14 +264,14 @@ fn restore_dump(
.status(ctx)?;
if !status.success() {
bail!("{tool} failed, the database is left empty");
return Err(left_empty(tool));
}
return Ok(());
}
let restore: Box<dyn Cmd> = match dump {
Dump::Gzip => Box::new(Gunzip.pipe(&*restore)),
Dump::Gzip => Box::new(Gunzip.pipe(&*restore)?),
_ => restore,
};
@@ -280,7 +287,7 @@ fn restore_dump(
.stdin_from(ctx, file)?;
if !status.success() {
bail!("{tool} failed, the database is left empty");
return Err(left_empty(tool));
}
Ok(())
@@ -328,7 +335,7 @@ pub fn dump(ctx: &Ctx, args: &cli::Dump) -> Result<()> {
let dumped = if gzip {
dumping
.pipe(&Gzip)
.pipe(&Gzip)?
.in_container(&db.container)
.stream_to(ctx, stdout)
} else {