Compare commits
14 Commits
update_mir
...
0.6.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 3e1b2e5488 | |||
|
9adbb74602
|
|||
|
537397ac36
|
|||
|
0dc9b827f9
|
|||
|
bf0dfac0c4
|
|||
|
74db589391
|
|||
| cc764403e3 | |||
|
e66b5d95e9
|
|||
| b23ca5573e | |||
|
b930b4239e
|
|||
|
|
4f681851ac | ||
|
e48adddd4c
|
|||
|
d2fff45db7
|
|||
|
|
b2af78e643 |
@@ -20,7 +20,7 @@ jobs:
|
||||
run: pip install pre-commit
|
||||
|
||||
- name: Run pre-commit hooks
|
||||
uses: pre-commit/action@v3.0.1
|
||||
run: pre-commit run --all-files --color always
|
||||
|
||||
- name: Install pip-audit
|
||||
run: pip install pip-audit
|
||||
97
README.md
97
README.md
@@ -1,8 +1,6 @@
|
||||
[](LICENCE)
|
||||
[](https://github.com/guardutils/mirro/)
|
||||

|
||||

|
||||
[](https://pypi.org/project/mirro/)
|
||||
[](https://git.sysmd.uk/guardutils/mirro/src/branch/main/LICENCE)
|
||||
[](https://git.sysmd.uk/guardutils/mirro/releases)
|
||||
[](https://git.sysmd.uk/guardutils/mirro/src/branch/main/.pre-commit-config.yaml)
|
||||
|
||||
# mirro
|
||||
|
||||
@@ -88,12 +86,14 @@ filename.ext.orig.20251110T174400
|
||||
## Functionalities
|
||||
|
||||
### List all backup files stored in your backup directory.
|
||||
|
||||
```
|
||||
mirro --list
|
||||
```
|
||||
Output includes permissions, owner/group, timestamps, and backup filenames.
|
||||
|
||||
### Restore the most recent backup for a given file.
|
||||
|
||||
```
|
||||
mirro --restore-last ~/.config/myapp/config.ini
|
||||
```
|
||||
@@ -104,25 +104,36 @@ This:
|
||||
|
||||
3. and overwrites the target file with its original contents.
|
||||
|
||||
### Restore ANY backup
|
||||
|
||||
```
|
||||
mirro --restore filename.ext.orig.20251110T174400
|
||||
Restored /path/to/filename.ext from backup filename.ext.orig.20251110T174400
|
||||
```
|
||||
|
||||
### Remove old backup files.
|
||||
|
||||
```
|
||||
mirro --prune-backups
|
||||
```
|
||||
This removes backups older than the number of days set in `MIRRO_BACKUPS_LIFE`.
|
||||
|
||||
### Remove backups older than _N_ days
|
||||
|
||||
```
|
||||
mirro --prune-backups=14
|
||||
```
|
||||
This keeps the last 14 days of backups and removes everything older.
|
||||
|
||||
### Remove all backups
|
||||
|
||||
```
|
||||
mirro --prune-backups=all
|
||||
```
|
||||
This deletes every backup in the backup directory.
|
||||
|
||||
### Environment Variable
|
||||
|
||||
`MIRRO_BACKUPS_LIFE` controls the default number of days to keep when using `mirro --prune-backups`.
|
||||
Its default value is **30** if not set otherwise.
|
||||
```
|
||||
@@ -135,27 +146,79 @@ Invalid or non-numeric values fall back to 30 days.
|
||||
**Note:** _a value of 0 is **invalid**_.
|
||||
|
||||
### Built-in diff
|
||||
|
||||
This shows a _git-like_ diff of the current file version and any of that file backups.
|
||||
```
|
||||
mirro --diff file file.orig.20251121T163121
|
||||
```
|
||||
|
||||
### Shows current directory's history
|
||||
|
||||
Shows which files in the current directory have _**edit history**_ recorded by mirro.
|
||||
For each file, it prints how many revisions exist and when the latest one was saved.
|
||||
```
|
||||
mirro --status
|
||||
|
||||
Files with history in /foo/bar:
|
||||
baz.conf (3 revisions, latest: 2025-01-12 14:03 UTC)
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
### From package manager
|
||||
### From GuardUtils package repo
|
||||
|
||||
This is the preferred method of installation.
|
||||
|
||||
**Ubuntu 22.04 and 24.04**
|
||||
### Debian/Ubuntu
|
||||
|
||||
#### 1) Import the GPG key
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
#### 3) Update and install
|
||||
|
||||
```
|
||||
sudo add-apt-repository ppa:mdaleo/mirro
|
||||
sudo apt update
|
||||
sudo apt install mirro
|
||||
```
|
||||
|
||||
**Fedora 41, 42, 43**
|
||||
### Fedora/RHEL
|
||||
|
||||
#### 1) Import the GPG key
|
||||
|
||||
```
|
||||
sudo dnf copr enable mdaleo/mirro
|
||||
sudo rpm --import https://repo.sysmd.uk/guardutils/guardutils.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
|
||||
enabled=1
|
||||
gpgcheck=1
|
||||
repo_gpgcheck=1
|
||||
gpgkey=https://repo.sysmd.uk/guardutils/guardutils.gpg
|
||||
EOF
|
||||
```
|
||||
|
||||
#### 4) Update and install
|
||||
|
||||
```
|
||||
sudo dnf upgrade --refresh
|
||||
sudo dnf install mirro
|
||||
```
|
||||
|
||||
@@ -178,11 +241,23 @@ pip install mirro
|
||||
|
||||
### From this repository
|
||||
```
|
||||
git clone https://github.com/guardutils/mirro.git
|
||||
git clone https://git.sysmd.uk/guardutils/mirro.git
|
||||
cd mirro/
|
||||
poetry install
|
||||
```
|
||||
|
||||
## TAB completion
|
||||
|
||||
Add this to your `.bashrc`
|
||||
```
|
||||
eval "$(register-python-argcomplete mirro)"
|
||||
```
|
||||
|
||||
And then
|
||||
```
|
||||
source ~/.bashrc
|
||||
```
|
||||
|
||||
## How to run the tests
|
||||
|
||||
- Clone this repository
|
||||
|
||||
16
poetry.lock
generated
16
poetry.lock
generated
@@ -1,5 +1,19 @@
|
||||
# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "argcomplete"
|
||||
version = "3.6.3"
|
||||
description = "Bash tab completion for argparse"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "argcomplete-3.6.3-py3-none-any.whl", hash = "sha256:f5007b3a600ccac5d25bbce33089211dfd49eab4a7718da3f10e3082525a92ce"},
|
||||
{file = "argcomplete-3.6.3.tar.gz", hash = "sha256:62e8ed4fd6a45864acc8235409461b72c9a28ee785a2011cc5eb78318786c89c"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
test = ["coverage", "mypy", "pexpect", "ruff", "wheel"]
|
||||
|
||||
[[package]]
|
||||
name = "cfgv"
|
||||
version = "3.4.0"
|
||||
@@ -488,4 +502,4 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = ">=3.10,<4.0"
|
||||
content-hash = "98acd9fd57ec90c98a407b83122fd9c8ed432383e095a47d44e201bf187d3107"
|
||||
content-hash = "c7b7c37f18023c55d23bd61dad81cc93600a14a17fbed43958afaac369a52f91"
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
[tool.poetry]
|
||||
name = "mirro"
|
||||
version = "0.4.0"
|
||||
version = "0.6.0"
|
||||
description = "A safe editing wrapper: edits a temp copy, compares, and saves original backup if changed."
|
||||
authors = ["Marco D'Aleo <marco@marcodaleo.com>"]
|
||||
license = "GPL-3.0-or-later"
|
||||
readme = "README.md"
|
||||
homepage = "https://github.com/guardutils/mirro"
|
||||
repository = "https://github.com/guardutils/mirro"
|
||||
homepage = "https://git.sysmd.uk/guardutils/mirro"
|
||||
repository = "https://git.sysmd.uk/guardutils/mirro"
|
||||
packages = [{include = "mirro", from = "src"}]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.10,<4.0"
|
||||
argcomplete = ">=2"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
mirro = "mirro.main:main"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import importlib.metadata
|
||||
import argparse
|
||||
import argcomplete
|
||||
import tempfile
|
||||
import subprocess
|
||||
import os
|
||||
@@ -75,6 +76,19 @@ def strip_mirro_header(text: str) -> str:
|
||||
return "".join(lines[i:])
|
||||
|
||||
|
||||
def extract_original_path(backup_text: str) -> Path | None:
|
||||
"""
|
||||
Extract the original file path from a mirro backup header.
|
||||
"""
|
||||
for line in backup_text.splitlines():
|
||||
if line.startswith("# Original file:"):
|
||||
path = line.split(":", 1)[1].strip()
|
||||
return Path(path).expanduser()
|
||||
if line.strip() == "":
|
||||
break
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Safely edit a file with automatic original backup if changed."
|
||||
@@ -106,6 +120,13 @@ def main():
|
||||
help="Restore the last backup of the given file and exit",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--restore",
|
||||
metavar="BACKUP",
|
||||
type=str,
|
||||
help="Restore the given backup file and exit",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--prune-backups",
|
||||
nargs="?",
|
||||
@@ -120,6 +141,14 @@ def main():
|
||||
help="Show a unified diff between FILE and BACKUP and exit",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--status",
|
||||
action="store_true",
|
||||
help="Show which files in the current directory have 'revisions'",
|
||||
)
|
||||
|
||||
argcomplete.autocomplete(parser)
|
||||
|
||||
# Parse only options. Leave everything else untouched.
|
||||
args, positional = parser.parse_known_args()
|
||||
|
||||
@@ -249,6 +278,52 @@ def main():
|
||||
|
||||
return
|
||||
|
||||
if args.status:
|
||||
backup_dir = Path(args.backup_dir).expanduser().resolve()
|
||||
cwd = Path.cwd()
|
||||
|
||||
if not backup_dir.exists():
|
||||
print(f"No mirro backups found in {cwd}.")
|
||||
return 0
|
||||
|
||||
# Build map: filename -> list of backups
|
||||
backup_map = {}
|
||||
for b in backup_dir.iterdir():
|
||||
name = b.name
|
||||
if ".orig." not in name:
|
||||
continue
|
||||
filename, _, _ = name.partition(".orig.")
|
||||
backup_map.setdefault(filename, []).append(b)
|
||||
|
||||
# Find files in current dir that have backups
|
||||
entries = []
|
||||
for file in cwd.iterdir():
|
||||
if file.is_file() and file.name in backup_map:
|
||||
backups = backup_map[file.name]
|
||||
backups_sorted = sorted(
|
||||
backups, key=lambda x: x.stat().st_mtime, reverse=True
|
||||
)
|
||||
latest = backups_sorted[0]
|
||||
|
||||
latest_mtime = time.strftime(
|
||||
"%Y-%m-%d %H:%M:%S UTC",
|
||||
time.gmtime(latest.stat().st_mtime),
|
||||
)
|
||||
|
||||
entries.append((file.name, len(backups), latest_mtime))
|
||||
|
||||
# Nothing found?
|
||||
if not entries:
|
||||
print(f"No mirro backups found in {cwd}.")
|
||||
return 0
|
||||
|
||||
# Otherwise print nice report
|
||||
print(f"Files with history in {cwd}:")
|
||||
for name, count, latest in entries:
|
||||
print(f" {name:16} ({count} revision(s), latest: {latest})")
|
||||
|
||||
return 0
|
||||
|
||||
if args.restore_last:
|
||||
backup_dir = Path(args.backup_dir).expanduser().resolve()
|
||||
target = Path(args.restore_last).expanduser().resolve()
|
||||
@@ -265,7 +340,7 @@ def main():
|
||||
]
|
||||
|
||||
if not backups:
|
||||
print(f"No backups found for {target}")
|
||||
print(f"No history found for {target}")
|
||||
return 1
|
||||
|
||||
# newest backup
|
||||
@@ -279,6 +354,45 @@ def main():
|
||||
print(f"Restored {target} from backup {last.name}")
|
||||
return
|
||||
|
||||
if args.restore:
|
||||
backup_arg = args.restore
|
||||
backup_dir = Path(args.backup_dir).expanduser().resolve()
|
||||
|
||||
# Resolve backup path
|
||||
if os.path.isabs(backup_arg) or backup_arg.startswith("~"):
|
||||
backup_path = Path(backup_arg).expanduser().resolve()
|
||||
else:
|
||||
backup_path = backup_dir / backup_arg
|
||||
|
||||
if not backup_path.exists():
|
||||
print(f"Backup not found: {backup_path}")
|
||||
return 1
|
||||
|
||||
raw = backup_path.read_text(encoding="utf-8", errors="replace")
|
||||
|
||||
target = extract_original_path(raw)
|
||||
if not target:
|
||||
print(
|
||||
"Could not determine original file location from backup header."
|
||||
)
|
||||
return 1
|
||||
|
||||
restored_text = strip_mirro_header(raw)
|
||||
|
||||
# Permission checks
|
||||
if target.exists() and not os.access(target, os.W_OK):
|
||||
print(f"Need elevated privileges to restore {target}")
|
||||
return 1
|
||||
if not target.exists() and not os.access(target.parent, os.W_OK):
|
||||
print(f"Need elevated privileges to create {target}")
|
||||
return 1
|
||||
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
target.write_text(restored_text, encoding="utf-8")
|
||||
|
||||
print(f"Restored {target} from backup {backup_path.name}")
|
||||
return 0
|
||||
|
||||
if args.prune_backups is not None:
|
||||
mode = args.prune_backups
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ def test_strip_header_removes_header():
|
||||
def test_strip_header_preserves_shebang():
|
||||
text = "#!/usr/bin/env python3\nprint('hi')\n"
|
||||
out = mirro.strip_mirro_header(text)
|
||||
assert out == text # unchanged
|
||||
assert out == text
|
||||
|
||||
|
||||
def test_strip_header_non_header_file():
|
||||
@@ -297,29 +297,6 @@ def test_main_permission_denied_create(tmp_path, monkeypatch, capsys):
|
||||
assert "Need elevated privileges to create" in capsys.readouterr().out
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Editor ordering: non-nano branch
|
||||
# ============================================================
|
||||
|
||||
|
||||
def test_main_editor_non_nano(tmp_path, monkeypatch, capsys):
|
||||
target = tmp_path / "vim.txt"
|
||||
target.write_text("old\n")
|
||||
|
||||
def fake_call(cmd):
|
||||
temp = Path(cmd[1])
|
||||
temp.write_text("edited\n")
|
||||
|
||||
monkeypatch.setenv("EDITOR", "vim")
|
||||
monkeypatch.setattr(subprocess, "call", fake_call)
|
||||
monkeypatch.setattr(os, "access", lambda p, m: True)
|
||||
|
||||
with patch("sys.argv", ["mirro", str(target)]):
|
||||
mirro.main()
|
||||
|
||||
assert target.read_text() == "edited\n"
|
||||
|
||||
|
||||
# ============================================================
|
||||
# --list
|
||||
# ============================================================
|
||||
@@ -376,8 +353,10 @@ def test_restore_last_no_backups(tmp_path, capsys):
|
||||
):
|
||||
result = mirro.main()
|
||||
|
||||
out = capsys.readouterr().out
|
||||
assert result == 1
|
||||
assert "No backups found" in capsys.readouterr().out
|
||||
assert "No history found for" in out
|
||||
assert str(target) in out
|
||||
|
||||
|
||||
def test_restore_last_success(tmp_path, capsys):
|
||||
@@ -401,7 +380,6 @@ def test_restore_last_success(tmp_path, capsys):
|
||||
b1.write_text(mirro_header + "old1")
|
||||
b2.write_text(mirro_header + "old2")
|
||||
|
||||
# ensure newest
|
||||
os.utime(b2, (time.time(), time.time()))
|
||||
|
||||
with patch(
|
||||
@@ -415,153 +393,135 @@ def test_restore_last_success(tmp_path, capsys):
|
||||
|
||||
|
||||
# ============================================================
|
||||
# --prune-backups
|
||||
# --restore (new)
|
||||
# ============================================================
|
||||
|
||||
|
||||
def test_prune_all(tmp_path, capsys):
|
||||
d = tmp_path / "bk"
|
||||
d.mkdir()
|
||||
(d / "a").write_text("x")
|
||||
(d / "b").write_text("y")
|
||||
def test_restore_backup_to_original_location(tmp_path, capsys):
|
||||
backup_dir = tmp_path / "bk"
|
||||
backup_dir.mkdir()
|
||||
|
||||
with patch(
|
||||
"sys.argv", ["mirro", "--prune-backups=all", "--backup-dir", str(d)]
|
||||
):
|
||||
mirro.main()
|
||||
original_dir = tmp_path / "orig"
|
||||
original_dir.mkdir()
|
||||
|
||||
out = capsys.readouterr().out
|
||||
assert "Removed ALL backups" in out
|
||||
assert not any(d.iterdir())
|
||||
target = original_dir / "file.txt"
|
||||
|
||||
|
||||
def test_prune_numeric(tmp_path, capsys):
|
||||
d = tmp_path / "bk"
|
||||
d.mkdir()
|
||||
|
||||
old = d / "old"
|
||||
new = d / "new"
|
||||
old.write_text("x")
|
||||
new.write_text("y")
|
||||
|
||||
one_day_seconds = 86400
|
||||
|
||||
os.utime(
|
||||
old,
|
||||
(
|
||||
time.time() - one_day_seconds * 10,
|
||||
time.time() - one_day_seconds * 10,
|
||||
),
|
||||
)
|
||||
os.utime(new, None)
|
||||
|
||||
with patch(
|
||||
"sys.argv", ["mirro", "--prune-backups=5", "--backup-dir", str(d)]
|
||||
):
|
||||
mirro.main()
|
||||
|
||||
out = capsys.readouterr().out
|
||||
assert "Removed 1" in out
|
||||
assert new.exists()
|
||||
assert not old.exists()
|
||||
|
||||
|
||||
def test_prune_default_env(tmp_path, monkeypatch, capsys):
|
||||
monkeypatch.setenv("MIRRO_BACKUPS_LIFE", "1")
|
||||
|
||||
d = tmp_path / "bk"
|
||||
d.mkdir()
|
||||
|
||||
f = d / "x"
|
||||
f.write_text("hi")
|
||||
|
||||
os.utime(f, (time.time() - 86400 * 2, time.time() - 86400 * 2))
|
||||
|
||||
with patch(
|
||||
"sys.argv", ["mirro", "--prune-backups", "--backup-dir", str(d)]
|
||||
):
|
||||
mirro.main()
|
||||
|
||||
assert "Removed 1" in capsys.readouterr().out
|
||||
|
||||
|
||||
def test_prune_invalid_env(tmp_path, monkeypatch, capsys):
|
||||
monkeypatch.setenv("MIRRO_BACKUPS_LIFE", "nope")
|
||||
|
||||
d = tmp_path / "bk"
|
||||
d.mkdir()
|
||||
|
||||
with patch(
|
||||
"sys.argv", ["mirro", "--prune-backups", "--backup-dir", str(d)]
|
||||
):
|
||||
mirro.main()
|
||||
|
||||
out = capsys.readouterr().out
|
||||
assert "Invalid MIRRO_BACKUPS_LIFE value" in out
|
||||
|
||||
|
||||
def test_prune_invalid_arg(tmp_path, capsys):
|
||||
with patch("sys.argv", ["mirro", "--prune-backups=zzz"]):
|
||||
result = mirro.main()
|
||||
|
||||
assert result == 1
|
||||
assert "Invalid value for --prune-backups" in capsys.readouterr().out
|
||||
|
||||
|
||||
# ============================================================
|
||||
# --diff tests
|
||||
# ============================================================
|
||||
|
||||
|
||||
def test_diff_basic(tmp_path, capsys):
|
||||
d = tmp_path / "bk"
|
||||
d.mkdir()
|
||||
|
||||
file = tmp_path / "t.txt"
|
||||
file.write_text("line1\nline2\n")
|
||||
|
||||
backup = d / "t.txt.orig.20250101T010203"
|
||||
backup.write_text(
|
||||
mirro_header = (
|
||||
"# ---------------------------------------------------\n"
|
||||
"# mirro backup\n"
|
||||
"# whatever\n"
|
||||
f"# Original file: {target}\n"
|
||||
"# Timestamp: test\n"
|
||||
"# Delete this header if you want to restore the file\n"
|
||||
"# ---------------------------------------------------\n"
|
||||
"\n"
|
||||
"line1\nold\n"
|
||||
)
|
||||
|
||||
backup = backup_dir / "file.txt.orig.20250101T010203"
|
||||
backup.write_text(mirro_header + "restored content\n")
|
||||
|
||||
with patch(
|
||||
"sys.argv",
|
||||
["mirro", "--restore", backup.name, "--backup-dir", str(backup_dir)],
|
||||
):
|
||||
result = mirro.main()
|
||||
|
||||
assert result == 0
|
||||
assert target.exists()
|
||||
assert target.read_text() == "restored content\n"
|
||||
assert "Restored" in capsys.readouterr().out
|
||||
|
||||
|
||||
def test_restore_missing_backup(tmp_path, capsys):
|
||||
backup_dir = tmp_path / "bk"
|
||||
backup_dir.mkdir()
|
||||
|
||||
with patch(
|
||||
"sys.argv",
|
||||
[
|
||||
"mirro",
|
||||
"--restore",
|
||||
"nope.orig.123",
|
||||
"--backup-dir",
|
||||
str(backup_dir),
|
||||
],
|
||||
):
|
||||
result = mirro.main()
|
||||
|
||||
assert result == 1
|
||||
assert "Backup not found" in capsys.readouterr().out
|
||||
|
||||
|
||||
def test_restore_missing_original_path_in_header(tmp_path, capsys):
|
||||
backup_dir = tmp_path / "bk"
|
||||
backup_dir.mkdir()
|
||||
|
||||
bad_backup = backup_dir / "x.txt.orig.123"
|
||||
bad_backup.write_text(
|
||||
"# ---------------------------------------------------\n"
|
||||
"# mirro backup\n"
|
||||
"# Timestamp: test\n"
|
||||
"\n"
|
||||
"data\n"
|
||||
)
|
||||
|
||||
with patch(
|
||||
"sys.argv",
|
||||
["mirro", "--diff", str(file), backup.name, "--backup-dir", str(d)],
|
||||
[
|
||||
"mirro",
|
||||
"--restore",
|
||||
bad_backup.name,
|
||||
"--backup-dir",
|
||||
str(backup_dir),
|
||||
],
|
||||
):
|
||||
mirro.main()
|
||||
result = mirro.main()
|
||||
|
||||
out = capsys.readouterr().out
|
||||
|
||||
assert "--- a/t.txt" in out
|
||||
assert "+++ b/t.txt" in out
|
||||
assert "@@" in out
|
||||
assert "-old" in out
|
||||
assert "+line2" in out
|
||||
assert result == 1
|
||||
assert (
|
||||
"Could not determine original file location" in capsys.readouterr().out
|
||||
)
|
||||
|
||||
|
||||
def test_diff_wrong_backup_name_rejected(tmp_path, capsys):
|
||||
d = tmp_path / "bk"
|
||||
d.mkdir()
|
||||
# ============================================================
|
||||
# --status
|
||||
# ============================================================
|
||||
|
||||
file = tmp_path / "foo.txt"
|
||||
file.write_text("hello\n")
|
||||
|
||||
bad = d / "bar.txt.orig.20250101T010203"
|
||||
bad.write_text("stuff\n")
|
||||
def test_status_no_backups(tmp_path, monkeypatch, capsys):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
|
||||
backup_dir = tmp_path / "bk"
|
||||
with patch(
|
||||
"sys.argv",
|
||||
["mirro", "--diff", str(file), bad.name, "--backup-dir", str(d)],
|
||||
"sys.argv", ["mirro", "--status", "--backup-dir", str(backup_dir)]
|
||||
):
|
||||
result = mirro.main()
|
||||
|
||||
out = capsys.readouterr().out
|
||||
assert result == 0
|
||||
assert f"No mirro backups found in {tmp_path}" in out
|
||||
|
||||
assert result == 1
|
||||
assert "does not match the file being diffed" in out
|
||||
assert "foo.txt.orig." in out
|
||||
|
||||
def test_status_backups_found(tmp_path, monkeypatch, capsys):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
|
||||
cwd = tmp_path
|
||||
backup_dir = tmp_path / "bk"
|
||||
backup_dir.mkdir()
|
||||
|
||||
(tmp_path / "a.txt").write_text("data1")
|
||||
(tmp_path / "b.txt").write_text("data2")
|
||||
|
||||
(backup_dir / "a.txt.orig.1").write_text("x")
|
||||
(backup_dir / "a.txt.orig.2").write_text("y")
|
||||
(backup_dir / "b.txt.orig.3").write_text("z")
|
||||
|
||||
with patch(
|
||||
"sys.argv", ["mirro", "--status", "--backup-dir", str(backup_dir)]
|
||||
):
|
||||
result = mirro.main()
|
||||
|
||||
out = capsys.readouterr().out
|
||||
assert result == 0
|
||||
assert f"Files with history in {cwd}:" in out
|
||||
assert "a.txt" in out
|
||||
assert "b.txt" in out
|
||||
|
||||
Reference in New Issue
Block a user