filter_branch.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  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(
  23. repo_path,
  24. "HEAD",
  25. filter_author=new_author
  26. )
  27. print(f"Rewrote {len(result)} commits")
  28. return result
  29. def example_prefix_messages(repo_path):
  30. """Example: Add a prefix to all commit messages."""
  31. print("Adding prefix to commit messages...")
  32. def add_prefix(message):
  33. return b"[PROJECT-123] " + message
  34. result = porcelain.filter_branch(
  35. repo_path,
  36. "HEAD",
  37. filter_message=add_prefix
  38. )
  39. print(f"Rewrote {len(result)} commits")
  40. return result
  41. def example_custom_filter(repo_path):
  42. """Example: Custom filter that changes multiple fields."""
  43. print("Applying custom filter...")
  44. def custom_filter(commit):
  45. # This filter:
  46. # - Standardizes author format
  47. # - Adds issue number to message if missing
  48. # - Updates committer to match author
  49. changes = {}
  50. # Standardize author format
  51. if b"<" not in commit.author:
  52. changes["author"] = commit.author + b" <unknown@example.com>"
  53. # Add issue number if missing
  54. if not commit.message.startswith(b"[") and not commit.message.startswith(b"Merge"):
  55. changes["message"] = b"[LEGACY] " + commit.message
  56. # Make committer match author
  57. if commit.author != commit.committer:
  58. changes["committer"] = commit.author
  59. return changes if changes else None
  60. result = porcelain.filter_branch(
  61. repo_path,
  62. "HEAD",
  63. filter_fn=custom_filter
  64. )
  65. print(f"Rewrote {len(result)} commits")
  66. return result
  67. def example_low_level_api(repo_path):
  68. """Example: Using the low-level filter_branch module API."""
  69. print("Using low-level filter_branch API...")
  70. with Repo(repo_path) as repo:
  71. # Create a custom filter
  72. def transform_message(msg):
  73. # Add timestamp and uppercase first line
  74. lines = msg.split(b'\n')
  75. if lines:
  76. lines[0] = lines[0].upper()
  77. return b'[TRANSFORMED] ' + b'\n'.join(lines)
  78. # Create the commit filter
  79. commit_filter = CommitFilter(
  80. repo.object_store,
  81. filter_message=transform_message,
  82. filter_author=lambda a: b"Transformed Author <transformed@example.com>"
  83. )
  84. # Filter the master branch
  85. result = filter_refs(
  86. repo.refs,
  87. repo.object_store,
  88. [b"refs/heads/master"],
  89. commit_filter,
  90. keep_original=True,
  91. force=False,
  92. )
  93. print(f"Rewrote {len(result)} commits using low-level API")
  94. return result
  95. def main():
  96. if len(sys.argv) < 2:
  97. print("Usage: filter_branch.py <repo_path> [example]")
  98. print("Examples: change_author, prefix_messages, custom_filter, low_level")
  99. sys.exit(1)
  100. repo_path = sys.argv[1]
  101. example = sys.argv[2] if len(sys.argv) > 2 else "change_author"
  102. examples = {
  103. "change_author": example_change_author,
  104. "prefix_messages": example_prefix_messages,
  105. "custom_filter": example_custom_filter,
  106. "low_level": example_low_level_api,
  107. }
  108. if example not in examples:
  109. print(f"Unknown example: {example}")
  110. print(f"Available examples: {', '.join(examples.keys())}")
  111. sys.exit(1)
  112. try:
  113. examples[example](repo_path)
  114. print("Filter-branch completed successfully!")
  115. except Exception as e:
  116. print(f"Error: {e}")
  117. sys.exit(1)
  118. if __name__ == "__main__":
  119. main()