Uniquify automatic wrapper snapshot names

This commit is contained in:
mdaleo404 committed 2026-10-10 08:07:19 +01:00
1 parent 3d3e9dae13
commit a317582b72
4 files changed
+83 -5

No files matched your search

+3 -2
View File
@@ -316,7 +316,7 @@ The wrapper snapshot flow is:
wrapper_cmd wrapper_cmd
-> _extract_paths_from_command() -> _extract_paths_from_command()
-> _common_snapshot_root() -> _common_snapshot_root()
-> create auto-YYYYMMDD-HHMMSS state -> create a unique auto-YYYYMMDD-HHMMSS[-N] state
-> _iter_entries_for_target() for each path -> _iter_entries_for_target() for each path
-> insert each relative path once -> insert each relative path once
-> run subprocess.run(wrapper_cmd) -> run subprocess.run(wrapper_cmd)
@@ -798,7 +798,8 @@ The list of `PlannedChange` objects produced before any mutation occurs.
The `chguard -- chmod|chown|chgrp ...` mode that saves an automatic pre-command snapshot before running the command. The `chguard -- chmod|chown|chgrp ...` mode that saves an automatic pre-command snapshot before running the command.
**Auto snapshot** **Auto snapshot**
A wrapper-created state named like `auto-YYYYMMDD-HHMMSS`. A wrapper-created state named like `auto-YYYYMMDD-HHMMSS`, with a numeric
suffix when needed to keep same-second snapshots unique.
**Owner restore** **Owner restore**
Restoring numeric uid/gid with `os.chown(..., follow_symlinks=False)`. Restoring numeric uid/gid with `os.chown(..., follow_symlinks=False)`.
+2
View File
@@ -57,6 +57,8 @@ auto-20251230-161301
``` ```
Auto-generated snapshots are visually distinguished in the output so they are easy to identify. Auto-generated snapshots are visually distinguished in the output so they are easy to identify.
Snapshots created during the same second use a numeric suffix to remain unique,
for example `auto-20251230-161301-1`.
### Scope control ### Scope control
Restore: Restore:
+25 -3
View File
@@ -6,6 +6,7 @@ import grp
import importlib.metadata import importlib.metadata
import os import os
import pwd import pwd
import sqlite3
import stat import stat
import subprocess import subprocess
import sys import sys
@@ -166,6 +167,28 @@ def _common_snapshot_root(paths: list[Path]) -> Path:
return Path(os.path.commonpath([str(p.resolve()) for p in paths])) return Path(os.path.commonpath([str(p.resolve()) for p in paths]))
def _create_auto_state(
conn, root_path: Path, created_by_uid: int
) -> tuple[int, str]:
base_name = f"auto-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
suffix = 0
while True:
auto_name = base_name if suffix == 0 else f"{base_name}-{suffix}"
try:
state_id = create_state(
conn,
auto_name,
str(root_path),
created_by_uid,
commit=False,
)
except sqlite3.IntegrityError:
suffix += 1
else:
return state_id, auto_name
def _type_for_mode(mode: int) -> str | None: def _type_for_mode(mode: int) -> str | None:
if stat.S_ISDIR(mode): if stat.S_ISDIR(mode):
return "dir" return "dir"
@@ -451,12 +474,11 @@ def main() -> None:
paths = _extract_paths_from_command(wrapper_cmd) paths = _extract_paths_from_command(wrapper_cmd)
if paths: if paths:
auto_name = f"auto-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
root_path = _common_snapshot_root(paths) root_path = _common_snapshot_root(paths)
with conn: with conn:
state_id = create_state( state_id, auto_name = _create_auto_state(
conn, auto_name, str(root_path), os.getuid(), commit=False conn, root_path, os.getuid()
) )
seen_paths: set[str] = set() seen_paths: set[str] = set()
+53
View File
@@ -2,9 +2,13 @@ from __future__ import annotations
import os import os
import sys import sys
from datetime import datetime
from pathlib import Path from pathlib import Path
from types import SimpleNamespace from types import SimpleNamespace
import pytest
import chguard.cli as cli
from chguard.cli import _restore_preview_rows, main from chguard.cli import _restore_preview_rows, main
from chguard.db import connect, create_state, init_db from chguard.db import connect, create_state, init_db
from chguard.restore import PlannedChange from chguard.restore import PlannedChange
@@ -109,3 +113,52 @@ def test_restore_with_only_skipped_items_does_not_prompt(
output = capsys.readouterr().out output = capsys.readouterr().out
assert "missing path" in output assert "missing path" in output
assert "No applicable changes" in output assert "No applicable changes" in output
def test_wrapper_uses_unique_auto_names_within_same_second(
tmp_path: Path, monkeypatch
) -> None:
db_path = tmp_path / "states.db"
target = tmp_path / "target"
target.write_text("data", encoding="utf-8")
class FixedDatetime:
@classmethod
def now(cls) -> datetime:
return datetime(2026, 10, 10, 0, 10, 48)
monkeypatch.setattr(cli, "datetime", FixedDatetime)
monkeypatch.setattr(
cli.subprocess,
"run",
lambda command: SimpleNamespace(returncode=0),
)
for _ in range(2):
monkeypatch.setattr(
sys,
"argv",
[
"chguard",
"--db",
str(db_path),
"--",
"chmod",
"600",
str(target),
],
)
with pytest.raises(SystemExit) as exc_info:
main()
assert exc_info.value.code == 0
conn = connect(db_path)
rows = conn.execute("SELECT name FROM states ORDER BY id").fetchall()
conn.close()
assert rows == [
("auto-20261010-001048",),
("auto-20261010-001048-1",),
]