fix: follow a link anywhere under the store base
This commit is contained in:
@@ -9,7 +9,7 @@ use std::path::{Path, PathBuf};
|
|||||||
|
|
||||||
use anyhow::{Context, Result, anyhow, bail};
|
use anyhow::{Context, Result, anyhow, bail};
|
||||||
|
|
||||||
use self::store::{Leads, Repo, ignored, leads, resolve, symlink_metadata_opt, tracked};
|
use self::store::{Leads, Repo, held, ignored, leads, resolve, symlink_metadata_opt, tracked};
|
||||||
use crate::cli::link as cli;
|
use crate::cli::link as cli;
|
||||||
use crate::ctx::Ctx;
|
use crate::ctx::Ctx;
|
||||||
use crate::fsops::suffixed;
|
use crate::fsops::suffixed;
|
||||||
@@ -104,7 +104,6 @@ pub fn restore(ctx: &Ctx, args: &cli::Restore) -> Result<()> {
|
|||||||
fn restore_one(ctx: &Ctx, repo: &Repo, path: &Path, report: &Report) -> Result<()> {
|
fn restore_one(ctx: &Ctx, repo: &Repo, path: &Path, report: &Report) -> Result<()> {
|
||||||
let src = resolve(path)?;
|
let src = resolve(path)?;
|
||||||
let rel = repo.relative(&src)?;
|
let rel = repo.relative(&src)?;
|
||||||
let stored = repo.store.join(&rel);
|
|
||||||
|
|
||||||
let Some(meta) = symlink_metadata_opt(&src)? else {
|
let Some(meta) = symlink_metadata_opt(&src)? else {
|
||||||
bail!("{} does not exist", rel.display());
|
bail!("{} does not exist", rel.display());
|
||||||
@@ -116,14 +115,16 @@ fn restore_one(ctx: &Ctx, repo: &Repo, path: &Path, report: &Report) -> Result<(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// followed to wherever under the base it points, so a checkout that moved
|
||||||
|
// or a store laid out by an older ahab can still take its files back
|
||||||
let dest = read_link(&src)?;
|
let dest = read_link(&src)?;
|
||||||
if dest != stored {
|
let Some(stored) = held(repo, &src, &dest) else {
|
||||||
bail!(
|
bail!(
|
||||||
"{} points at {}, which is not where the store keeps it",
|
"{} points at {}, which is not in the store",
|
||||||
rel.display(),
|
rel.display(),
|
||||||
dest.display()
|
dest.display()
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
if symlink_metadata_opt(&stored)?.is_none() {
|
if symlink_metadata_opt(&stored)?.is_none() {
|
||||||
bail!("{} is missing from the store", rel.display());
|
bail!("{} is missing from the store", rel.display());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use std::path::{Path, PathBuf};
|
|||||||
|
|
||||||
use anyhow::{Result, anyhow};
|
use anyhow::{Result, anyhow};
|
||||||
|
|
||||||
use super::store::{Leads, Repo, leads, resolve, symlink_metadata_opt};
|
use super::store::{Leads, Repo, held, leads, resolve, symlink_metadata_opt};
|
||||||
use crate::cli::link as cli;
|
use crate::cli::link as cli;
|
||||||
use crate::cmd::{Cmd, LsFiles};
|
use crate::cmd::{Cmd, LsFiles};
|
||||||
use crate::ctx::Ctx;
|
use crate::ctx::Ctx;
|
||||||
@@ -179,10 +179,10 @@ fn classify(repo: &Repo, rel: &Path, mark: char) -> Result<Option<Exposed>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let dest = read_link(&src)?;
|
let dest = read_link(&src)?;
|
||||||
if dest == repo.store.join(rel) {
|
if held(repo, &src, &dest).is_some() {
|
||||||
// it names the store, but what the store holds there can be a symlink of
|
// it names the store, but what the store holds there can be a symlink of
|
||||||
// its own leading straight back out, which is not being held at all
|
// its own leading straight back out, which is not being held at all
|
||||||
return Ok(match leads(&src, &repo.store) {
|
return Ok(match leads(&src, &repo.base) {
|
||||||
Leads::Inside | Leads::Dangling => None,
|
Leads::Inside | Leads::Dangling => None,
|
||||||
Leads::Outside(end) => Some(Exposed {
|
Leads::Outside(end) => Some(Exposed {
|
||||||
mark,
|
mark,
|
||||||
|
|||||||
@@ -289,6 +289,19 @@ pub(super) fn symlink_metadata_opt(path: &Path) -> Result<Option<std::fs::Metada
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// where a link into the store points, if it is one: absolute, under the base.
|
||||||
|
// the directory need not be this checkout's own, since a moved checkout or an
|
||||||
|
// older layout still holds the file
|
||||||
|
pub(super) fn held(repo: &Repo, link: &Path, dest: &Path) -> Option<PathBuf> {
|
||||||
|
let dest = match dest.is_absolute() {
|
||||||
|
true => dest.to_path_buf(),
|
||||||
|
false => link.parent()?.join(dest),
|
||||||
|
};
|
||||||
|
let dest = normalized(dest).ok()?;
|
||||||
|
|
||||||
|
dest.starts_with(&repo.base).then_some(dest)
|
||||||
|
}
|
||||||
|
|
||||||
// where a chain of symlinks actually ends up
|
// where a chain of symlinks actually ends up
|
||||||
pub(super) enum Leads {
|
pub(super) enum Leads {
|
||||||
// somewhere under the directory it was supposed to stay in
|
// somewhere under the directory it was supposed to stay in
|
||||||
|
|||||||
@@ -282,3 +282,27 @@ fn two_checkouts_of_one_remote_keep_their_own_files() {
|
|||||||
assert_eq!(fs::read(second.path(".env")).unwrap(), b"SECOND\n");
|
assert_eq!(fs::read(second.path(".env")).unwrap(), b"SECOND\n");
|
||||||
second.ahab(&["link", "list"]).ok().says("linked: .env");
|
second.ahab(&["link", "list"]).ok().says("linked: .env");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_link_into_an_older_store_layout_still_checks_clean_and_restores() {
|
||||||
|
let case = Case::new("legacy");
|
||||||
|
// the flat layout, before the store was keyed by checkout
|
||||||
|
let old = case.store.parent().unwrap().join(".env");
|
||||||
|
fs::create_dir_all(old.parent().unwrap()).unwrap();
|
||||||
|
fs::write(&old, b"SECRET=1\n").unwrap();
|
||||||
|
case.link(&case.path(".env"), &old);
|
||||||
|
case.gitignore(".env\n");
|
||||||
|
|
||||||
|
case.ahab(&["link", "check", "--exit-code"]).ok();
|
||||||
|
case.ahab(&["link", "restore", ".env"]).ok();
|
||||||
|
|
||||||
|
assert!(!case.is_symlink(".env"));
|
||||||
|
assert_eq!(fs::read(case.path(".env")).unwrap(), b"SECRET=1\n");
|
||||||
|
assert!(!old.exists());
|
||||||
|
|
||||||
|
// a link that leaves the store altogether is still refused
|
||||||
|
case.link(&case.path("key"), &case.outside.join("key"));
|
||||||
|
case.ahab(&["link", "restore", "key"])
|
||||||
|
.failed()
|
||||||
|
.says("not in the store");
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user