feat: list what the store holds for this repository
This commit is contained in:
@@ -30,6 +30,12 @@ pub enum Link {
|
||||
store: Store,
|
||||
},
|
||||
|
||||
/// List what this repository keeps in the store
|
||||
List {
|
||||
#[command(flatten)]
|
||||
store: Store,
|
||||
},
|
||||
|
||||
/// List untracked paths a sandbox would still see
|
||||
Check {
|
||||
/// Limit the listing to these paths
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
@@ -75,6 +75,9 @@ fn run(ctx: &Ctx, command: cli::Commands) -> Result<ExitCode> {
|
||||
force,
|
||||
store,
|
||||
} => commands::link::add(ctx, &paths, force, store.root.as_deref()).map(|()| done),
|
||||
cli::Link::List { store } => {
|
||||
commands::link::list(ctx, store.root.as_deref()).map(|()| done)
|
||||
}
|
||||
cli::Link::Restore { paths, all, store } => {
|
||||
commands::link::restore(ctx, &paths, all, store.root.as_deref()).map(|()| done)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user