Skip to content

Commit 04244fa

Browse files
feat(fields): add author-date and committer-date to FIELD_ATTR
* feat(fields): add author-date and committer-date to FIELD_ATTR - Extend FIELD_ATTR in ops.py with 'author-date' and 'committer-date' mapped to commit.author_date / commit.committer_date; strip/replace now accept --field author-date and --field committer-date automatically (choices derive from the dict keys). - Fix _get_field_value in cli.py: add %ad/%cd to the fmt_map and pass --date=raw for date fields so preview/match-counting reflects the raw b'<epoch> <tz>' format that git-filter-repo uses. - Add examples/normalize_timezone.py: rewrites all timezone offsets to UTC (+0000) while preserving the epoch second. - Update README --field table to list the two new fields and note they require git-filter-repo; add replace --field date examples. - Add tests in test_ops.py (strip/replace on author-date, committer-date) and test_cli.py (parser accepts --field author-date/committer-date). Closes #2 * fix(lint): resolve all 14 ruff errors (I001, E702, F401, F541, F841, E741) * refactor(fields): drop lint churn; add strip-date warning to README - Revert scope-creep from the fix(lint) commit: - cli.py: restore f-string on 'action' print, restore body variable, restore l loop variable in list comprehension - tests/test_cli.py: restore original import order (build_parser first) and blank line after import block - tests/test_ops.py: restore textwrap-before-tempfile import order and blank line after from-import - README: add blockquote note under strip section explaining that using --field author-date / --field committer-date with strip zeroes the field to an empty byte string (invalid date); replace is the right operation for date fields --------- Co-authored-by: gfargo-horizon-agent[bot] <294710345+gfargo-horizon-agent[bot]@users.noreply.github.com>
1 parent 4d2d03e commit 04244fa

6 files changed

Lines changed: 101 additions & 2 deletions

File tree

README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ git-rewrite strip --field author-email "old@example\.com"
8181
git-rewrite strip --invert "^[A-Z][a-z-]+: " --field message
8282
```
8383

84+
> **Note:** Using `strip` on a date field (`--field author-date` or `--field committer-date`) zeroes the
85+
> field to an empty byte string, producing an invalid date. Use `replace` instead to rewrite specific
86+
> parts of the date value while keeping it valid.
87+
8488
#### `replace` — substitute a pattern with a replacement
8589

8690
```bash
@@ -89,6 +93,11 @@ git-rewrite replace "Co-Authored-By: Claude Sonnet \d+\.\d+" "Co-Authored-By: AI
8993

