refactor: name the modules after what they hold
This commit is contained in:
284
src/commands/link.rs
Normal file
284
src/commands/link.rs
Normal file
@@ -0,0 +1,284 @@
|
||||
mod check;
|
||||
mod store;
|
||||
|
||||
pub use check::check;
|
||||
|
||||
use fs_err::{read_dir, read_link};
|
||||
use std::cell::Cell;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{Result, anyhow, bail};
|
||||
|
||||
use self::store::{Repo, ignored, resolve, symlink_metadata_opt, tracked};
|
||||
use crate::ctx::Ctx;
|
||||
use crate::fsops::{move_path, place_link, prune_empty, remove_file, rename, suffixed};
|
||||
use crate::output::{note, warning};
|
||||
|
||||
const BACKUP_SUFFIX: &str = ".ahab-bak";
|
||||
|
||||
// move untracked paths out of the repo and symlink them back
|
||||
pub fn add(ctx: &Ctx, paths: &[PathBuf], force: bool, store: Option<&Path>) -> Result<()> {
|
||||
let repo = Repo::discover(ctx, store)?;
|
||||
let report = Report::new(&repo);
|
||||
|
||||
if let [path] = paths {
|
||||
return link_one(ctx, &repo, path, force, &report);
|
||||
}
|
||||
|
||||
let mut failed = 0;
|
||||
for path in paths {
|
||||
if let Err(e) = link_one(ctx, &repo, path, force, &report) {
|
||||
note!("error: {e:#}");
|
||||
failed += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if failed > 0 {
|
||||
return Err(anyhow!("{failed} of {} paths failed", paths.len()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// move paths in the store back into the repo, the inverse of add
|
||||
pub fn restore(ctx: &Ctx, paths: &[PathBuf], all: bool, store: Option<&Path>) -> Result<()> {
|
||||
let repo = Repo::discover(ctx, store)?;
|
||||
let report = Report::new(&repo);
|
||||
|
||||
let paths = match (all, paths) {
|
||||
(true, []) => linked_paths(&repo, &repo.store)?,
|
||||
(true, _) => bail!("--all restores everything, so it takes no paths"),
|
||||
(false, []) => bail!("name a path to restore, or pass --all"),
|
||||
(false, paths) => paths.to_vec(),
|
||||
};
|
||||
|
||||
if paths.is_empty() {
|
||||
note!("nothing in the store for this repository");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let [path] = paths.as_slice() {
|
||||
return restore_one(ctx, &repo, path, &report);
|
||||
}
|
||||
|
||||
let mut failed = 0;
|
||||
for path in &paths {
|
||||
if let Err(e) = restore_one(ctx, &repo, path, &report) {
|
||||
note!("error: {e:#}");
|
||||
failed += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if failed > 0 {
|
||||
return Err(anyhow!("{failed} of {} paths failed", paths.len()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn restore_one(ctx: &Ctx, repo: &Repo, path: &Path, report: &Report) -> Result<()> {
|
||||
let src = resolve(path)?;
|
||||
let rel = repo.relative(&src)?;
|
||||
let stored = repo.store.join(&rel);
|
||||
|
||||
let Some(meta) = symlink_metadata_opt(&src)? else {
|
||||
bail!("{} does not exist", rel.display());
|
||||
};
|
||||
if !meta.is_symlink() {
|
||||
bail!(
|
||||
"{} is not a symlink, so it is not in the store",
|
||||
rel.display()
|
||||
);
|
||||
}
|
||||
|
||||
let dest = read_link(&src)?;
|
||||
if dest != stored {
|
||||
bail!(
|
||||
"{} points at {}, which is not where the store keeps it",
|
||||
rel.display(),
|
||||
dest.display()
|
||||
);
|
||||
}
|
||||
if symlink_metadata_opt(&stored)?.is_none() {
|
||||
bail!("{} is missing from the store", rel.display());
|
||||
}
|
||||
|
||||
remove_file(ctx, &src)?;
|
||||
move_path(ctx, &stored, &src)?;
|
||||
prune_empty(ctx, stored.parent(), &repo.base);
|
||||
|
||||
report.line("restored", &rel);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// the store mirrors the repository layout, so walking it finds every path this
|
||||
// repository has linked without asking git anything
|
||||
fn linked_paths(repo: &Repo, dir: &Path) -> Result<Vec<PathBuf>> {
|
||||
let mut found = Vec::new();
|
||||
|
||||
let entries = match read_dir(dir) {
|
||||
Ok(entries) => entries,
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(found),
|
||||
Err(e) => return Err(e.into()),
|
||||
};
|
||||
|
||||
for entry in entries {
|
||||
let stored = entry?.path();
|
||||
let rel = stored
|
||||
.strip_prefix(&repo.store)
|
||||
.expect("walked out of the store");
|
||||
let src = repo.root.join(rel);
|
||||
|
||||
let linked = symlink_metadata_opt(&src)?.is_some_and(|meta| meta.is_symlink())
|
||||
&& read_link(&src).is_ok_and(|dest| dest == stored);
|
||||
|
||||
if linked {
|
||||
found.push(src);
|
||||
} else if stored.is_dir() {
|
||||
found.extend(linked_paths(repo, &stored)?);
|
||||
}
|
||||
}
|
||||
|
||||
found.sort();
|
||||
Ok(found)
|
||||
}
|
||||
|
||||
struct Report {
|
||||
store: PathBuf,
|
||||
named: Cell<bool>,
|
||||
}
|
||||
|
||||
impl Report {
|
||||
fn new(repo: &Repo) -> Self {
|
||||
Self {
|
||||
store: repo.store.clone(),
|
||||
named: Cell::new(false),
|
||||
}
|
||||
}
|
||||
|
||||
fn line(&self, verb: &str, path: &Path) {
|
||||
// worth naming once per run
|
||||
if !self.named.replace(true) {
|
||||
println!("store: {}", self.store.display());
|
||||
}
|
||||
|
||||
println!("\t{:<11}{}", format!("{verb}:"), path.display());
|
||||
}
|
||||
}
|
||||
|
||||
// list untracked paths not in the store, i.e. what a sandbox can still read
|
||||
fn link_one(ctx: &Ctx, repo: &Repo, path: &Path, force: bool, report: &Report) -> Result<()> {
|
||||
let src = resolve(path)?;
|
||||
let rel = repo.relative(&src)?;
|
||||
let target = repo.store.join(&rel);
|
||||
|
||||
// a target inside the repo would be readable from the sandbox anyway
|
||||
if target.starts_with(&repo.root) {
|
||||
return Err(anyhow!(
|
||||
"target {} is inside the repository; point AHAB_LINK_ROOT elsewhere",
|
||||
target.display()
|
||||
));
|
||||
}
|
||||
|
||||
if tracked(ctx, repo, &rel)? {
|
||||
return Err(anyhow!(
|
||||
"{} is tracked by git; only untracked or ignored paths can be externalized",
|
||||
rel.display()
|
||||
));
|
||||
}
|
||||
if !ignored(ctx, repo, &rel) {
|
||||
warning!("{} is not gitignored", rel.display());
|
||||
}
|
||||
|
||||
let src_meta = symlink_metadata_opt(&src)?;
|
||||
let target_taken = symlink_metadata_opt(&target)?.is_some();
|
||||
|
||||
match src_meta {
|
||||
Some(meta) if meta.is_symlink() => {
|
||||
let dest = read_link(&src)?;
|
||||
|
||||
if dest == target {
|
||||
if !target_taken {
|
||||
return Err(anyhow!(
|
||||
"{} already points at {}, but nothing is there",
|
||||
rel.display(),
|
||||
target.display()
|
||||
));
|
||||
}
|
||||
report.line("unchanged", &rel);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// nothing in the store to adopt, so the symlink itself moves out
|
||||
if !target_taken {
|
||||
if !src.exists() {
|
||||
warning!(
|
||||
"{} is a broken symlink to {}",
|
||||
rel.display(),
|
||||
dest.display()
|
||||
);
|
||||
}
|
||||
move_path(ctx, &src, &target)?;
|
||||
place_link(ctx, &src, &target)?;
|
||||
report.line("moved", &rel);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if !force {
|
||||
return Err(needs_force(&target));
|
||||
}
|
||||
|
||||
place_link(ctx, &src, &target)?;
|
||||
report.line("repointed", &rel);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Some(_) if target_taken => {
|
||||
if !force {
|
||||
return Err(needs_force(&target));
|
||||
}
|
||||
|
||||
let backup = suffixed(&src, BACKUP_SUFFIX);
|
||||
if symlink_metadata_opt(&backup)?.is_some() {
|
||||
return Err(anyhow!(
|
||||
"{} already exists; remove it before re-linking",
|
||||
backup.display()
|
||||
));
|
||||
}
|
||||
|
||||
rename(ctx, &src, &backup)?;
|
||||
report.line("saved", &suffixed(&rel, BACKUP_SUFFIX));
|
||||
|
||||
place_link(ctx, &src, &target)?;
|
||||
report.line("linked", &rel);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Some(_) => {
|
||||
move_path(ctx, &src, &target)?;
|
||||
place_link(ctx, &src, &target)?;
|
||||
report.line("moved", &rel);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
None if target_taken => {
|
||||
if !force {
|
||||
return Err(needs_force(&target));
|
||||
}
|
||||
place_link(ctx, &src, &target)?;
|
||||
report.line("linked", &rel);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
None => Err(anyhow!(
|
||||
"{} does not exist and the store has no {}",
|
||||
rel.display(),
|
||||
target.display()
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn needs_force(target: &Path) -> anyhow::Error {
|
||||
anyhow!(
|
||||
"{} already exists; pass --force to link to it",
|
||||
target.display()
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user