git-rewrite 0.5.0__py3-none-any.whl

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_rewrite/cli.py ADDED
@@ -0,0 +1,415 @@
1
+ # ────────────────────────────────────────────────────────────────────────────────────────
2
+ # cli.py
3
+ # ──────
4
+ #
5
+ # Main CLI for git-rewrite with subcommand dispatch.
6
+ #
7
+ # (c) 2026 Cyber Assessment Labs — MIT License; see LICENSE in the project root.
8
+ #
9
+ # Authors
10
+ # ───────
11
+ # bena (via Claude)
12
+ #
13
+ # Version History
14
+ # ───────────────
15
+ # Jan 2026 - Created
16
+ # ────────────────────────────────────────────────────────────────────────────────────────
17
+
18
+ # ────────────────────────────────────────────────────────────────────────────────────────
19
+ # Imports
20
+ # ────────────────────────────────────────────────────────────────────────────────────────
21
+
22
+ import sys
23
+ import traceback
24
+ from pathlib import Path
25
+ from .argbuilder import ArgsParser
26
+ from .argbuilder import Namespace
27
+ from .version import VERSION_STR
28
+
29
+ # ────────────────────────────────────────────────────────────────────────────────────────
30
+ # Descriptions
31
+ # ────────────────────────────────────────────────────────────────────────────────────────
32
+
33
+ SCAN_DESCRIPTION = """\
34
+ Scan a git repository for sensitive words without modifying anything.
35
+
36
+ This command streams through all commits and blobs using git fast-export,
37
+ searching for occurrences of words defined in your config file.
38
+ Use this to audit a repository before sanitising it.
39
+
40
+ The config file should be JSON with a "words" array:
41
+ {"words": ["secretword", "internalname"]}
42
+
43
+ Example:
44
+ git-rewrite scan -r ./my-repo -c dirty-words.json
45
+ git-rewrite scan -r ./my-repo -c dirty-words.json -v # verbose
46
+ """
47
+
48
+ SANITISE_DESCRIPTION = """\
49
+ Rewrite git history to remove or replace sensitive words.
50
+
51
+ Creates a NEW repository with sanitised history. The original is never modified.
52
+ Uses git fast-export/fast-import for efficient streaming.
53
+
54
+ Features:
55
+ - Replace words in file contents, commit messages, author/committer names
56
+ - Map specific words to specific replacements (or use default "REDACTED")
57
+ - Map author names to specific email addresses
58
+ - Exclude specific files from the output (e.g., lock files)
59
+ - Remap submodule commit references if you've sanitised submodules too
60
+
61
+ Config file (JSON):
62
+ - words: list of words to find/replace
63
+ - word_mapping: dict mapping words to replacements
64
+ - email_mapping: dict mapping author names to emails
65
+ - exclude_files: list of files to exclude
66
+
67
+ Example:
68
+ git-rewrite sanitise -r ./dirty-repo -o ./clean-repo -c config.json
69
+ git-rewrite sanitise --sample-config # print example config
70
+ """
71
+
72
+ FLATTEN_DESCRIPTION = """\
73
+ Flatten submodules by inlining their contents into the main repository.
74
+
75
+ Creates a NEW repository where submodule references (gitlinks) are replaced
76
+ with the actual file contents from those submodules at each commit.
77
+ Useful for merging submodule history into a monorepo.
78
+
79
+ The command will:
80
+ 1. Scan history to find all submodule paths and URLs
81
+ 2. Clone or use existing checkouts of submodule repositories
82
+ 3. Stream history, replacing gitlinks with actual file trees
83
+ 4. Write a commit mapping file (old SHA -> new SHA)
84
+
85
+ Use --scan-only to just list submodules without flattening.
86
+
87
+ Example:
88
+ git-rewrite flatten -r ./repo-with-submodules -o ./flat-repo
89
+ git-rewrite flatten -r ./repo --scan-only # list submodules
90
+ """
91
+
92
+ REMAP_DESCRIPTION = """\
93
+ Remap submodule commit references using a mapping file.
94
+
95
+ When you sanitise or rewrite a submodule repository, its commit SHAs change.
96
+ If you have a parent repository that references those submodules, the gitlink
97
+ references become invalid. This command updates those references.
98
+
99
+ The mapping file format is: old_sha new_sha (one per line)
100
+ This is the output from a previous sanitise operation.
101
+
102
+ Optionally rewrite submodule URLs in .gitmodules with --url-rewrite.
103
+
104
+ Example:
105
+ git-rewrite remap -r ./parent -o ./remapped -m mapping.txt
106
+ git-rewrite remap -r ./repo -o ./out -m map.txt \\
107
+ --url-rewrite ../old-submodule.git ../new-submodule.git
108
+ """
109
+
110
+ COMPOSE_DESCRIPTION = """\
111
+ Compose multiple commit mapping files into one.
112
+
113
+ When chaining operations (e.g., flatten -> sanitise), each produces a mapping
114
+ file. This command composes them: given A->B and B->C mappings, produces A->C.
115
+
116
+ Mapping files are processed in order, following the chain of transformations.
117
+ The format is: old_sha new_sha (one per line).
118
+
119
+ Example:
120
+ git-rewrite compose -m flatten.txt -m sanitise.txt -o combined.txt
121
+ git-rewrite compose -m a.txt -m b.txt -m c.txt -o final.txt
122
+ """
123
+
124
+ # ────────────────────────────────────────────────────────────────────────────────────────
125
+ # Argument Parsing
126
+ # ────────────────────────────────────────────────────────────────────────────────────────
127
+
128
+
129
+ # ────────────────────────────────────────────────────────────────────────────────────────
130
+ def create_parser() -> ArgsParser:
131
+ """Create the main argument parser with subcommands."""
132
+ parser = ArgsParser(
133
+ prog="git-rewrite",
134
+ description=(
135
+ "Git history rewriting tools for sanitising, flattening, and remapping."
136
+ ),
137
+ version=f"git-rewrite {VERSION_STR}",
138
+ )
139
+
140
+ # =========== Common Options ===========
141
+
142
+ common = parser.create_common_collection()
143
+ common.add_argument(
144
+ "-v",
145
+ "--verbose",
146
+ action="store_true",
147
+ help="Show verbose output",
148
+ )
149
+
150
+ # =========== Scan Command ===========
151
+
152
+ scan_cmd = parser.add_command(
153
+ "scan",
154
+ help="Scan repository for sensitive words (read-only)",
155
+ description=SCAN_DESCRIPTION,
156
+ )
157
+ scan_cmd.add_argument(
158
+ "-r",
159
+ "--repo",
160
+ type=Path,
161
+ required=True,
162
+ help="Path to the git repository",
163
+ )
164
+ scan_cmd.add_argument(
165
+ "-c",
166
+ "--config",
167
+ type=Path,
168
+ required=True,
169
+ help="Path to JSON config file",
170
+ )
171
+
172
+ # =========== Sanitise Command ===========
173
+
174
+ sanitise_cmd = parser.add_command(
175
+ "sanitise",
176
+ help="Rewrite history to remove sensitive words",
177
+ description=SANITISE_DESCRIPTION,
178
+ )
179
+ sanitise_cmd.add_argument(
180
+ "--sample-config",
181
+ action="store_true",
182
+ help="Print sample config and exit",
183
+ )
184
+ sanitise_cmd.add_argument(
185
+ "-r",
186
+ "--repo",
187
+ type=Path,
188
+ help="Path to source repository",
189
+ )
190
+ sanitise_cmd.add_argument(
191
+ "-o",
192
+ "--output",
193
+ type=Path,
194
+ help="Path for output repository",
195
+ )
196
+ sanitise_cmd.add_argument(
197
+ "-c",
198
+ "--config",
199
+ type=Path,
200
+ help="Path to JSON config file",
201
+ )
202
+ sanitise_cmd.add_argument(
203
+ "--delete",
204
+ action="store_true",
205
+ help="Delete output directory if exists",
206
+ )
207
+ sanitise_cmd.add_argument(
208
+ "-d",
209
+ "--default",
210
+ default="REDACTED",
211
+ help="Default replacement word (default: REDACTED)",
212
+ )
213
+ sanitise_cmd.add_argument(
214
+ "-m",
215
+ "--commit-map",
216
+ type=Path,
217
+ help="Path to write commit mapping file",
218
+ )
219
+ sanitise_cmd.add_argument(
220
+ "-s",
221
+ "--submodule-map",
222
+ type=Path,
223
+ help="Path to submodule commit mapping file",
224
+ )
225
+ sanitise_cmd.add_argument(
226
+ "--copy-remotes",
227
+ action="store_true",
228
+ help="Copy git remotes from source to output repository",
229
+ )
230
+
231
+ # =========== Flatten Command ===========
232
+
233
+ flatten_cmd = parser.add_command(
234
+ "flatten",
235
+ help="Flatten submodules into main repository",
236
+ description=FLATTEN_DESCRIPTION,
237
+ )
238
+ flatten_cmd.add_argument(
239
+ "-r",
240
+ "--repo",
241
+ type=Path,
242
+ required=True,
243
+ help="Path to source repository",
244
+ )
245
+ flatten_cmd.add_argument(
246
+ "-o",
247
+ "--output",
248
+ type=Path,
249
+ help="Path for output repository",
250
+ )
251
+ flatten_cmd.add_argument(
252
+ "-m",
253
+ "--commit-map",
254
+ type=Path,
255
+ help="Path to write commit mapping file",
256
+ )
257
+ flatten_cmd.add_argument(
258
+ "--delete",
259
+ action="store_true",
260
+ help="Delete output directory if exists",
261
+ )
262
+ flatten_cmd.add_argument(
263
+ "--scan-only",
264
+ action="store_true",
265
+ help="Only scan and list submodules",
266
+ )
267
+ flatten_cmd.add_argument(
268
+ "--copy-remotes",
269
+ action="store_true",
270
+ help="Copy git remotes from source to output repository",
271
+ )
272
+
273
+ # =========== Remap Command ===========
274
+
275
+ remap_cmd = parser.add_command(
276
+ "remap",
277
+ help="Remap submodule commit references",
278
+ description=REMAP_DESCRIPTION,
279
+ )
280
+ remap_cmd.add_argument(
281
+ "-r",
282
+ "--repo",
283
+ type=Path,
284
+ required=True,
285
+ help="Path to source repository",
286
+ )
287
+ remap_cmd.add_argument(
288
+ "-o",
289
+ "--output",
290
+ type=Path,
291
+ required=True,
292
+ help="Path for output repository",
293
+ )
294
+ remap_cmd.add_argument(
295
+ "-m",
296
+ "--commit-map",
297
+ type=Path,
298
+ required=True,
299
+ help="Path to commit mapping file (old_sha new_sha per line)",
300
+ )
301
+ remap_cmd.add_argument(
302
+ "--submodule-path",
303
+ type=str,
304
+ help="Specific submodule path to remap (for gitlink filtering)",
305
+ )
306
+ remap_cmd.add_argument(
307
+ "--url-rewrite",
308
+ nargs=2,
309
+ action="append",
310
+ metavar=("OLD_URL", "NEW_URL"),
311
+ help="Rewrite URL in .gitmodules (can be specified multiple times)",
312
+ )
313
+ remap_cmd.add_argument(
314
+ "--delete",
315
+ action="store_true",
316
+ help="Delete output directory if exists",
317
+ )
318
+ remap_cmd.add_argument(
319
+ "--copy-remotes",
320
+ action="store_true",
321
+ help="Copy git remotes from source to output repository",
322
+ )
323
+
324
+ # =========== Compose Command ===========
325
+
326
+ compose_cmd = parser.add_command(
327
+ "compose",
328
+ help="Compose multiple commit mapping files",
329
+ description=COMPOSE_DESCRIPTION,
330
+ )
331
+ compose_cmd.add_argument(
332
+ "-m",
333
+ "--mapping",
334
+ type=Path,
335
+ action="append",
336
+ dest="mappings",
337
+ required=True,
338
+ help="Mapping file to include (can be specified multiple times, in order)",
339
+ )
340
+ compose_cmd.add_argument(
341
+ "-o",
342
+ "--output",
343
+ type=Path,
344
+ required=True,
345
+ help="Path for output mapping file",
346
+ )
347
+
348
+ return parser
349
+
350
+
351
+ # ────────────────────────────────────────────────────────────────────────────────────────
352
+ # Main Entry Point
353
+ # ────────────────────────────────────────────────────────────────────────────────────────
354
+
355
+
356
+ # ────────────────────────────────────────────────────────────────────────────────────────
357
+ def main(argv: list[str] | None = None) -> int:
358
+ """
359
+ Main entry point for git-rewrite CLI.
360
+
361
+ Parameters:
362
+ argv: Command line arguments (without program name). If None, uses sys.argv[1:].
363
+
364
+ Returns:
365
+ Exit code (0 for success, non-zero for error)
366
+ """
367
+ if argv is None:
368
+ argv = sys.argv[1:]
369
+
370
+ try:
371
+ return _main_inner(argv)
372
+ except KeyboardInterrupt:
373
+ print()
374
+ print("---- Manually Terminated ----")
375
+ print()
376
+ return 1
377
+ except SystemExit:
378
+ raise
379
+ except BaseException as e:
380
+ t = "-----------------------------------------------------------------------------\n"
381
+ t += "UNHANDLED EXCEPTION OCCURRED!!\n"
382
+ t += "\n"
383
+ t += traceback.format_exc()
384
+ t += "\n"
385
+ t += f"EXCEPTION: {type(e)} {e}\n"
386
+ t += "-----------------------------------------------------------------------------\n"
387
+ t += "\n"
388
+ print(t, file=sys.stderr)
389
+ return 1
390
+
391
+
392
+ # ────────────────────────────────────────────────────────────────────────────────────────
393
+ def _main_inner(argv: list[str]) -> int:
394
+ """Inner main function that does the actual work."""
395
+ from . import compose
396
+ from . import flatten
397
+ from . import remap
398
+ from . import sanitise
399
+ from . import scan
400
+
401
+ parser = create_parser()
402
+ args: Namespace = parser.parse(argv)
403
+
404
+ if not args.command:
405
+ return 0 # Help was shown by ArgsParser
406
+
407
+ commands = {
408
+ "scan": scan.run,
409
+ "sanitise": sanitise.run,
410
+ "flatten": flatten.run,
411
+ "remap": remap.run,
412
+ "compose": compose.run,
413
+ }
414
+
415
+ return commands[args.command](args)
@@ -0,0 +1,103 @@
1
+ # ────────────────────────────────────────────────────────────────────────────────────────
2
+ # common/__init__.py
3
+ # ───────────────────
4
+ #
5
+ # Common utilities for git-rewrite.
6
+ #
7
+ # (c) 2026 Cyber Assessment Labs — MIT License; see LICENSE in the project root.
8
+ #
9
+ # Authors
10
+ # ───────
11
+ # bena (via Claude)
12
+ #
13
+ # Version History
14
+ # ───────────────
15
+ # Jan 2026 - Created
16
+ # ────────────────────────────────────────────────────────────────────────────────────────
17
+
18
+ from .config import BLOB_MODE
19
+ from .config import EXEC_MODE
20
+ from .config import GITLINK_MODE
21
+ from .config import TREE_MODE
22
+ from .config import CommitInfo
23
+ from .config import SubmoduleInfo
24
+ from .config import TreeEntry
25
+ from .repo import clone_repo
26
+ from .repo import commit_tree
27
+ from .repo import copy_remotes
28
+ from .repo import get_all_branches
29
+ from .repo import get_all_commits
30
+ from .repo import get_all_tags
31
+ from .repo import get_branch_commit
32
+ from .repo import get_commit_info
33
+ from .repo import get_gitmodules_at_commit
34
+ from .repo import get_remotes
35
+ from .repo import get_tag_commit
36
+ from .repo import get_tree_sha
37
+ from .repo import git
38
+ from .repo import git_bytes
39
+ from .repo import hash_object
40
+ from .repo import init_repo
41
+ from .repo import ls_tree
42
+ from .repo import mktree
43
+ from .repo import object_exists
44
+ from .repo import parse_gitmodules
45
+ from .repo import read_blob
46
+ from .repo import update_ref
47
+ from .stream import COMMIT_PATTERN
48
+ from .stream import D_PATTERN
49
+ from .stream import FROM_PATTERN
50
+ from .stream import M_PATTERN
51
+ from .stream import MARK_PATTERN
52
+ from .stream import MERGE_PATTERN
53
+ from .stream import checkout_default_branch
54
+ from .stream import create_stream_writer
55
+ from .stream import finalise_stream
56
+ from .stream import start_fast_export
57
+ from .stream import start_fast_import
58
+
59
+ __all__ = [
60
+ # config
61
+ "GITLINK_MODE",
62
+ "TREE_MODE",
63
+ "BLOB_MODE",
64
+ "EXEC_MODE",
65
+ "SubmoduleInfo",
66
+ "CommitInfo",
67
+ "TreeEntry",
68
+ # repo
69
+ "git",
70
+ "git_bytes",
71
+ "init_repo",
72
+ "clone_repo",
73
+ "get_all_branches",
74
+ "get_all_tags",
75
+ "get_tag_commit",
76
+ "get_all_commits",
77
+ "get_commit_info",
78
+ "get_branch_commit",
79
+ "ls_tree",
80
+ "read_blob",
81
+ "get_tree_sha",
82
+ "mktree",
83
+ "hash_object",
84
+ "commit_tree",
85
+ "update_ref",
86
+ "get_gitmodules_at_commit",
87
+ "parse_gitmodules",
88
+ "object_exists",
89
+ "get_remotes",
90
+ "copy_remotes",
91
+ # stream
92
+ "MARK_PATTERN",
93
+ "COMMIT_PATTERN",
94
+ "FROM_PATTERN",
95
+ "MERGE_PATTERN",
96
+ "M_PATTERN",
97
+ "D_PATTERN",
98
+ "start_fast_export",
99
+ "start_fast_import",
100
+ "create_stream_writer",
101
+ "finalise_stream",
102
+ "checkout_default_branch",
103
+ ]
@@ -0,0 +1,65 @@
1
+ # ────────────────────────────────────────────────────────────────────────────────────────
2
+ # config.py
3
+ # ─────────
4
+ #
5
+ # Shared configuration types and constants for git-rewrite.
6
+ #
7
+ # (c) 2026 Cyber Assessment Labs — MIT License; see LICENSE in the project root.
8
+ #
9
+ # Authors
10
+ # ───────
11
+ # bena (via Claude)
12
+ #
13
+ # Version History
14
+ # ───────────────
15
+ # Jan 2026 - Created
16
+ # ────────────────────────────────────────────────────────────────────────────────────────
17
+
18
+ # ────────────────────────────────────────────────────────────────────────────────────────
19
+ # Imports
20
+ # ────────────────────────────────────────────────────────────────────────────────────────
21
+
22
+ from typing import TypedDict
23
+
24
+ # ────────────────────────────────────────────────────────────────────────────────────────
25
+ # Constants
26
+ # ────────────────────────────────────────────────────────────────────────────────────────
27
+
28
+ GITLINK_MODE = "160000"
29
+ TREE_MODE = "040000"
30
+ BLOB_MODE = "100644"
31
+ EXEC_MODE = "100755"
32
+
33
+ # ────────────────────────────────────────────────────────────────────────────────────────
34
+ # Types
35
+ # ────────────────────────────────────────────────────────────────────────────────────────
36
+
37
+
38
+ class SubmoduleInfo(TypedDict):
39
+ """Information about a submodule from .gitmodules."""
40
+
41
+ path: str
42
+ url: str
43
+
44
+
45
+ class CommitInfo(TypedDict):
46
+ """Information about a git commit."""
47
+
48
+ tree: str
49
+ parents: list[str]
50
+ author_name: str
51
+ author_email: str
52
+ author_date: str
53
+ committer_name: str
54
+ committer_email: str
55
+ committer_date: str
56
+ message: str
57
+
58
+
59
+ class TreeEntry(TypedDict):
60
+ """A single entry in a git tree."""
61
+
62
+ mode: str
63
+ type: str
64
+ sha: str
65
+ name: str