feat: list what the store holds for this repository

This commit is contained in:
2026-09-08 14:16:38 +00:00
parent 6389f3f1f5
commit 79ef5cca2d
3 changed files with 93 additions and 18 deletions

View File

@@ -44,24 +44,49 @@ pub fn restore(ctx: &Ctx, paths: &[PathBuf], all: bool, store: Option<&Path>) ->
let repo = Repo::discover(ctx, store)?;
let report = Report::new(&repo);
let paths = match (all, paths) {
(true, []) => linked_paths(&repo, &repo.store)?,
let stored = match (all, paths) {
(true, []) => stored_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(),
(false, paths) => paths.iter().cloned().map(|p| (p, Stored::Linked)).collect(),
};
if paths.is_empty() {
note!(ctx, "nothing in the store for this repository");
// only a linked path can be moved back; the store can hold orphans too
let linked: Vec<PathBuf> = stored
.iter()
.filter(|(_, state)| *state == Stored::Linked)
.map(|(path, _)| path.clone())
.collect();
let skipped = stored.len() - linked.len();
if linked.is_empty() {
match skipped {
0 => note!(ctx, "nothing in the store for this repository"),
n => note!(
ctx,
"nothing to restore: {n} path{} in the store {} not linked, see `ahab link list`",
if n == 1 { "" } else { "s" },
if n == 1 { "is" } else { "are" }
),
}
return Ok(());
}
if let [path] = paths.as_slice() {
if skipped > 0 {
note!(
ctx,
"leaving {skipped} path{} that the store holds but nothing links to",
if skipped == 1 { "" } else { "s" }
);
}
if let [path] = linked.as_slice() {
return restore_one(ctx, &repo, path, &report);
}
let mut failed = 0;
for path in &paths {
for path in &linked {
if let Err(e) = restore_one(ctx, &repo, path, &report) {
note!(ctx, "error: {e:#}");
failed += 1;
@@ -69,7 +94,7 @@ pub fn restore(ctx: &Ctx, paths: &[PathBuf], all: bool, store: Option<&Path>) ->
}
if failed > 0 {
return Err(anyhow!("{failed} of {} paths failed", paths.len()));
return Err(anyhow!("{failed} of {} paths failed", linked.len()));
}
Ok(())
}
@@ -109,9 +134,20 @@ fn restore_one(ctx: &Ctx, repo: &Repo, path: &Path, report: &Report) -> Result<(
Ok(())
}
// what the repository looks like from the store's side
#[derive(PartialEq)]
enum Stored {
// the symlink is there and points at the store, which is the whole point
Linked,
// nothing is at the path the store holds it for
Missing,
// something else took the path, so the stored copy is the one nobody reads
Taken,
}
// 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>> {
// repository has put there without asking git anything
fn stored_paths(repo: &Repo, dir: &Path) -> Result<Vec<(PathBuf, Stored)>> {
let mut found = Vec::new();
let entries = match read_dir(dir) {
@@ -127,20 +163,51 @@ fn linked_paths(repo: &Repo, dir: &Path) -> Result<Vec<PathBuf>> {
.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);
let state = match symlink_metadata_opt(&src)? {
None => Stored::Missing,
Some(meta) if !meta.is_symlink() => Stored::Taken,
Some(_) if read_link(&src).is_ok_and(|dest| dest == stored) => Stored::Linked,
Some(_) => Stored::Taken,
};
if linked {
found.push(src);
} else if stored.is_dir() {
found.extend(linked_paths(repo, &stored)?);
// a linked directory is one entry; otherwise the paths inside it are
if state == Stored::Linked || !stored.is_dir() {
found.push((src, state));
} else {
found.extend(stored_paths(repo, &stored)?);
}
}
found.sort();
found.sort_by(|(a, _), (b, _)| a.cmp(b));
Ok(found)
}
// list what the store holds for this repository, the inverse of check
pub fn list(ctx: &Ctx, store: Option<&Path>) -> Result<()> {
let repo = Repo::discover(ctx, store)?;
let stored = stored_paths(&repo, &repo.store)?;
println!("store: {}", repo.store.display());
if stored.is_empty() {
println!("nothing in the store for this repository");
return Ok(());
}
for (path, state) in stored {
let rel = path.strip_prefix(&repo.root).unwrap_or(&path);
let verb = match state {
Stored::Linked => "linked",
Stored::Missing => "missing",
Stored::Taken => "shadowed",
};
println!("\t{:<11}{}", format!("{verb}:"), rel.display());
}
Ok(())
}
struct Report {
store: PathBuf,
named: Cell<bool>,
@@ -164,7 +231,6 @@ impl Report {
}
}
// 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)?;