feat: migrate out of the flat store layout
This commit is contained in:
@@ -19,6 +19,15 @@ pub enum Link {
|
|||||||
|
|
||||||
/// List untracked paths a sandbox would still see
|
/// List untracked paths a sandbox would still see
|
||||||
Check(Check),
|
Check(Check),
|
||||||
|
|
||||||
|
/// Move this checkout's paths from the old store layout into its own directory
|
||||||
|
Migrate(Migrate),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Args, Debug)]
|
||||||
|
pub struct Migrate {
|
||||||
|
#[command(flatten)]
|
||||||
|
pub store: Store,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Args, Debug)]
|
#[derive(Args, Debug)]
|
||||||
|
|||||||
@@ -170,6 +170,12 @@ enum Stored {
|
|||||||
// 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 put there without asking git anything
|
// repository has put there without asking git anything
|
||||||
fn stored_paths(repo: &Repo, dir: &Path) -> Result<Vec<(PathBuf, Stored)>> {
|
fn stored_paths(repo: &Repo, dir: &Path) -> Result<Vec<(PathBuf, Stored)>> {
|
||||||
|
mirrored_paths(repo, &repo.store, dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
// the paths under `dir` as the repository would link them, with `mirror` as
|
||||||
|
// the directory that stands for the repository root
|
||||||
|
fn mirrored_paths(repo: &Repo, mirror: &Path, 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) {
|
||||||
@@ -180,11 +186,11 @@ fn stored_paths(repo: &Repo, dir: &Path) -> Result<Vec<(PathBuf, Stored)>> {
|
|||||||
|
|
||||||
for entry in entries {
|
for entry in entries {
|
||||||
let stored = entry?.path();
|
let stored = entry?.path();
|
||||||
let rel = stored.strip_prefix(&repo.store).with_context(|| {
|
let rel = stored.strip_prefix(mirror).with_context(|| {
|
||||||
format!(
|
format!(
|
||||||
"{} is not under the store {}",
|
"{} is not under the store {}",
|
||||||
stored.display(),
|
stored.display(),
|
||||||
repo.store.display()
|
mirror.display()
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
let src = repo.root.join(rel);
|
let src = repo.root.join(rel);
|
||||||
@@ -204,7 +210,7 @@ fn stored_paths(repo: &Repo, dir: &Path) -> Result<Vec<(PathBuf, Stored)>> {
|
|||||||
if state == Stored::Linked || !holds_dir {
|
if state == Stored::Linked || !holds_dir {
|
||||||
found.push((src, state));
|
found.push((src, state));
|
||||||
} else {
|
} else {
|
||||||
found.extend(stored_paths(repo, &stored)?);
|
found.extend(mirrored_paths(repo, mirror, &stored)?);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,8 +218,29 @@ fn stored_paths(repo: &Repo, dir: &Path) -> Result<Vec<(PathBuf, Stored)>> {
|
|||||||
Ok(found)
|
Ok(found)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// what this repository links into the flat layout above its own directory,
|
||||||
|
// from before the store was keyed by checkout
|
||||||
|
fn legacy_paths(repo: &Repo) -> Result<Vec<PathBuf>> {
|
||||||
|
let Some(legacy) = repo.store.parent() else {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(mirrored_paths(repo, legacy, legacy)?
|
||||||
|
.into_iter()
|
||||||
|
.filter(|(_, state)| *state == Stored::Linked)
|
||||||
|
.map(|(src, _)| src)
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Summary {
|
||||||
|
pub store: PathBuf,
|
||||||
|
pub linked: usize,
|
||||||
|
pub other: usize,
|
||||||
|
pub legacy: usize,
|
||||||
|
}
|
||||||
|
|
||||||
// what the store holds, without the git questions check asks
|
// what the store holds, without the git questions check asks
|
||||||
pub fn stored_summary(ctx: &Ctx, store: Option<&Path>) -> Result<(PathBuf, usize, usize)> {
|
pub fn stored_summary(ctx: &Ctx, store: Option<&Path>) -> Result<Summary> {
|
||||||
let repo = Repo::discover(ctx, store)?;
|
let repo = Repo::discover(ctx, store)?;
|
||||||
let stored = stored_paths(&repo, &repo.store)?;
|
let stored = stored_paths(&repo, &repo.store)?;
|
||||||
|
|
||||||
@@ -222,17 +249,27 @@ pub fn stored_summary(ctx: &Ctx, store: Option<&Path>) -> Result<(PathBuf, usize
|
|||||||
.filter(|(_, state)| *state == Stored::Linked)
|
.filter(|(_, state)| *state == Stored::Linked)
|
||||||
.count();
|
.count();
|
||||||
|
|
||||||
Ok((repo.store, linked, stored.len() - linked))
|
let legacy = legacy_paths(&repo)?.len();
|
||||||
|
|
||||||
|
Ok(Summary {
|
||||||
|
store: repo.store,
|
||||||
|
linked,
|
||||||
|
other: stored.len() - linked,
|
||||||
|
legacy,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const MIGRATE_HINT: &str = "run `ahab link migrate` to move them into it";
|
||||||
|
|
||||||
// list what the store holds for this repository, the inverse of check
|
// list what the store holds for this repository, the inverse of check
|
||||||
pub fn list(ctx: &Ctx, args: &cli::List) -> Result<()> {
|
pub fn list(ctx: &Ctx, args: &cli::List) -> Result<()> {
|
||||||
let repo = Repo::discover(ctx, args.store.root())?;
|
let repo = Repo::discover(ctx, args.store.root())?;
|
||||||
let stored = stored_paths(&repo, &repo.store)?;
|
let stored = stored_paths(&repo, &repo.store)?;
|
||||||
|
let legacy = legacy_paths(&repo)?;
|
||||||
|
|
||||||
line!("store: {}", repo.store.display());
|
line!("store: {}", repo.store.display());
|
||||||
|
|
||||||
if stored.is_empty() {
|
if stored.is_empty() && legacy.is_empty() {
|
||||||
line!("nothing in the store for this repository");
|
line!("nothing in the store for this repository");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@@ -248,6 +285,67 @@ pub fn list(ctx: &Ctx, args: &cli::List) -> Result<()> {
|
|||||||
entry(verb, rel);
|
entry(verb, rel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for path in &legacy {
|
||||||
|
entry("legacy", path.strip_prefix(&repo.root).unwrap_or(path));
|
||||||
|
}
|
||||||
|
if !legacy.is_empty() {
|
||||||
|
line!(
|
||||||
|
"{} path{} in the old layout, {MIGRATE_HINT}",
|
||||||
|
legacy.len(),
|
||||||
|
plural(legacy.len())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// move this checkout's paths from the old layout into its own directory
|
||||||
|
pub fn migrate(ctx: &Ctx, args: &cli::Migrate) -> Result<()> {
|
||||||
|
let repo = Repo::discover(ctx, args.store.root())?;
|
||||||
|
let report = Report::new(&repo);
|
||||||
|
let legacy = legacy_paths(&repo)?;
|
||||||
|
|
||||||
|
if legacy.is_empty() {
|
||||||
|
note!(ctx, "nothing in the old layout for this repository");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
each(&legacy, |src| migrate_one(ctx, &repo, src, &report))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn migrate_one(ctx: &Ctx, repo: &Repo, src: &Path, report: &Report) -> Result<()> {
|
||||||
|
let rel = repo.relative(src)?;
|
||||||
|
let old = read_link(src)?;
|
||||||
|
let target = repo.store.join(&rel);
|
||||||
|
|
||||||
|
if symlink_metadata_opt(&target)?.is_some() {
|
||||||
|
bail!(
|
||||||
|
"{} already exists; restore {} and add it again instead",
|
||||||
|
target.display(),
|
||||||
|
rel.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
stays_in_store(repo, &rel)?;
|
||||||
|
ctx.fs().ensure_private_parent(&repo.base, &target)?;
|
||||||
|
ctx.fs().move_path(&old, &target)?;
|
||||||
|
|
||||||
|
// the link still names the old place until it is repointed, so a failure
|
||||||
|
// here puts the payload back where it points
|
||||||
|
if let Err(e) = ctx.fs().place_link(src, &target) {
|
||||||
|
ctx.fs().move_path(&target, &old).with_context(|| {
|
||||||
|
format!(
|
||||||
|
"could not put {} back after failing to link {} to it",
|
||||||
|
old.display(),
|
||||||
|
rel.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.fs().prune_empty(old.parent(), &repo.base);
|
||||||
|
report.line("migrated", &rel);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,12 +32,15 @@ pub fn status(ctx: &Ctx, store: Option<&Path>) -> Result<()> {
|
|||||||
fn stored(ctx: &Ctx, store: Option<&Path>) {
|
fn stored(ctx: &Ctx, store: Option<&Path>) {
|
||||||
match link::stored_summary(ctx, store) {
|
match link::stored_summary(ctx, store) {
|
||||||
Err(e) => line!("store: {e:#}"),
|
Err(e) => line!("store: {e:#}"),
|
||||||
Ok((store, linked, other)) => {
|
Ok(summary) => {
|
||||||
line!("store: {}", store.display());
|
line!("store: {}", summary.store.display());
|
||||||
line!("\tlinked: {linked}");
|
line!("\tlinked: {}", summary.linked);
|
||||||
|
|
||||||
if other > 0 {
|
if summary.other > 0 {
|
||||||
line!("\tnot linked: {other} (see `ahab link list`)");
|
line!("\tnot linked: {} (see `ahab link list`)", summary.other);
|
||||||
|
}
|
||||||
|
if summary.legacy > 0 {
|
||||||
|
line!("\tlegacy: {} ({})", summary.legacy, link::MIGRATE_HINT);
|
||||||
}
|
}
|
||||||
|
|
||||||
line!("\t`ahab link check` lists what a sandbox can still read");
|
line!("\t`ahab link check` lists what a sandbox can still read");
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ fn run(ctx: &Ctx, command: cli::Commands) -> Result<ExitCode> {
|
|||||||
cli::Link::Add(args) => commands::link::add(ctx, &args).map(|()| done),
|
cli::Link::Add(args) => commands::link::add(ctx, &args).map(|()| done),
|
||||||
cli::Link::List(args) => commands::link::list(ctx, &args).map(|()| done),
|
cli::Link::List(args) => commands::link::list(ctx, &args).map(|()| done),
|
||||||
cli::Link::Restore(args) => commands::link::restore(ctx, &args).map(|()| done),
|
cli::Link::Restore(args) => commands::link::restore(ctx, &args).map(|()| done),
|
||||||
|
cli::Link::Migrate(args) => commands::link::migrate(ctx, &args).map(|()| done),
|
||||||
// the one command with something to say through its exit code
|
// the one command with something to say through its exit code
|
||||||
cli::Link::Check(args) => match commands::link::check(ctx, &args)? && args.exit_code {
|
cli::Link::Check(args) => match commands::link::check(ctx, &args)? && args.exit_code {
|
||||||
true => Ok(ExitCode::from(FINDINGS)),
|
true => Ok(ExitCode::from(FINDINGS)),
|
||||||
|
|||||||
@@ -306,3 +306,44 @@ fn a_link_into_an_older_store_layout_still_checks_clean_and_restores() {
|
|||||||
.failed()
|
.failed()
|
||||||
.says("not in the store");
|
.says("not in the store");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn migrate_moves_only_this_checkouts_paths_out_of_the_old_layout() {
|
||||||
|
let case = Case::new("migrate");
|
||||||
|
let old = case.store.parent().unwrap().to_path_buf();
|
||||||
|
fs::create_dir_all(old.join("secrets")).unwrap();
|
||||||
|
fs::write(old.join(".env"), b"SECRET=1\n").unwrap();
|
||||||
|
fs::write(old.join("secrets/token"), b"tok\n").unwrap();
|
||||||
|
// another checkout's file in the same flat layout, linked from nowhere here
|
||||||
|
fs::write(old.join("other.env"), b"OTHER\n").unwrap();
|
||||||
|
case.link(&case.path(".env"), &old.join(".env"));
|
||||||
|
case.link(&case.path("secrets"), &old.join("secrets"));
|
||||||
|
case.gitignore(".env\nsecrets/\n");
|
||||||
|
|
||||||
|
case.ahab(&["link", "list"])
|
||||||
|
.ok()
|
||||||
|
.says("legacy: .env")
|
||||||
|
.says("legacy: secrets")
|
||||||
|
.says("ahab link migrate");
|
||||||
|
case.ahab(&["status"]).says("legacy: 2");
|
||||||
|
|
||||||
|
case.ahab(&["link", "migrate"]).ok().says("migrated: .env");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
fs::read_link(case.path(".env")).unwrap(),
|
||||||
|
case.store.join(".env")
|
||||||
|
);
|
||||||
|
assert_eq!(fs::read(case.path(".env")).unwrap(), b"SECRET=1\n");
|
||||||
|
assert_eq!(fs::read(case.path("secrets/token")).unwrap(), b"tok\n");
|
||||||
|
assert!(!old.join(".env").exists());
|
||||||
|
assert!(!old.join("secrets").exists());
|
||||||
|
assert_eq!(fs::read(old.join("other.env")).unwrap(), b"OTHER\n");
|
||||||
|
|
||||||
|
case.ahab(&["link", "list"])
|
||||||
|
.ok()
|
||||||
|
.says("linked: .env")
|
||||||
|
.silent_about("legacy");
|
||||||
|
case.ahab(&["link", "migrate"])
|
||||||
|
.ok()
|
||||||
|
.says("nothing in the old layout");
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user