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
910import sys
1011import time
1112
12- from dulwich .objects import Commit , Tree
13+ from dulwich .objects import Commit , Tag , Tree
1314from dulwich .repo import Repo
1415
15- BANNED_NAMES = [".git" ]
16+ BANNED_NAMES = [b ".git" ]
1617
1718
1819def 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
290417if __name__ == "__main__" :
0 commit comments