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.
- git_alibi-0.1.0/LICENSE +21 -0
- git_alibi-0.1.0/PKG-INFO +245 -0
- git_alibi-0.1.0/README.md +215 -0
- git_alibi-0.1.0/git_alibi/__init__.py +1 -0
- git_alibi-0.1.0/git_alibi/backup.py +133 -0
- git_alibi-0.1.0/git_alibi/cli.py +831 -0
- git_alibi-0.1.0/git_alibi/config.py +599 -0
- git_alibi-0.1.0/git_alibi/filter.py +379 -0
- git_alibi-0.1.0/git_alibi/rewrite.py +1204 -0
- git_alibi-0.1.0/git_alibi.egg-info/PKG-INFO +245 -0
- git_alibi-0.1.0/git_alibi.egg-info/SOURCES.txt +19 -0
- git_alibi-0.1.0/git_alibi.egg-info/dependency_links.txt +1 -0
- git_alibi-0.1.0/git_alibi.egg-info/entry_points.txt +2 -0
- git_alibi-0.1.0/git_alibi.egg-info/requires.txt +9 -0
- git_alibi-0.1.0/git_alibi.egg-info/top_level.txt +1 -0
- git_alibi-0.1.0/pyproject.toml +42 -0
- git_alibi-0.1.0/setup.cfg +4 -0
- git_alibi-0.1.0/tests/test_backup.py +405 -0
- git_alibi-0.1.0/tests/test_config.py +464 -0
- git_alibi-0.1.0/tests/test_filter.py +750 -0
- git_alibi-0.1.0/tests/test_rewrite.py +234 -0
git_alibi-0.1.0/LICENSE
ADDED
|
@@ -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.
|
git_alibi-0.1.0/PKG-INFO
ADDED
|
@@ -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)
|