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
-> _extract_paths_from_command()
-> _common_snapshot_root()
-> create auto-YYYYMMDD-HHMMSS state
-> create a unique auto-YYYYMMDD-HHMMSS[-N] state
-> _iter_entries_for_target() for each path
-> insert each relative path once
-> 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.
**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**
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.
Snapshots created during the same second use a numeric suffix to remain unique,
for example `auto-20251230-161301-1`.
### Scope control
Restore:
+25 -3
View File
@@ -6,6 +6,7 @@ import grp
import importlib.metadata
import os
import pwd
import sqlite3
import stat
import subprocess
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]))
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:
if stat.S_ISDIR(mode):
return "dir"
@@ -451,12 +474,11 @@ def main() -> None:
paths = _extract_paths_from_command(wrapper_cmd)
if paths:
auto_name = f"auto-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
root_path = _common_snapshot_root(paths)
with conn:
state_id = create_state(
conn, auto_name, str(root_path), os.getuid(), commit=False
state_id, auto_name = _create_auto_state(
conn, root_path, os.getuid()
)
seen_paths: set[str] = set()
+53
View File
@@ -2,9 +2,13 @@ from __future__ import annotations
import os
import sys
from datetime import datetime
from pathlib import Path
from types import SimpleNamespace
import pytest
import chguard.cli as cli
from chguard.cli import _restore_preview_rows, main
from chguard.db import connect, create_state, init_db
from chguard.restore import PlannedChange
@@ -109,3 +113,52 @@ def test_restore_with_only_skipped_items_does_not_prompt(
output = capsys.readouterr().out
assert "missing path" 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",),
]