Merge pull request 'Add tab completion, update README, version bump 0.2.0' (#1) from relax_dependencies into main

Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
2025-12-21 10:03:33 +00:00
5 changed files with 119 additions and 25 deletions

View File

@@ -22,8 +22,15 @@ jobs:
- name: Run pre-commit hooks - name: Run pre-commit hooks
run: pre-commit run --all-files --color always run: pre-commit run --all-files --color always
- name: Install Poetry
run: |
pip install poetry
poetry self add poetry-plugin-export
- name: Install pip-audit - name: Install pip-audit
run: pip install pip-audit run: pip install pip-audit
- name: Run pip-audit - name: Audit dependencies (Poetry lockfile)
run: pip-audit run: |
poetry export -f requirements.txt --without-hashes \
| pip-audit -r /dev/stdin

View File

@@ -198,6 +198,16 @@ Snapshots are stored in a local SQLite database containing:
Usernames and permission strings are resolved only for display. Usernames and permission strings are resolved only for display.
### TAB completion
Add this to your `.bashrc`
```
eval "$(register-python-argcomplete chguard)"
```
And then
```
source ~/.bashrc
```
## pre-commit ## pre-commit
This project uses [**pre-commit**](https://pre-commit.com/) to run automatic formatting and security checks before each commit (Black, Bandit, and various safety checks). This project uses [**pre-commit**](https://pre-commit.com/) to run automatic formatting and security checks before each commit (Black, Bandit, and various safety checks).

View File

@@ -1,6 +1,8 @@
from __future__ import annotations from __future__ import annotations
import argparse import argparse
import argcomplete
import importlib.metadata
import os import os
import sys import sys
import stat import stat
@@ -27,6 +29,13 @@ from chguard.restore import plan_restore, apply_restore
from chguard.util import normalize_root from chguard.util import normalize_root
def get_version():
try:
return importlib.metadata.version("chguard")
except importlib.metadata.PackageNotFoundError:
return "unknown"
def _uid_to_name(uid: int) -> str: def _uid_to_name(uid: int) -> str:
"""Return username for uid, or uid as string if unknown.""" """Return username for uid, or uid as string if unknown."""
try: try:
@@ -83,6 +92,19 @@ def _is_root() -> bool:
return os.geteuid() == 0 return os.geteuid() == 0
def complete_state_names(prefix, parsed_args, **kwargs):
try:
conn = connect(
Path(parsed_args.db).expanduser().resolve()
if parsed_args.db
else None
)
rows = conn.execute("SELECT name FROM states").fetchall()
return [name for (name,) in rows if name.startswith(prefix)]
except Exception:
return []
def main() -> None: def main() -> None:
""" """
Entry point for the CLI. Entry point for the CLI.
@@ -100,45 +122,99 @@ def main() -> None:
) )
actions = parser.add_mutually_exclusive_group(required=True) actions = parser.add_mutually_exclusive_group(required=True)
actions.add_argument("--save", metavar="PATH", help="Save state for PATH")
parser.add_argument(
"--version",
action="version",
version=f"chguard {get_version()}",
)
actions.add_argument( actions.add_argument(
"--restore", action="store_true", help="Restore a saved state" "--save",
) metavar="PATH",
help="Save state for PATH",
).completer = argcomplete.FilesCompleter()
actions.add_argument( actions.add_argument(
"--list", action="store_true", help="List saved states" "--restore",
action="store_true",
help="Restore a saved state",
) )
actions.add_argument( actions.add_argument(
"--delete", metavar="STATE", help="Delete a saved state" "--list",
action="store_true",
help="List saved states",
)
actions.add_argument(
"--delete",
metavar="STATE",
help="Delete a saved state",
).completer = complete_state_names
# positional STATE
parser.add_argument(
"state",
nargs="?",
help="State name (required with --restore)",
).completer = complete_state_names
parser.add_argument(
"--name",
help="State name (required with --save)",
) )
parser.add_argument( parser.add_argument(
"state", nargs="?", help="State name (required with --restore)" "--overwrite",
) action="store_true",
parser.add_argument("--name", help="State name (required with --save)") help="Overwrite existing state",
parser.add_argument(
"--overwrite", action="store_true", help="Overwrite existing state"
) )
parser.add_argument( parser.add_argument(
"--permissions", action="store_true", help="Restore MODE only" "--permissions",
) action="store_true",
parser.add_argument( help="Restore MODE only",
"--owner", action="store_true", help="Restore OWNER only"
) )
parser.add_argument( parser.add_argument(
"--dry-run", action="store_true", help="Preview only; do not apply" "--owner",
) action="store_true",
parser.add_argument( help="Restore OWNER only",
"--yes", action="store_true", help="Apply without confirmation"
) )
parser.add_argument("--root", metavar="PATH", help="Override restore root")
parser.add_argument( parser.add_argument(
"--exclude", action="append", default=[], help="Exclude path prefix" "--dry-run",
action="store_true",
help="Preview only; do not apply",
) )
parser.add_argument("--db", metavar="PATH", help="Override database path")
parser.add_argument(
"--yes",
action="store_true",
help="Apply without confirmation",
)
parser.add_argument(
"--root",
metavar="PATH",
help="Override restore root",
).completer = argcomplete.FilesCompleter()
parser.add_argument(
"--exclude",
action="append",
default=[],
help="Exclude path prefix",
).completer = argcomplete.FilesCompleter()
parser.add_argument(
"--db",
metavar="PATH",
help="Override database path",
).completer = argcomplete.FilesCompleter()
argcomplete.autocomplete(parser)
args = parser.parse_args() args = parser.parse_args()
console = Console() console = Console()

2
poetry.lock generated
View File

@@ -289,4 +289,4 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = ">=3.10,<4.0" python-versions = ">=3.10,<4.0"
content-hash = "49f77d614e46109e49e997fa270cb7093d6f7e7d258e370c4eddd4354c20437f" content-hash = "aa4ded468b14fc02b90fdb2a0b1bd446195d02affc359c045b6bbb93858aa747"

View File

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "chguard" name = "chguard"
version = "0.1.0" version = "0.2.0"
description = "Safety-first tool to snapshot and restore filesystem ownership and permissions." description = "Safety-first tool to snapshot and restore filesystem ownership and permissions."
authors = ["Marco D'Aleo <marco@marcodaleo.com>"] authors = ["Marco D'Aleo <marco@marcodaleo.com>"]
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
@@ -13,6 +13,7 @@ python = ">=3.10,<4.0"
rich = ">=12" rich = ">=12"
argcomplete = ">=2" argcomplete = ">=2"
platformdirs = "^4.5.1" platformdirs = "^4.5.1"
filelock = "^3.20.1"
[tool.poetry.scripts] [tool.poetry.scripts]
chguard = "chguard.cli:main" chguard = "chguard.cli:main"