9094
git-rewrite replace "Co-Authored-By: Claude Sonnet \d+\.\d+" "Co-Authored-By: AI"
9195
git-rewrite replace --field author-name "Old Name" "New Name"
96+
97+
# Normalize timezone offsets to UTC (requires git-filter-repo)
98+
# Date values are in raw format: "<unix-timestamp> <tz-offset>", e.g. "1700000000 -0700"
99+
git-rewrite replace --field author-date "[-+]\d{4}$" "+0000"
100+
git-rewrite replace --field committer-date "[-+]\d{4}$" "+0000"
92101
```
93102

94103
#### `run` — execute a custom Python callback
@@ -105,7 +114,7 @@ git-rewrite run my_callback.py --refs main feature/branch
105114
| `--dry-run` | Show what would happen without modifying history |
106115
| `--yes / -y` | Skip the confirmation prompt |
107116
| `--refs REF …` | Limit to specific refs (default: all) |
108-
| `--field FIELD` | Field to target: `message`, `author-name`, `author-email`, `committer-name`, `committer-email` |
117+
| `--field FIELD` | Field to target: `message`, `author-name`, `author-email`, `committer-name`, `committer-email`, `author-date`, `committer-date` (date fields require git-filter-repo) |
109118
| `--case-sensitive` | Disable case-insensitive matching |
110119
| `--preview` | (`strip`/`replace`) Diff-style preview of changes — no history rewritten |
111120
| `--invert` | (`strip`) Keep only matches; strip everything else |

examples/normalize_timezone.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""
2+
normalize_timezone.py — rewrite all commit timestamps to UTC (+0000).
3+
4+
git-filter-repo stores dates as bytes in the format ``b"<unix-epoch> <tz-offset>"``,
5+
e.g. ``b"1700000000 -0700"``. This script keeps the epoch second intact and
6+
replaces only the timezone offset with ``+0000``.
7+
8+
Run:
9+
git-rewrite run examples/normalize_timezone.py --dry-run
10+
git-rewrite run examples/normalize_timezone.py
11+
12+
Requires git-filter-repo (the ``run`` command always uses git-filter-repo).
13+
"""
14+
15+
16+
def _to_utc(date_bytes: bytes) -> bytes:
17+
"""Return *date_bytes* with the timezone offset replaced by +0000.
18+
19+
Input: b"1700000000 -0700"
20+
Output: b"1700000000 +0000"
21+
"""
22+
parts = date_bytes.split(b" ", 1)
23+
if len(parts) != 2:
24+
# Unexpected format — leave untouched rather than corrupting history.
25+
return date_bytes
26+
epoch = parts[0]
27+
return epoch + b" +0000"
28+
29+
30+
def process_commit(commit):
31+
commit.author_date = _to_utc(commit.author_date)
32+
commit.committer_date = _to_utc(commit.committer_date)

git_rewrite/cli.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,10 +113,19 @@ def _get_field_value(sha: str, field: str) -> str:
113113
"author-email": "%ae",
114114
"committer-name": "%cn",
115115
"committer-email": "%ce",
116+
"author-date": "%ad",
117+
"committer-date": "%cd",
116118
}
117119
fmt = fmt_map.get(field, "%B")
120+
# --date=raw produces "<unix-timestamp> <tz-offset>" which matches the
121+
# bytes format git-filter-repo uses (b"1234567890 +0000"). Only meaningful
122+
# for date fields, but harmless for others.
123+
is_date_field = field in ("author-date", "committer-date")
124+
cmd = ["git", "log", "-1", f"--format={fmt}", sha]
125+
if is_date_field:
126+
cmd.append("--date=raw")
118127
result = subprocess.run(
119-
["git", "log", "-1", f"--format={fmt}", sha],
128+
cmd,
120129
capture_output=True,
121130
text=True,
122131
)

git_rewrite/ops.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
"author-email": "commit.author_email",
1818
"committer-name": "commit.committer_name",
1919
"committer-email": "commit.committer_email",
20+
"author-date": "commit.author_date",
21+
"committer-date": "commit.committer_date",
2022
}
2123

2224

tests/test_cli.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,23 @@ def test_invalid_field_exits(self):
7878
with pytest.raises(SystemExit):
7979
self.parser.parse_args(["strip", "pat", "--field", "not-a-field"])
8080

81+
def test_strip_with_author_date_field(self):
82+
args = self.parser.parse_args(["strip", "pat", "--field", "author-date"])
83+
assert args.field == "author-date"
84+
85+
def test_strip_with_committer_date_field(self):
86+
args = self.parser.parse_args(["strip", "pat", "--field", "committer-date"])
87+
assert args.field == "committer-date"
88+
89+
def test_replace_with_author_date_field(self):
90+
args = self.parser.parse_args(["replace", r"[-+]\d{4}$", "+0000", "--field", "author-date"])
91+
assert args.field == "author-date"
92+
assert args.pattern == r"[-+]\d{4}$"
93+
assert args.replacement == "+0000"
94+
95+
def test_replace_with_committer_date_field(self):
96+
args = self.parser.parse_args(["replace", r"[-+]\d{4}$", "+0000", "--field", "committer-date"])
97+
assert args.field == "committer-date"
8198
def test_strip_scope_defaults_none(self):
8299
args = self.parser.parse_args(["strip", "pat"])
83100
assert args.since is None

tests/test_ops.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,24 @@ def test_leaves_author_email_on_no_match(self):
7777
run_callback(code, c)
7878
assert c.author_email == b"keep@example.com"
7979

80+
def test_zeros_author_date_on_match(self):
81+
code = ops.strip(r"[-+]\d{4}$", flags=0, field="author-date")
82+
c = FakeCommit(author_date=b"1700000000 -0700")
83+
run_callback(code, c)
84+
assert c.author_date == b""
85+
86+
def test_leaves_author_date_on_no_match(self):
87+
code = ops.strip(r"\+9999$", flags=0, field="author-date")
88+
c = FakeCommit(author_date=b"1700000000 +0000")
89+
run_callback(code, c)
90+
assert c.author_date == b"1700000000 +0000"
91+
92+
def test_zeros_committer_date_on_match(self):
93+
code = ops.strip(r"[-+]\d{4}$", flags=0, field="committer-date")
94+
c = FakeCommit(committer_date=b"1700000000 +0530")
95+
run_callback(code, c)
96+
assert c.committer_date == b""
97+
8098
def test_noop_on_empty_message(self):
8199
code = ops.strip("anything", flags=0, field="message")
82100
c = FakeCommit(message=b"")
@@ -135,6 +153,18 @@ def test_replaces_author_name_field(self):
135153
run_callback(code, c)
136154
assert c.author_name == b"New Name"
137155

156+
def test_replaces_author_date_timezone(self):
157+
code = ops.replace(r"[-+]\d{4}$", "+0000", flags=0, field="author-date")
158+
c = FakeCommit(author_date=b"1700000000 -0700")
159+
run_callback(code, c)
160+
assert c.author_date == b"1700000000 +0000"
161+
162+
def test_replaces_committer_date_timezone(self):
163+
code = ops.replace(r"[-+]\d{4}$", "+0000", flags=0, field="committer-date")
164+
c = FakeCommit(committer_date=b"1700000000 +0530")
165+
run_callback(code, c)
166+
assert c.committer_date == b"1700000000 +0000"
167+
138168
def test_noop_when_no_match(self):
139169
code = ops.replace("not-there", "replacement", flags=0, field="message")
140170
c = FakeCommit(message=b"unchanged\n")

0 commit comments

Comments
 (0)