15 Commits
Author SHA1 Message Date
mdaleo404 c404ea6ea5 Merge branch 'renovate/rich-15.x-lockfile' 2026-10-10 08:25:05 +01:00
mdaleo404 80e6fee2df Merge branch 'renovate/rich-15.x-lockfile' of ssh://git.sysmd.uk:222/guardutils/chguard into renovate/rich-15.x-lockfile
Lint & Security / lint-and-security (pull_request) Successful in 1m5s
Lint & Security / tests (pull_request) Successful in 22s
2026-10-10 08:20:36 +01:00
mdaleo404 cfbfcb6f4d Merge pull request 'Update dependency pre-commit to v4' (#18) from renovate/pre-commit-4.x into main
Reviewed-on: #18
2026-10-10 07:20:01 +00:00
mdaleo404 dc8ce892f8 Merge branch 'renovate/pre-commit-4.x' of ssh://git.sysmd.uk:222/guardutils/chguard into renovate/pre-commit-4.x
Lint & Security / lint-and-security (pull_request) Successful in 1m3s
Lint & Security / tests (pull_request) Successful in 23s
2026-10-10 08:18:03 +01:00
kamabot 7ef733b36e Update dependency pre-commit to v4 2026-10-10 08:17:52 +01:00
mdaleo404 58247bf06f Merge pull request 'Update non-major dependencies' (#17) from renovate/non-major-dependencies into main
Reviewed-on: #17
2026-10-10 07:16:59 +00:00
mdaleo404 b833fe0deb Merge branch 'renovate/non-major-dependencies' of ssh://git.sysmd.uk:222/guardutils/chguard into renovate/non-major-dependencies
Lint & Security / lint-and-security (pull_request) Successful in 1m3s
Lint & Security / tests (pull_request) Successful in 24s
2026-10-10 08:14:48 +01:00
kamabot 626b9e91d8 Update non-major dependencies 2026-10-10 08:14:27 +01:00
mdaleo404 345ed69dc1 Bump version to 0.6.1 2026-10-10 08:08:43 +01:00
mdaleo404 a317582b72 Uniquify automatic wrapper snapshot names 2026-10-10 08:07:19 +01:00
kamabot e01c21446f Update non-major dependencies
renovate/stability-days Updates have met minimum release age requirement
renovate/artifacts Artifact file update failure
Lint & Security / lint-and-security (pull_request) Successful in 1m15s
Lint & Security / tests (pull_request) Successful in 23s
2026-10-10 03:02:53 +00:00
kamabot 0bade103d0 Update dependency rich to v15
renovate/stability-days Updates have met minimum release age requirement
Lint & Security / lint-and-security (pull_request) Successful in 1m4s
Lint & Security / tests (pull_request) Successful in 24s
2026-10-09 17:07:16 +00:00
kamabot 1df5a31a7c Update dependency pre-commit to v4
renovate/stability-days Updates have met minimum release age requirement
Lint & Security / lint-and-security (pull_request) Successful in 1m9s
Lint & Security / tests (pull_request) Successful in 23s
2026-10-09 17:07:07 +00:00
mdaleo404 3d3e9dae13 Update workflows and tests 2026-10-09 18:01:04 +01:00
mdaleo404 f6a04e3166 Update package installation for sysmd repository 2026-10-06 18:44:42 +01:00
8 changed files with 164 additions and 41 deletions

No files matched your search

+25
View File
@@ -77,3 +77,28 @@ jobs:
and
( [ .dependencies[] | (.vulns // [])[] | select((.fix_versions | length) > 0) ] | length == 0 )
' pip-audit.json
tests:
name: tests
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"
- name: Install Poetry
run: pip install "poetry>=2,<3"
- name: Install project and dependencies
run: poetry install --with dev
- name: Run tests
run: poetry run pytest
+25
View File
@@ -77,3 +77,28 @@ jobs:
and
( [ .dependencies[] | (.vulns // [])[] | select((.fix_versions | length) > 0) ] | length == 0 )
' pip-audit.json
tests:
name: tests
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"
- name: Install Poetry
run: pip install "poetry>=2,<3"
- name: Install project and dependencies
run: poetry install --with dev
- name: Run tests
run: poetry run pytest
+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)`.
+11 -9
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:
@@ -85,7 +87,7 @@ It only concerns itself with **ownership** and **permissions**.
## Installation
### From GuardUtils package repo
### From the SysMD package repository
This is the preferred method of installation.
@@ -95,7 +97,7 @@ This is the preferred method of installation.
```bash
sudo mkdir -p /usr/share/keyrings
curl -fsSL https://repo.sysmd.uk/guardutils/guardutils.gpg | sudo gpg --dearmor -o /usr/share/keyrings/guardutils.gpg
curl -fsSL https://repo.sysmd.uk/sysmd/sysmd.gpg | sudo gpg --dearmor -o /usr/share/keyrings/sysmd.gpg
```
The GPG fingerprint is `0032C71FA6A11EF9567D4434C5C06BD4603C28B1`.
@@ -103,7 +105,7 @@ The GPG fingerprint is `0032C71FA6A11EF9567D4434C5C06BD4603C28B1`.
#### 2) Add the APT source
```bash
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/guardutils.gpg] https://repo.sysmd.uk/guardutils/debian stable main" | sudo tee /etc/apt/sources.list.d/guardutils.list
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/sysmd.gpg] https://repo.sysmd.uk/sysmd/debian stable main" | sudo tee /etc/apt/sources.list.d/sysmd.list
```
#### 3) Update and install
@@ -118,20 +120,20 @@ sudo apt install chguard
#### 1) Import the GPG key
```
sudo rpm --import https://repo.sysmd.uk/guardutils/guardutils.gpg
sudo rpm --import https://repo.sysmd.uk/sysmd/sysmd.gpg
```
#### 2) Add the repository configuration
```
sudo tee /etc/yum.repos.d/guardutils.repo > /dev/null << 'EOF'
[guardutils]
name=GuardUtils Repository
baseurl=https://repo.sysmd.uk/guardutils/rpm/$basearch
sudo tee /etc/yum.repos.d/sysmd.repo > /dev/null << 'EOF'
[sysmd]
name=SysMD Repository
baseurl=https://repo.sysmd.uk/sysmd/rpm/$basearch
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=https://repo.sysmd.uk/guardutils/guardutils.gpg
gpgkey=https://repo.sysmd.uk/sysmd/sysmd.gpg
EOF
```
+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()
Generated
+20 -25
View File
@@ -1,15 +1,15 @@
# This file is automatically @generated by Poetry 2.5.0 and should not be changed by hand.
# This file is automatically @generated by Poetry 2.4.0 and should not be changed by hand.
[[package]]
name = "argcomplete"
version = "3.6.3"
version = "3.7.2"
description = "Bash tab completion for argparse"
optional = false
python-versions = ">=3.8"
python-versions = ">=3.10"
groups = ["main"]
files = [
{file = "argcomplete-3.6.3-py3-none-any.whl", hash = "sha256:f5007b3a600ccac5d25bbce33089211dfd49eab4a7718da3f10e3082525a92ce"},
{file = "argcomplete-3.6.3.tar.gz", hash = "sha256:62e8ed4fd6a45864acc8235409461b72c9a28ee785a2011cc5eb78318786c89c"},
{file = "argcomplete-3.7.2-py3-none-any.whl", hash = "sha256:6029205678bdd9c1c728a155f5f9ecf5812393f969eef58807641a2bc2aa5b19"},
{file = "argcomplete-3.7.2.tar.gz", hash = "sha256:aad8b69a0b9969edb62db0d1752354c0d50717b10e0cbb00e2a958381b9fc6b9"},
]
[package.extras]
@@ -54,14 +54,14 @@ files = [
[[package]]
name = "filelock"
version = "4.0.8"
version = "4.1.0"
description = "A platform independent file lock."
optional = false
python-versions = ">=3.10"
groups = ["main", "dev"]
files = [
{file = "filelock-4.0.8-py3-none-any.whl", hash = "sha256:325ff22f358c18443b1fcdfa0a7aa3faec4b500c2807c554719da4567b533d31"},
{file = "filelock-4.0.8.tar.gz", hash = "sha256:733d9b6b153fc63672f86104324186818b6bbe9dd7db84e9bb9887b6a04a2775"},
{file = "filelock-4.1.0-py3-none-any.whl", hash = "sha256:2ce9818e3e2d8f284c1a964414447ef148d42a5fd5e2a477a7118e574b293ec1"},
{file = "filelock-4.1.0.tar.gz", hash = "sha256:ad7f724afef953e731b1cc39bcd3a09166d72ed7fcdf29e6e88b1c3235c6715d"},
]
[[package]]
@@ -153,21 +153,16 @@ files = [
[[package]]
name = "platformdirs"
version = "4.5.1"
version = "4.12.4"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
optional = false
python-versions = ">=3.10"
groups = ["main", "dev"]
files = [
{file = "platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31"},
{file = "platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda"},
{file = "platformdirs-4.12.4-py3-none-any.whl", hash = "sha256:78bfb9db2a8471ed7eebe3c3c932da413911042994e699b384fbb4493fa872d7"},
{file = "platformdirs-4.12.4.tar.gz", hash = "sha256:63743c02414e755de4e31b8f68125c1407495b86c5a006e203c01ff8b9924250"},
]
[package.extras]
docs = ["furo (>=2025.9.25)", "proselint (>=0.14)", "sphinx (>=8.2.3)", "sphinx-autodoc-typehints (>=3.2)"]
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.4.2)", "pytest-cov (>=7)", "pytest-mock (>=3.15.1)"]
type = ["mypy (>=1.18.2)"]
[[package]]
name = "pluggy"
version = "1.6.0"
@@ -186,14 +181,14 @@ testing = ["coverage", "pytest", "pytest-benchmark"]
[[package]]
name = "pre-commit"
version = "3.8.0"
version = "4.6.2"
description = "A framework for managing and maintaining multi-language pre-commit hooks."
optional = false
python-versions = ">=3.9"
python-versions = ">=3.10"
groups = ["dev"]
files = [
{file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"},
{file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"},
{file = "pre_commit-4.6.2-py2.py3-none-any.whl", hash = "sha256:e2dde9a75d3bce11bd3831c26d134df00a2803c1d818be6a0383c3dcda25dc4e"},
{file = "pre_commit-4.6.2.tar.gz", hash = "sha256:8f5d7bfb021ecdbcd9d49d89847082dd24172ccde534390081a679ad046e2441"},
]
[package.dependencies]
@@ -340,14 +335,14 @@ files = [
[[package]]
name = "rich"
version = "14.2.0"
version = "15.0.0"
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
optional = false
python-versions = ">=3.8.0"
python-versions = ">=3.9.0"
groups = ["main"]
files = [
{file = "rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd"},
{file = "rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4"},
{file = "rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb"},
{file = "rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36"},
]
[package.dependencies]
@@ -379,4 +374,4 @@ python-discovery = ">=1.6"
[metadata]
lock-version = "2.1"
python-versions = ">=3.11,<4.0"
content-hash = "16fd1da8878d300a16fcff44591a79e0b90041b844eba6c976a53766a3fd5ec3"
content-hash = "b7038c1e7741ec33308a455f42c3dc2e9430365483f42a33b2ef07cdcd819805"
+2 -2
View File
@@ -1,6 +1,6 @@
[project]
name = "chguard"
version = "0.6.0"
version = "0.6.1"
description = "Safety-first tool to snapshot and restore filesystem ownership and permissions."
authors = [{ name = "Marco D'Aleo", email = "marco@marcodaleo.com" }]
license = "GPL-3.0-or-later"
@@ -21,7 +21,7 @@ Repository = "https://git.sysmd.uk/guardutils/chguard"
chguard = "chguard.cli:main"
[tool.poetry.group.dev.dependencies]
pre-commit = "^3.8"
pre-commit = "^4.0.0"
pytest = "^9.1.1"
[tool.black]
+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",),
]