feat: list what the store holds for this repository
This commit is contained in:
@@ -30,6 +30,12 @@ pub enum Link {
|
|||||||
store: Store,
|
store: Store,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// List what this repository keeps in the store
|
||||||
|
List {
|
||||||
|
#[command(flatten)]
|
||||||
|
store: Store,
|
||||||
|
},
|
||||||
|
|
||||||
/// List untracked paths a sandbox would still see
|
/// List untracked paths a sandbox would still see
|
||||||
Check {
|
Check {
|
||||||
/// Limit the listing to these paths
|
/// 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 repo = Repo::discover(ctx, store)?;
|
||||||
let report = Report::new(&repo);
|
let report = Report::new(&repo);
|
||||||
|
|
||||||
let paths = match (all, paths) {
|
let stored = match (all, paths) {
|
||||||
(true, []) => linked_paths(&repo, &repo.store)?,
|
(true, []) => stored_paths(&repo, &repo.store)?,
|
||||||
(true, _) => bail!("--all restores everything, so it takes no paths"),
|
(true, _) => bail!("--all restores everything, so it takes no paths"),
|
||||||
(false, []) => bail!("name a path to restore, or pass --all"),
|
(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() {
|
// only a linked path can be moved back; the store can hold orphans too
|
||||||
note!(ctx, "nothing in the store for this repository");
|
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(());
|
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);
|
return restore_one(ctx, &repo, path, &report);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut failed = 0;
|
let mut failed = 0;
|
||||||
for path in &paths {
|
for path in &linked {
|
||||||
if let Err(e) = restore_one(ctx, &repo, path, &report) {
|
if let Err(e) = restore_one(ctx, &repo, path, &report) {
|
||||||
note!(ctx, "error: {e:#}");
|
note!(ctx, "error: {e:#}");
|
||||||
failed += 1;
|
failed += 1;
|
||||||
@@ -69,7 +94,7 @@ pub fn restore(ctx: &Ctx, paths: &[PathBuf], all: bool, store: Option<&Path>) ->
|
|||||||
}
|
}
|
||||||
|
|
||||||
if failed > 0 {
|
if failed > 0 {
|
||||||
return Err(anyhow!("{failed} of {} paths failed", paths.len()));
|
return Err(anyhow!("{failed} of {} paths failed", linked.len()));
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -109,9 +134,20 @@ fn restore_one(ctx: &Ctx, repo: &Repo, path: &Path, report: &Report) -> Result<(
|
|||||||
Ok(())
|
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
|
// the store mirrors the repository layout, so walking it finds every path this
|
||||||
// repository has linked without asking git anything
|
// repository has put there without asking git anything
|
||||||
fn linked_paths(repo: &Repo, dir: &Path) -> Result<Vec<PathBuf>> {
|
fn stored_paths(repo: &Repo, dir: &Path) -> Result<Vec<(PathBuf, Stored)>> {
|
||||||
let mut found = Vec::new();
|
let mut found = Vec::new();
|
||||||
|
|
||||||
let entries = match read_dir(dir) {
|
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");
|
.expect("walked out of the store");
|
||||||
let src = repo.root.join(rel);
|
let src = repo.root.join(rel);
|
||||||
|
|
||||||
let linked = symlink_metadata_opt(&src)?.is_some_and(|meta| meta.is_symlink())
|
let state = match symlink_metadata_opt(&src)? {
|
||||||
&& read_link(&src).is_ok_and(|dest| dest == stored);
|
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 {
|
// a linked directory is one entry; otherwise the paths inside it are
|
||||||
found.push(src);
|
if state == Stored::Linked || !stored.is_dir() {
|
||||||
} else if stored.is_dir() {
|
found.push((src, state));
|
||||||
found.extend(linked_paths(repo, &stored)?);
|
} else {
|
||||||
|
found.extend(stored_paths(repo, &stored)?);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
found.sort();
|
found.sort_by(|(a, _), (b, _)| a.cmp(b));
|
||||||
Ok(found)
|
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 {
|
struct Report {
|
||||||
store: PathBuf,
|
store: PathBuf,
|
||||||
named: Cell<bool>,
|
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<()> {
|
fn link_one(ctx: &Ctx, repo: &Repo, path: &Path, force: bool, report: &Report) -> Result<()> {
|
||||||
let src = resolve(path)?;
|
let src = resolve(path)?;
|
||||||
let rel = repo.relative(&src)?;
|
let rel = repo.relative(&src)?;
|
||||||
|
|||||||
@@ -75,6 +75,9 @@ fn run(ctx: &Ctx, command: cli::Commands) -> Result<ExitCode> {
|
|||||||
force,
|
force,
|
||||||
store,
|
store,
|
||||||
} => commands::link::add(ctx, &paths, force, store.root.as_deref()).map(|()| done),
|
} => 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 } => {
|
cli::Link::Restore { paths, all, store } => {
|
||||||
commands::link::restore(ctx, &paths, all, store.root.as_deref()).map(|()| done)
|
commands::link::restore(ctx, &paths, all, store.root.as_deref()).map(|()| done)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user