Uniquify automatic wrapper snapshot names
This commit is contained in:
4 files changed
+83
-5
No files matched your search
+3
-2
@@ -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)`.
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
@@ -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",),
|
||||
]
|
||||
Reference in new issue
Block a user