filter_branch.py 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. #!/usr/bin/env python3
  2. """Example of using filter-branch to rewrite commit history.
  3. This demonstrates how to use dulwich's filter-branch functionality to:
  4. - Change author/committer information
  5. - Modify commit messages
  6. - Apply custom filters
  7. The example shows both the high-level porcelain interface and the
  8. lower-level filter_branch module API.
  9. """
  10. import sys
  11. from dulwich import porcelain
  12. from dulwich.filter_branch import CommitFilter, filter_refs
  13. from dulwich.repo import Repo
  14. def example_change_author(repo_path):
  15. """Example: Change all commits to have a new author."""
  16. print("Changing author for all commits...")
  17. def new_author(old_author):
  18. # Change any commit by "Old Author" to "New Author"
  19. if b"Old Author" in old_author:
  20. return b"New Author <new@example.com>"
  21. return old_author
  22. result = porcelain.filter_branch(repo_path, "HEAD", filter_author=new_author)
  23. print(f"Rewrote {len(result)} commits")
  24. return result
  25. def example_prefix_messages(repo_path):
  26. """Example: Add a prefix to all commit messages."""
  27. print("Adding prefix to commit messages...")
  28. def add_prefix(message):
  29. return b"[PROJECT-123] " + message
  30. result = porcelain.filter_branch(repo_path, "HEAD", filter_message=add_prefix)
  31. print(f"Rewrote {len(result)} commits")
  32. return result
  33. def example_custom_filter(repo_path):
  34. """Example: Custom filter that changes multiple fields."""
  35. print("Applying custom filter...")
  36. def custom_filter(commit):
  37. # This filter:
  38. # - Standardizes author format
  39. # - Adds issue number to message if missing
  40. # - Updates committer to match author
  41. changes = {}
  42. # Standardize author format
  43. if b"<" not in commit.author:
  44. changes["author"] = commit.author + b" <unknown@example.com>"
  45. # Add issue number if missing
  46. if not commit.message.startswith(b"[") and not commit.message.startswith(
  47. b"Merge"
  48. ):
  49. changes["message"] = b"[LEGACY] " + commit.message
  50. # Make committer match author
  51. if commit.author != commit.committer:
  52. changes["committer"] = commit.author
  53. return changes if changes else None
  54. result = porcelain.filter_branch(repo_path, "HEAD", filter_fn=custom_filter)
  55. print(f"Rewrote {len(result)} commits")
  56. return result
  57. def example_low_level_api(repo_path):
  58. """Example: Using the low-level filter_branch module API."""
  59. print("Using low-level filter_branch API...")
  60. with Repo(repo_path) as repo:
  61. # Create a custom filter
  62. def transform_message(msg):
  63. # Add timestamp and uppercase first line
  64. lines = msg.split(b"\n")
  65. if lines:
  66. lines[0] = lines[0].upper()
  67. return b"[TRANSFORMED] " + b"\n".join(lines)
  68. # Create the commit filter
  69. commit_filter = CommitFilter(
  70. repo.object_store,
  71. filter_message=transform_message,
  72. filter_author=lambda a: b"Transformed Author <transformed@example.com>",
  73. )
  74. # Filter the master branch
  75. result = filter_refs(
  76. repo.refs,
  77. repo.object_store,
  78. [b"refs/heads/master"],
  79. commit_filter,
  80. keep_original=True,
  81. force=False,
  82. )
  83. print(f"Rewrote {len(result)} commits using low-level API")
  84. return result
  85. def main():
  86. if len(sys.argv) < 2:
  87. print("Usage: filter_branch.py <repo_path> [example]")
  88. print("Examples: change_author, prefix_messages, custom_filter, low_level")
  89. sys.exit(1)
  90. repo_path = sys.argv[1]
  91. example = sys.argv[2] if len(sys.argv) > 2 else "change_author"
  92. examples = {
  93. "change_author": example_change_author,
  94. "prefix_messages": example_prefix_messages,
  95. "custom_filter": example_custom_filter,
  96. "low_level": example_low_level_api,
  97. }
  98. if example not in examples:
  99. print(f"Unknown example: {example}")
  100. print(f"Available examples: {', '.join(examples.keys())}")
  101. sys.exit(1)
  102. try:
  103. examples[example](repo_path)
  104. print("Filter-branch completed successfully!")
  105. except Exception as e:
  106. print(f"Error: {e}")
  107. sys.exit(1)
  108. if __name__ == "__main__":
  109. main()