Skip to content

Commit 1c196b6

Browse files
authored
fix-history: Add --update-tags (#2057)
2 parents 7b9abf6 + 41ab316 commit 1c196b6

File tree

1 file changed

+143
-16
lines changed

1 file changed

+143
-16
lines changed

devscripts/fix-history.py

Lines changed: 143 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,18 @@
22

33
"""Fix dulwich history by removing .git directories and updating old timestamps.
44
5-
Usage: ./fix-history.py <source-branch> <target-branch>
6-
Example: ./fix-history.py master main
5+
Usage: ./fix-history.py <source-branch> <target-branch> [--update-tags]
6+
Example: ./fix-history.py master main --update-tags
77
"""
88

9+
import argparse
910
import sys
1011
import time
1112

12-
from dulwich.objects import Commit, Tree
13+
from dulwich.objects import Commit, Tag, Tree
1314
from dulwich.repo import Repo
1415

15-
BANNED_NAMES = [".git"]
16+
BANNED_NAMES = [b".git"]
1617

1718

1819
def fix_tree(repo, tree_id, seen_trees=None):
@@ -184,21 +185,124 @@ def rewrite_history(repo, source_branch, target_branch):
184185
print(
185186
f"✓ Created branch '{target_branch}' with {len([k for k, v in commit_map.items() if k != v])} modified commits"
186187
)
187-
return True
188+
return commit_map
188189

189190

190-
def main():
191-
if len(sys.argv) != 3:
192-
print(f"Usage: {sys.argv[0]} <source-branch> <target-branch>")
193-
print(f"Example: {sys.argv[0]} master main")
194-
print("")
191+
def update_tags(repo, commit_map):
192+
"""Update tags to point to rewritten commits."""
193+
print("")
194+
print("=== Updating tags ===")
195+
196+
updated_tags = []
197+
skipped_tags = []
198+
199+
# Iterate through all refs looking for tags
200+
for ref_name, ref_value in list(repo.refs.as_dict().items()):
201+
if not ref_name.startswith(b"refs/tags/"):
202+
continue
203+
204+
tag_name = ref_name[len(b"refs/tags/") :].decode()
205+
206+
# Try to get the tag object
207+
try:
208+
tag_obj = repo[ref_value]
209+
except KeyError:
210+
print(f"Warning: Could not find object for tag '{tag_name}'")
211+
continue
212+
213+
# Handle annotated tags (Tag objects)
214+
if isinstance(tag_obj, Tag):
215+
# Get the commit that the tag points to
216+
target_sha = tag_obj.object[1]
217+
218+
if target_sha in commit_map:
219+
new_target_sha = commit_map[target_sha]
220+
221+
if new_target_sha != target_sha:
222+
# Create a new tag object pointing to the rewritten commit
223+
new_tag = Tag()
224+
new_tag.name = tag_obj.name
225+
new_tag.object = (tag_obj.object[0], new_target_sha)
226+
new_tag.tag_time = tag_obj.tag_time
227+
new_tag.tag_timezone = tag_obj.tag_timezone
228+
new_tag.tagger = tag_obj.tagger
229+
new_tag.message = tag_obj.message
230+
231+
# Add the new tag object to the object store
232+
repo.object_store.add_object(new_tag)
233+
234+
# Update the ref to point to the new tag object
235+
repo.refs[ref_name] = new_tag.id
236+
237+
print(
238+
f"Updated annotated tag '{tag_name}': {target_sha.decode()[:8]} -> {new_target_sha.decode()[:8]}"
239+
)
240+
updated_tags.append(tag_name)
241+
else:
242+
skipped_tags.append(tag_name)
243+
else:
244+
print(
245+
f"Warning: Tag '{tag_name}' points to commit not in history, skipping"
246+
)
247+
skipped_tags.append(tag_name)
248+
249+
# Handle lightweight tags (direct references to commits)
250+
elif isinstance(tag_obj, Commit):
251+
commit_sha = ref_value
252+
253+
if commit_sha in commit_map:
254+
new_commit_sha = commit_map[commit_sha]
255+
256+
if new_commit_sha != commit_sha:
257+
# Update the ref to point to the new commit
258+
repo.refs[ref_name] = new_commit_sha
259+
260+
print(
261+
f"Updated lightweight tag '{tag_name}': {commit_sha.decode()[:8]} -> {new_commit_sha.decode()[:8]}"
262+
)
263+
updated_tags.append(tag_name)
264+
else:
265+
skipped_tags.append(tag_name)
266+
else:
267+
print(
268+
f"Warning: Tag '{tag_name}' points to commit not in history, skipping"
269+
)
270+
skipped_tags.append(tag_name)
271+
else:
272+
print(
273+
f"Warning: Tag '{tag_name}' points to non-commit/non-tag object, skipping"
274+
)
275+
skipped_tags.append(tag_name)
276+
277+
print(f"✓ Updated {len(updated_tags)} tags")
278+
if skipped_tags:
195279
print(
196-
"This will create a new branch <target-branch> with the rewritten history from <source-branch>"
280+
f" Skipped {len(skipped_tags)} tags (unchanged or not in rewritten history)"
197281
)
198-
sys.exit(1)
199282

200-
source_branch = sys.argv[1]
201-
target_branch = sys.argv[2]
283+
return updated_tags
284+
285+
286+
def main():
287+
parser = argparse.ArgumentParser(
288+
description="Fix dulwich history by removing .git directories and updating old timestamps.",
289+
epilog="This will create a new branch <target-branch> with the rewritten history from <source-branch>",
290+
)
291+
parser.add_argument("source_branch", help="Source branch to rewrite from")
292+
parser.add_argument(
293+
"target_branch", help="Target branch to create with rewritten history"
294+
)
295+
parser.add_argument(
296+
"--update-tags",
297+
action="store_true",
298+
help="Update existing tags to point to rewritten commits",
299+
)
300+
301+
args = parser.parse_args()
302+
303+
source_branch = args.source_branch
304+
target_branch = args.target_branch
305+
update_tags_flag = args.update_tags
202306

203307
print("=== Dulwich History Fix Script ===")
204308
print("This script will:")
@@ -207,9 +311,13 @@ def main():
207311
print(
208312
f"3. Create new branch '{target_branch}' from '{source_branch}' with fixed history"
209313
)
314+
if update_tags_flag:
315+
print("4. Update existing tags to point to rewritten commits")
210316
print("")
211317
print(f"Source branch: {source_branch}")
212318
print(f"Target branch: {target_branch}")
319+
if update_tags_flag:
320+
print("Update tags: Yes")
213321
print("")
214322

215323
# Open the repository
@@ -260,9 +368,14 @@ def main():
260368

261369
# Rewrite history
262370
print("")
263-
if not rewrite_history(repo, source_branch, target_branch):
371+
commit_map = rewrite_history(repo, source_branch, target_branch)
372+
if not commit_map:
264373
sys.exit(1)
265374

375+
# Update tags if requested
376+
if update_tags_flag:
377+
update_tags(repo, commit_map)
378+
266379
print("")
267380
print("=== Complete ===")
268381
print(
@@ -273,18 +386,32 @@ def main():
273386
print("- Removed .git directories from tree objects")
274387
print("- Fixed commit timestamps that were before 1990")
275388
print(f"- Created clean history in branch '{target_branch}'")
389+
if update_tags_flag:
390+
print("- Updated tags to point to rewritten commits")
276391
print("")
277392
print("IMPORTANT NEXT STEPS:")
278393
print(f"1. Review the changes: git log --oneline {target_branch}")
279394
print(
280395
f"2. Compare commit count: git rev-list --count {source_branch} vs git rev-list --count {target_branch}"
281396
)
282-
print("3. If satisfied, you can:")
397+
if update_tags_flag:
398+
print("3. Review updated tags: git tag -l")
399+
print(
400+
"3. If satisfied, you can:"
401+
if not update_tags_flag
402+
else "4. If satisfied, you can:"
403+
)
283404
print(f" - Push the new branch: git push origin {target_branch}")
405+
if update_tags_flag:
406+
print(" - Force push updated tags: git push origin --tags --force")
284407
print(" - Set it as default branch on GitHub/GitLab")
285408
print(f" - Update local checkout: git checkout {target_branch}")
286409
print("")
287410
print(f"The original branch '{source_branch}' remains unchanged.")
411+
if update_tags_flag:
412+
print(
413+
"WARNING: Tags have been updated. You may need to force push them to remote."
414+
)
288415

289416

290417
if __name__ == "__main__":

0 commit comments

Comments
 (0)