git-alibi 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Zach Light
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,245 @@
1
+ Metadata-Version: 2.4
2
+ Name: git-alibi
3
+ Version: 0.1.0
4
+ Summary: Rewrite git commit timestamps to fit within or exclude certain time windows
5
+ Author-email: Zach Light <zachary.j.light@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/zlight97/git-alibi
8
+ Project-URL: Issues, https://github.com/zlight97/git-alibi/issues
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Operating System :: POSIX :: Linux
15
+ Classifier: Operating System :: MacOS
16
+ Classifier: Topic :: Software Development :: Version Control
17
+ Classifier: Topic :: Utilities
18
+ Requires-Python: >=3.11
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: click>=8.0
22
+ Requires-Dist: gitpython>=3.1
23
+ Requires-Dist: tomlkit>=0.12
24
+ Requires-Dist: git-filter-repo>=2.38
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=7.0; extra == "dev"
27
+ Requires-Dist: build>=1.0; extra == "dev"
28
+ Requires-Dist: twine>=5.0; extra == "dev"
29
+ Dynamic: license-file
30
+
31
+ # git-alibi
32
+
33
+ Rewrite git commit timestamps to fit within (or avoid) configured time windows.
34
+
35
+ Useful for keeping commit history clean when you work odd hours, across timezones,
36
+ or want commits to consistently appear within business hours. Alibi saves a backup
37
+ before every rewrite so changes can always be undone.
38
+
39
+ ## Requirements
40
+
41
+ - Python 3.11 or newer
42
+ - git 2.25 or newer
43
+
44
+ ## Installation
45
+
46
+ **pip** (simplest):
47
+ ```bash
48
+ cd git-alibi
49
+ pip install .
50
+ ```
51
+
52
+ **pipx** (isolated, recommended for CLI tools):
53
+ ```bash
54
+ pipx install /path/to/git-alibi
55
+ ```
56
+
57
+ **Virtual environment** (if you prefer not to touch system Python):
58
+ ```bash
59
+ cd git-alibi
60
+ python3 -m venv .venv
61
+ source .venv/bin/activate # Windows: .venv\Scripts\activate
62
+ pip install .
63
+ ```
64
+
65
+ Verify the install:
66
+ ```bash
67
+ git-alibi --help
68
+ # git also picks it up automatically:
69
+ git alibi --help
70
+ ```
71
+
72
+ ## Uninstallation
73
+
74
+ ```bash
75
+ pip uninstall git-alibi # if installed with pip
76
+ pipx uninstall git-alibi # if installed with pipx
77
+ rm -rf .venv # if installed into a venv, just delete it
78
+ ```
79
+
80
+ ## Quick start
81
+
82
+ Preview what would change without touching the repo:
83
+ ```bash
84
+ git-alibi rewrite --dry-run
85
+ ```
86
+
87
+ Apply the rewrite:
88
+ ```bash
89
+ git-alibi rewrite
90
+ ```
91
+
92
+ If the branch was already pushed:
93
+ ```bash
94
+ git push --force-with-lease
95
+ ```
96
+
97
+ Undo the last rewrite:
98
+ ```bash
99
+ git-alibi restore
100
+ ```
101
+
102
+ ## Configuration
103
+
104
+ Alibi is configured with TOML files. Settings in the local file take precedence
105
+ over the global one.
106
+
107
+ | File | Scope |
108
+ |------|-------|
109
+ | `~/.config/alibi/config.toml` | Global (all repos) |
110
+ | `.git/alibi/config.toml` | Local (this repo only) |
111
+
112
+ Open a config file in `$EDITOR` (created with commented-out defaults if it doesn't exist):
113
+ ```bash
114
+ git-alibi config local
115
+ git-alibi config global
116
+ ```
117
+
118
+ ### Example config
119
+
120
+ ```toml
121
+ [behavior]
122
+ timezone = "America/Chicago"
123
+ out_of_window = "nearest" # nearest / previous / next / random
124
+ spacing = "preserve" # preserve / proportional / even / random
125
+
126
+ [days]
127
+ block = ["SAT", "SUN"] # never place commits on weekends
128
+
129
+ [times]
130
+ allow = ["09:00-17:00"] # only allow commits during business hours
131
+
132
+ [authors]
133
+ # CURRENT expands to the email in git config user.email
134
+ emails = ["CURRENT"]
135
+
136
+ [markers]
137
+ # Commits containing this string in their message are never rewritten
138
+ skip = ["[no-alibi]"]
139
+ ```
140
+
141
+ ## Commands
142
+
143
+ ### `rewrite`
144
+
145
+ Rewrites commit timestamps to fit within the configured windows. Operates on
146
+ commits in the current branch since it diverged from `main`/`master` (or the
147
+ upstream tracking branch), for the current git user's commits only.
148
+
149
+ ```bash
150
+ git-alibi rewrite [OPTIONS] [REF]
151
+ ```
152
+
153
+ | Option | Description |
154
+ |--------|-------------|
155
+ | `--dry-run` | Preview changes without modifying the repo |
156
+ | `-v, --verbose` | Show all commits in dry-run output, not just changed ones |
157
+ | `--no-backup` | Skip saving a backup before rewriting |
158
+ | `--shift DURATION` | Shift timestamps by a fixed amount instead of fitting windows (e.g. `+2h`, `-5h30m`, `+5:30`) |
159
+ | `--timezone TZ` | Override the timezone (IANA name, e.g. `America/Chicago`) |
160
+ | `--allow-days DAYS` | Comma-separated days to allow (e.g. `MON,TUE,WED,THU,FRI`) |
161
+ | `--block-days DAYS` | Comma-separated days to block (e.g. `SAT,SUN`) |
162
+ | `--allow-times RANGES` | Time ranges to allow (e.g. `09:00-17:00`, `MON+FRI@09:00-12:00`) |
163
+ | `--block-times RANGES` | Time ranges to block |
164
+ | `--allow-dates DATES` | Dates or ranges to allow (e.g. `2024-03-01:2024-03-31`) |
165
+ | `--block-dates DATES` | Dates or ranges to block (e.g. `2024-12-25`) |
166
+ | `--out-of-window` | How to handle commits outside all windows: `nearest` (default), `previous`, `next`, `random` |
167
+ | `--spacing` | How to distribute commits within a window: `preserve` (default), `proportional`, `even`, `random` |
168
+ | `--author-emails EMAILS` | Comma-separated emails to rewrite; `CURRENT` = git config `user.email` |
169
+ | `--all-authors` | Rewrite commits by all authors, not just the current user |
170
+ | `--all-history` | Rewrite all reachable history, not just the current branch |
171
+ | `--skip-markers MARKERS` | Comma-separated message markers that exempt a commit (default: `[no-alibi]`) |
172
+ | `-f, --force` | Proceed even if signed commits would be rewritten |
173
+
174
+ #### Timezone correction with `--shift`
175
+
176
+ If you committed in the wrong timezone, shift all timestamps by a fixed offset:
177
+
178
+ ```bash
179
+ git-alibi rewrite --shift +5:30 # move everything forward 5h30m
180
+ git-alibi rewrite --shift -8h # move everything back 8 hours
181
+ git-alibi rewrite --dry-run --shift +2h # preview first
182
+ ```
183
+
184
+ #### Opting out of rewriting
185
+
186
+ Add `[no-alibi]` anywhere in a commit message to permanently exempt that commit:
187
+
188
+ ```
189
+ fix: correct off-by-one error [no-alibi]
190
+ ```
191
+
192
+ The marker string is configurable via `[markers] skip` in the config file or
193
+ `--skip-markers` on the command line.
194
+
195
+ ### `restore`
196
+
197
+ Restores commit timestamps from the backup saved before a previous rewrite.
198
+
199
+ ```bash
200
+ git-alibi restore [OPTIONS] [REF]
201
+ ```
202
+
203
+ | Option | Description |
204
+ |--------|-------------|
205
+ | `--dry-run` | Preview what would be restored without applying |
206
+ | `-v, --verbose` | Show all commits, not just changed ones |
207
+ | `--last N` | Undo the Nth most recent rewrite (default: `1` = last) |
208
+ | `--rewrite ID` | Restore a specific rewrite by ID (see `history`) |
209
+ | `-f, --force` | Proceed even if signed commits would be rewritten |
210
+
211
+ ```bash
212
+ git-alibi restore # undo the last rewrite
213
+ git-alibi restore --last 2 # undo the second-to-last rewrite
214
+ git-alibi restore --rewrite 3 # restore to a specific snapshot ID
215
+ ```
216
+
217
+ ### `history`
218
+
219
+ Shows all recorded rewrites and the exact command to restore each one.
220
+
221
+ ```bash
222
+ git-alibi history [-v]
223
+ ```
224
+
225
+ ```
226
+ Rewrite history — 3 snapshots in .git/alibi/backup.json
227
+
228
+ ID WHEN COMMITS --last RESTORE
229
+ ───────────────────────────────────────────────────────
230
+ 1 2024-01-06 Sat 09:15:00 5 3 alibi restore --last 3
231
+ 2 2024-01-07 Sun 14:30:00 3 2 alibi restore --last 2
232
+ 3 2024-01-08 Mon 11:15:00 2 1 alibi restore ← latest
233
+ ```
234
+
235
+ Use `-v` to also list the individual commits and their original timestamps.
236
+
237
+ ### `config`
238
+
239
+ Opens a config file in `$EDITOR`, creating it with commented-out defaults if it
240
+ doesn't exist yet.
241
+
242
+ ```bash
243
+ git-alibi config local # .git/alibi/config.toml
244
+ git-alibi config global # ~/.config/alibi/config.toml
245
+ ```
@@ -0,0 +1,215 @@
1
+ # git-alibi
2
+
3
+ Rewrite git commit timestamps to fit within (or avoid) configured time windows.
4
+
5
+ Useful for keeping commit history clean when you work odd hours, across timezones,
6
+ or want commits to consistently appear within business hours. Alibi saves a backup
7
+ before every rewrite so changes can always be undone.
8
+
9
+ ## Requirements
10
+
11
+ - Python 3.11 or newer
12
+ - git 2.25 or newer
13
+
14
+ ## Installation
15
+
16
+ **pip** (simplest):
17
+ ```bash
18
+ cd git-alibi
19
+ pip install .
20
+ ```
21
+
22
+ **pipx** (isolated, recommended for CLI tools):
23
+ ```bash
24
+ pipx install /path/to/git-alibi
25
+ ```
26
+
27
+ **Virtual environment** (if you prefer not to touch system Python):
28
+ ```bash
29
+ cd git-alibi
30
+ python3 -m venv .venv
31
+ source .venv/bin/activate # Windows: .venv\Scripts\activate
32
+ pip install .
33
+ ```
34
+
35
+ Verify the install:
36
+ ```bash
37
+ git-alibi --help
38
+ # git also picks it up automatically:
39
+ git alibi --help
40
+ ```
41
+
42
+ ## Uninstallation
43
+
44
+ ```bash
45
+ pip uninstall git-alibi # if installed with pip
46
+ pipx uninstall git-alibi # if installed with pipx
47
+ rm -rf .venv # if installed into a venv, just delete it
48
+ ```
49
+
50
+ ## Quick start
51
+
52
+ Preview what would change without touching the repo:
53
+ ```bash
54
+ git-alibi rewrite --dry-run
55
+ ```
56
+
57
+ Apply the rewrite:
58
+ ```bash
59
+ git-alibi rewrite
60
+ ```
61
+
62
+ If the branch was already pushed:
63
+ ```bash
64
+ git push --force-with-lease
65
+ ```
66
+
67
+ Undo the last rewrite:
68
+ ```bash
69
+ git-alibi restore
70
+ ```
71
+
72
+ ## Configuration
73
+
74
+ Alibi is configured with TOML files. Settings in the local file take precedence
75
+ over the global one.
76
+
77
+ | File | Scope |
78
+ |------|-------|
79
+ | `~/.config/alibi/config.toml` | Global (all repos) |
80
+ | `.git/alibi/config.toml` | Local (this repo only) |
81
+
82
+ Open a config file in `$EDITOR` (created with commented-out defaults if it doesn't exist):
83
+ ```bash
84
+ git-alibi config local
85
+ git-alibi config global
86
+ ```
87
+
88
+ ### Example config
89
+
90
+ ```toml
91
+ [behavior]
92
+ timezone = "America/Chicago"
93
+ out_of_window = "nearest" # nearest / previous / next / random
94
+ spacing = "preserve" # preserve / proportional / even / random
95
+
96
+ [days]
97
+ block = ["SAT", "SUN"] # never place commits on weekends
98
+
99
+ [times]
100
+ allow = ["09:00-17:00"] # only allow commits during business hours
101
+
102
+ [authors]
103
+ # CURRENT expands to the email in git config user.email
104
+ emails = ["CURRENT"]
105
+
106
+ [markers]
107
+ # Commits containing this string in their message are never rewritten
108
+ skip = ["[no-alibi]"]
109
+ ```
110
+
111
+ ## Commands
112
+
113
+ ### `rewrite`
114
+
115
+ Rewrites commit timestamps to fit within the configured windows. Operates on
116
+ commits in the current branch since it diverged from `main`/`master` (or the
117
+ upstream tracking branch), for the current git user's commits only.
118
+
119
+ ```bash
120
+ git-alibi rewrite [OPTIONS] [REF]
121
+ ```
122
+
123
+ | Option | Description |
124
+ |--------|-------------|
125
+ | `--dry-run` | Preview changes without modifying the repo |
126
+ | `-v, --verbose` | Show all commits in dry-run output, not just changed ones |
127
+ | `--no-backup` | Skip saving a backup before rewriting |
128
+ | `--shift DURATION` | Shift timestamps by a fixed amount instead of fitting windows (e.g. `+2h`, `-5h30m`, `+5:30`) |
129
+ | `--timezone TZ` | Override the timezone (IANA name, e.g. `America/Chicago`) |
130
+ | `--allow-days DAYS` | Comma-separated days to allow (e.g. `MON,TUE,WED,THU,FRI`) |
131
+ | `--block-days DAYS` | Comma-separated days to block (e.g. `SAT,SUN`) |
132
+ | `--allow-times RANGES` | Time ranges to allow (e.g. `09:00-17:00`, `MON+FRI@09:00-12:00`) |
133
+ | `--block-times RANGES` | Time ranges to block |
134
+ | `--allow-dates DATES` | Dates or ranges to allow (e.g. `2024-03-01:2024-03-31`) |
135
+ | `--block-dates DATES` | Dates or ranges to block (e.g. `2024-12-25`) |
136
+ | `--out-of-window` | How to handle commits outside all windows: `nearest` (default), `previous`, `next`, `random` |
137
+ | `--spacing` | How to distribute commits within a window: `preserve` (default), `proportional`, `even`, `random` |
138
+ | `--author-emails EMAILS` | Comma-separated emails to rewrite; `CURRENT` = git config `user.email` |
139
+ | `--all-authors` | Rewrite commits by all authors, not just the current user |
140
+ | `--all-history` | Rewrite all reachable history, not just the current branch |
141
+ | `--skip-markers MARKERS` | Comma-separated message markers that exempt a commit (default: `[no-alibi]`) |
142
+ | `-f, --force` | Proceed even if signed commits would be rewritten |
143
+
144
+ #### Timezone correction with `--shift`
145
+
146
+ If you committed in the wrong timezone, shift all timestamps by a fixed offset:
147
+
148
+ ```bash
149
+ git-alibi rewrite --shift +5:30 # move everything forward 5h30m
150
+ git-alibi rewrite --shift -8h # move everything back 8 hours
151
+ git-alibi rewrite --dry-run --shift +2h # preview first
152
+ ```
153
+
154
+ #### Opting out of rewriting
155
+
156
+ Add `[no-alibi]` anywhere in a commit message to permanently exempt that commit:
157
+
158
+ ```
159
+ fix: correct off-by-one error [no-alibi]
160
+ ```
161
+
162
+ The marker string is configurable via `[markers] skip` in the config file or
163
+ `--skip-markers` on the command line.
164
+
165
+ ### `restore`
166
+
167
+ Restores commit timestamps from the backup saved before a previous rewrite.
168
+
169
+ ```bash
170
+ git-alibi restore [OPTIONS] [REF]
171
+ ```
172
+
173
+ | Option | Description |
174
+ |--------|-------------|
175
+ | `--dry-run` | Preview what would be restored without applying |
176
+ | `-v, --verbose` | Show all commits, not just changed ones |
177
+ | `--last N` | Undo the Nth most recent rewrite (default: `1` = last) |
178
+ | `--rewrite ID` | Restore a specific rewrite by ID (see `history`) |
179
+ | `-f, --force` | Proceed even if signed commits would be rewritten |
180
+
181
+ ```bash
182
+ git-alibi restore # undo the last rewrite
183
+ git-alibi restore --last 2 # undo the second-to-last rewrite
184
+ git-alibi restore --rewrite 3 # restore to a specific snapshot ID
185
+ ```
186
+
187
+ ### `history`
188
+
189
+ Shows all recorded rewrites and the exact command to restore each one.
190
+
191
+ ```bash
192
+ git-alibi history [-v]
193
+ ```
194
+
195
+ ```
196
+ Rewrite history — 3 snapshots in .git/alibi/backup.json
197
+
198
+ ID WHEN COMMITS --last RESTORE
199
+ ───────────────────────────────────────────────────────
200
+ 1 2024-01-06 Sat 09:15:00 5 3 alibi restore --last 3
201
+ 2 2024-01-07 Sun 14:30:00 3 2 alibi restore --last 2
202
+ 3 2024-01-08 Mon 11:15:00 2 1 alibi restore ← latest
203
+ ```
204
+
205
+ Use `-v` to also list the individual commits and their original timestamps.
206
+
207
+ ### `config`
208
+
209
+ Opens a config file in `$EDITOR`, creating it with commented-out defaults if it
210
+ doesn't exist yet.
211
+
212
+ ```bash
213
+ git-alibi config local # .git/alibi/config.toml
214
+ git-alibi config global # ~/.config/alibi/config.toml
215
+ ```
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,133 @@
1
+ """Backup of original commit timestamps, organised as rewrite snapshots.
2
+
3
+ The backup file lives at <git_dir>/alibi/backup.json.
4
+
5
+ Each call to ``save_rewrite`` appends one snapshot — capturing the before/after
6
+ SHA and pre-rewrite timestamps for every commit touched by that run. Multiple
7
+ snapshots allow restoring to any previous state via SHA chaining:
8
+
9
+ sha_after[n] == sha_before[n+1]
10
+
11
+ so walking backwards through snapshots traces a commit from its current SHA
12
+ all the way back to its original timestamps.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ from datetime import datetime, timezone
19
+ from pathlib import Path
20
+ from typing import TypedDict
21
+
22
+ import git
23
+
24
+
25
+ class RewriteCommit(TypedDict):
26
+ sha_before: str # SHA of the commit before this rewrite
27
+ sha_after: str # SHA of the commit after this rewrite
28
+ author_date: str # author timestamp before this rewrite
29
+ committer_date: str # committer timestamp before this rewrite
30
+
31
+
32
+ class RewriteSnapshot(TypedDict):
33
+ id: int
34
+ timestamp: str
35
+ commits: list[RewriteCommit]
36
+
37
+
38
+ def backup_path(repo: git.Repo) -> Path:
39
+ """Return the absolute path to the backup file for this repository.
40
+
41
+ Uses repo.common_dir when available (handles git worktrees correctly —
42
+ all worktrees share one backup in the main .git directory). Falls back
43
+ to repo.git_dir for older GitPython versions.
44
+ """
45
+ git_dir = Path(getattr(repo, "common_dir", None) or repo.git_dir)
46
+ return git_dir / "alibi" / "backup.json"
47
+
48
+
49
+ def _load(path: Path) -> dict:
50
+ if not path.exists():
51
+ return {}
52
+ try:
53
+ return json.loads(path.read_text(encoding="utf-8"))
54
+ except Exception as exc:
55
+ raise RuntimeError(f"Failed to read backup file {path}: {exc}") from exc
56
+
57
+
58
+ def _write(path: Path, data: dict) -> None:
59
+ """Atomically write data to path (via .tmp → rename)."""
60
+ tmp = path.with_suffix(".json.tmp")
61
+ try:
62
+ path.parent.mkdir(parents=True, exist_ok=True)
63
+ tmp.write_text(json.dumps(data, indent=2, ensure_ascii=True), encoding="utf-8")
64
+ tmp.replace(path)
65
+ except OSError as exc:
66
+ raise RuntimeError(f"Cannot write backup file {path}: {exc}") from exc
67
+
68
+
69
+ def load_rewrites(repo: git.Repo) -> list[RewriteSnapshot]:
70
+ """Load all rewrite snapshots from the backup file.
71
+
72
+ Raises RuntimeError if no backup file exists or the format is unrecognised.
73
+ """
74
+ path = backup_path(repo)
75
+ if not path.exists():
76
+ raise RuntimeError(
77
+ f"No backup file found at {path}. "
78
+ "Run 'alibi rewrite' (without --no-backup) at least once to create one."
79
+ )
80
+ data = _load(path)
81
+ if "rewrites" not in data:
82
+ raise RuntimeError(
83
+ "Backup file is in an old format and cannot be used for restore. "
84
+ "Run 'alibi rewrite' again to create a new backup."
85
+ )
86
+ return data.get("rewrites", [])
87
+
88
+
89
+ def save_rewrite(repo: git.Repo, commits: list[RewriteCommit]) -> int:
90
+ """Append a new rewrite snapshot to the backup file and return its ID.
91
+
92
+ Each call records the before/after SHA and pre-rewrite timestamps for
93
+ every commit touched by one alibi rewrite run. Returns -1 and writes
94
+ nothing if commits is empty.
95
+ """
96
+ if not commits:
97
+ return -1
98
+
99
+ path = backup_path(repo)
100
+ data = _load(path)
101
+ now = datetime.now(timezone.utc).isoformat()
102
+
103
+ if not data or "rewrites" not in data:
104
+ data = {"created_at": now, "updated_at": now, "rewrites": []}
105
+
106
+ next_id = (data["rewrites"][-1]["id"] + 1) if data["rewrites"] else 1
107
+ data["rewrites"].append(
108
+ {
109
+ "id": next_id,
110
+ "timestamp": now,
111
+ "commits": commits,
112
+ }
113
+ )
114
+ data["updated_at"] = now
115
+
116
+ _write(path, data)
117
+ return next_id
118
+
119
+
120
+ def remove_rewrites_from(repo: git.Repo, from_id: int) -> None:
121
+ """Remove the snapshot with the given ID and all subsequent snapshots.
122
+
123
+ Called after a successful restore to prune stale history — snapshots
124
+ after the restore point no longer reflect any real repo state.
125
+ """
126
+ path = backup_path(repo)
127
+ data = _load(path)
128
+ if not data or "rewrites" not in data:
129
+ return
130
+
131
+ data["rewrites"] = [r for r in data["rewrites"] if r["id"] < from_id]
132
+ data["updated_at"] = datetime.now(timezone.utc).isoformat()
133
+ _write(path, data)