feat: ADR-0006 §8 step 6 — .snapshots/ gitignore + export + cleanup
The undo ring is local working state, handled at all three project-file seams (R13): - .gitignore template ignores /.snapshots/ - export excludes .snapshots/ (like playground.db / history.log) - safely_delete_temp_project allowlists .snapshots/ so a temp that was modified then undone back to empty stays auto-deletable - undo::SNAPSHOTS_DIR is now a pub const referenced by all three - tests: gitignore content, export exclusion, cleanup allowlist 1693 passed / 0 failed / 1 ignored; clippy clean.
This commit is contained in:
@@ -43,6 +43,9 @@ const EXPORT_EXCLUDED_NAMES: &[&str] = &[
|
|||||||
PLAYGROUND_DB,
|
PLAYGROUND_DB,
|
||||||
HISTORY_LOG,
|
HISTORY_LOG,
|
||||||
".rdbms-playground.lock",
|
".rdbms-playground.lock",
|
||||||
|
// Undo snapshot ring (ADR-0006 Amendment 1): local working
|
||||||
|
// state, never shared.
|
||||||
|
crate::undo::SNAPSHOTS_DIR,
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Maximum auto-suffix attempts when resolving a colliding
|
/// Maximum auto-suffix attempts when resolving a colliding
|
||||||
@@ -306,6 +309,7 @@ fn add_directory_recursive(
|
|||||||
/// - `playground.db` (always derived; ADR-0007 / ADR-0015 §11)
|
/// - `playground.db` (always derived; ADR-0007 / ADR-0015 §11)
|
||||||
/// - `history.log` (ADR-0007 amendment 1: user-private)
|
/// - `history.log` (ADR-0007 amendment 1: user-private)
|
||||||
/// - `.rdbms-playground.lock` (per-process)
|
/// - `.rdbms-playground.lock` (per-process)
|
||||||
|
/// - `.snapshots/` (undo ring; local working state, ADR-0006 Am. 1)
|
||||||
/// - `*.tmp` (atomic-write staging files)
|
/// - `*.tmp` (atomic-write staging files)
|
||||||
/// - `project.yaml.v*.bak` (migration backups; recipient
|
/// - `project.yaml.v*.bak` (migration backups; recipient
|
||||||
/// doesn't need our local recovery aids)
|
/// doesn't need our local recovery aids)
|
||||||
@@ -524,6 +528,18 @@ mod tests {
|
|||||||
fs::write(p.join(GITIGNORE), "/playground.db\n").unwrap();
|
fs::write(p.join(GITIGNORE), "/playground.db\n").unwrap();
|
||||||
// Stray atomic-write staging file — must be excluded.
|
// Stray atomic-write staging file — must be excluded.
|
||||||
fs::write(p.join("project.yaml.tmp"), "stale").unwrap();
|
fs::write(p.join("project.yaml.tmp"), "stale").unwrap();
|
||||||
|
// Undo snapshot ring — local working state, must be excluded.
|
||||||
|
fs::create_dir_all(p.join(crate::undo::SNAPSHOTS_DIR).join("0")).unwrap();
|
||||||
|
fs::write(
|
||||||
|
p.join(crate::undo::SNAPSHOTS_DIR).join("index.yaml"),
|
||||||
|
"next_id: 1\nundo: []\nredo: []\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
fs::write(
|
||||||
|
p.join(crate::undo::SNAPSHOTS_DIR).join("0").join(PLAYGROUND_DB),
|
||||||
|
[0u8; 16],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
p
|
p
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -548,6 +564,11 @@ mod tests {
|
|||||||
assert!(!names.iter().any(|n| n.contains("history.log")));
|
assert!(!names.iter().any(|n| n.contains("history.log")));
|
||||||
assert!(!names.iter().any(|n| n.contains(".lock")));
|
assert!(!names.iter().any(|n| n.contains(".lock")));
|
||||||
assert!(!names.iter().any(|n| n.ends_with(".tmp")));
|
assert!(!names.iter().any(|n| n.ends_with(".tmp")));
|
||||||
|
// Undo snapshot ring is local working state — never shared.
|
||||||
|
assert!(
|
||||||
|
!names.iter().any(|n| n.contains(".snapshots")),
|
||||||
|
"snapshots leaked into export: {names:?}"
|
||||||
|
);
|
||||||
// .gitignore IS included (sensible default for the recipient).
|
// .gitignore IS included (sensible default for the recipient).
|
||||||
assert!(names.iter().any(|n| n == "MyProject/.gitignore"));
|
assert!(names.iter().any(|n| n == "MyProject/.gitignore"));
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-3
@@ -416,10 +416,12 @@ impl Project {
|
|||||||
write_if_missing(&path.join(PROJECT_YAML), &yaml)?;
|
write_if_missing(&path.join(PROJECT_YAML), &yaml)?;
|
||||||
|
|
||||||
// .gitignore template (ADR-0015 §11). Excludes the
|
// .gitignore template (ADR-0015 §11). Excludes the
|
||||||
// derived `.db`, the per-process lock, and migration
|
// derived `.db`, the per-process lock, migration backups,
|
||||||
// backups. `history.log` is intentionally NOT ignored
|
// and the undo snapshot ring (ADR-0006 Amendment 1 — local
|
||||||
|
// working state). `history.log` is intentionally NOT ignored
|
||||||
// (ADR-0007 amendment 1: per-user choice).
|
// (ADR-0007 amendment 1: per-user choice).
|
||||||
let gitignore = "/playground.db\n/.rdbms-playground.lock\n/project.yaml.v*.bak\n";
|
let gitignore =
|
||||||
|
"/playground.db\n/.rdbms-playground.lock\n/project.yaml.v*.bak\n/.snapshots/\n";
|
||||||
write_if_missing(&path.join(GITIGNORE), gitignore)?;
|
write_if_missing(&path.join(GITIGNORE), gitignore)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -502,6 +504,10 @@ const ALLOWED_PROJECT_ENTRIES: &[&str] = &[
|
|||||||
PLAYGROUND_DB,
|
PLAYGROUND_DB,
|
||||||
GITIGNORE,
|
GITIGNORE,
|
||||||
".rdbms-playground.lock",
|
".rdbms-playground.lock",
|
||||||
|
// Undo snapshot ring (ADR-0006 Amendment 1): a temp that was
|
||||||
|
// modified then undone back to empty can still carry this, and
|
||||||
|
// must remain auto-deletable.
|
||||||
|
crate::undo::SNAPSHOTS_DIR,
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Reasons `safely_delete_temp_project` refuses to delete a
|
/// Reasons `safely_delete_temp_project` refuses to delete a
|
||||||
@@ -841,6 +847,7 @@ mod tests {
|
|||||||
let gi = fs::read_to_string(path.join(GITIGNORE)).unwrap();
|
let gi = fs::read_to_string(path.join(GITIGNORE)).unwrap();
|
||||||
assert!(gi.contains("/playground.db"));
|
assert!(gi.contains("/playground.db"));
|
||||||
assert!(gi.contains("/.rdbms-playground.lock"));
|
assert!(gi.contains("/.rdbms-playground.lock"));
|
||||||
|
assert!(gi.contains("/.snapshots/"), "undo ring should be ignored");
|
||||||
assert!(!gi.contains("history.log"), "history.log should NOT be ignored");
|
assert!(!gi.contains("history.log"), "history.log should NOT be ignored");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+5
-1
@@ -49,7 +49,11 @@ use crate::project::{DATA_DIR, PLAYGROUND_DB, PROJECT_YAML};
|
|||||||
pub const DEFAULT_RING_CAPACITY: usize = 50;
|
pub const DEFAULT_RING_CAPACITY: usize = 50;
|
||||||
|
|
||||||
/// Directory under the project that holds the ring.
|
/// Directory under the project that holds the ring.
|
||||||
const SNAPSHOTS_DIR: &str = ".snapshots";
|
///
|
||||||
|
/// Public so the `.gitignore` template, the export exclusion, and
|
||||||
|
/// the temp-project cleanup allowlist all reference the one
|
||||||
|
/// canonical name (ADR-0006 Amendment 1).
|
||||||
|
pub const SNAPSHOTS_DIR: &str = ".snapshots";
|
||||||
/// In-flight snapshot directory (one at a time).
|
/// In-flight snapshot directory (one at a time).
|
||||||
const STAGING_DIR: &str = ".staging";
|
const STAGING_DIR: &str = ".staging";
|
||||||
/// The ring index file.
|
/// The ring index file.
|
||||||
|
|||||||
@@ -494,6 +494,24 @@ fn safely_delete_allows_migration_backups_and_tmp_files() {
|
|||||||
assert!(!path.exists());
|
assert!(!path.exists());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn safely_delete_allows_undo_snapshot_ring() {
|
||||||
|
// A temp that was modified then undone back to empty can still
|
||||||
|
// carry the `.snapshots/` ring; it must remain auto-deletable
|
||||||
|
// (ADR-0006 Amendment 1).
|
||||||
|
let data = tempdir();
|
||||||
|
let project = project::open_or_create(None, Some(data.path())).unwrap();
|
||||||
|
let path = project.path().to_path_buf();
|
||||||
|
let snaps = path.join(".snapshots");
|
||||||
|
fs::create_dir_all(snaps.join("3")).unwrap();
|
||||||
|
fs::write(snaps.join("index.yaml"), "next_id: 4\nundo: []\nredo: []\n").unwrap();
|
||||||
|
fs::write(snaps.join("3").join("playground.db"), [0u8; 16]).unwrap();
|
||||||
|
drop(project);
|
||||||
|
|
||||||
|
safely_delete_temp_project(&path, data.path()).expect("should delete");
|
||||||
|
assert!(!path.exists());
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
#[test]
|
#[test]
|
||||||
fn safely_delete_refuses_symlink_top_level() {
|
fn safely_delete_refuses_symlink_top_level() {
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ fn no_args_creates_temp_project_under_data_root() {
|
|||||||
// .gitignore must NOT include history.log (ADR-0007 amendment).
|
// .gitignore must NOT include history.log (ADR-0007 amendment).
|
||||||
let gi = fs::read_to_string(path.join(GITIGNORE)).unwrap();
|
let gi = fs::read_to_string(path.join(GITIGNORE)).unwrap();
|
||||||
assert!(!gi.contains("history.log"));
|
assert!(!gi.contains("history.log"));
|
||||||
|
// …but it must ignore the undo ring (ADR-0006 Amendment 1).
|
||||||
|
assert!(gi.contains("/.snapshots/"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user