2
0

merge_driver.py 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. #!/usr/bin/python3
  2. # SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
  3. """Simple example demonstrating merge driver usage in dulwich.
  4. This example:
  5. 1. Creates a test repository with .gitattributes
  6. 2. Implements a JSON merge driver
  7. 3. Creates two branches with conflicting JSON changes
  8. 4. Merges one commit into another using the custom driver
  9. """
  10. import json
  11. import os
  12. from typing import Optional
  13. from dulwich import porcelain
  14. from dulwich.merge_drivers import get_merge_driver_registry
  15. from dulwich.repo import Repo
  16. class JSONMergeDriver:
  17. """Simple merge driver for JSON files."""
  18. def merge(
  19. self,
  20. ancestor: bytes,
  21. ours: bytes,
  22. theirs: bytes,
  23. path: Optional[str] = None,
  24. marker_size: int = 7,
  25. ) -> tuple[bytes, bool]:
  26. """Merge JSON files by combining objects."""
  27. try:
  28. # Parse JSON content
  29. ancestor_data = json.loads(ancestor.decode()) if ancestor.strip() else {}
  30. ours_data = json.loads(ours.decode()) if ours.strip() else {}
  31. theirs_data = json.loads(theirs.decode()) if theirs.strip() else {}
  32. # Simple merge: combine all fields
  33. merged: dict = {}
  34. merged.update(ancestor_data)
  35. merged.update(ours_data)
  36. merged.update(theirs_data)
  37. # Convert back to JSON with nice formatting
  38. result = json.dumps(merged, indent=2).encode()
  39. return result, True
  40. except (json.JSONDecodeError, UnicodeDecodeError):
  41. # Fall back to simple concatenation on parse error
  42. result = ours + b"\n<<<<<<< MERGE CONFLICT >>>>>>>\n" + theirs
  43. return result, False
  44. # Create temporary directory for test repo
  45. # Initialize repository
  46. repo = Repo.init("merge-driver", mkdir=True)
  47. # Create .gitattributes file
  48. gitattributes_path = os.path.join(repo.path, ".gitattributes")
  49. with open(gitattributes_path, "w") as f:
  50. f.write("*.json merge=jsondriver\n")
  51. # Create initial JSON file
  52. config_path = os.path.join(repo.path, "config.json")
  53. initial_config = {"name": "test-project", "version": "1.0.0"}
  54. with open(config_path, "w") as f:
  55. json.dump(initial_config, f, indent=2)
  56. # Add and commit initial files
  57. repo.get_worktree().stage([".gitattributes", "config.json"])
  58. initial_commit = repo.get_worktree().commit(
  59. b"Initial commit", committer=b"Test <test@example.com>"
  60. )
  61. # Register our custom merge driver globally
  62. registry = get_merge_driver_registry()
  63. registry.register_driver("jsondriver", JSONMergeDriver())
  64. # Create and switch to feature branch
  65. porcelain.branch_create(repo, "feature")
  66. repo.refs[b"HEAD"] = repo.refs[b"refs/heads/feature"]
  67. # Make changes on feature branch
  68. feature_config = {
  69. "name": "test-project",
  70. "version": "1.0.0",
  71. "author": "Alice",
  72. "features": ["logging", "database"],
  73. }
  74. with open(config_path, "w") as f:
  75. json.dump(feature_config, f, indent=2)
  76. repo.get_worktree().stage(["config.json"])
  77. feature_commit = repo.get_worktree().commit(
  78. b"Add author and features", committer=b"Alice <alice@example.com>"
  79. )
  80. # Switch back to master
  81. repo.refs[b"HEAD"] = repo.refs[b"refs/heads/master"]
  82. # Make different changes on master
  83. master_config = {
  84. "name": "test-project",
  85. "version": "1.1.0",
  86. "description": "A test project for merge drivers",
  87. "license": "Apache-2.0",
  88. }
  89. with open(config_path, "w") as f:
  90. json.dump(master_config, f, indent=2)
  91. repo.get_worktree().stage(["config.json"])
  92. master_commit = repo.get_worktree().commit(
  93. b"Add description and license", committer=b"Bob <bob@example.com>"
  94. )
  95. # Perform the merge using porcelain.merge
  96. # The merge should use our custom JSON driver for config.json
  97. merge_result = porcelain.merge(repo, "feature")
  98. # Show the merged content
  99. with open(config_path) as f:
  100. merged_content = f.read()
  101. print("\nMerged config.json content:")
  102. print(merged_content)