merge.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497
  1. """Git merge implementation."""
  2. from typing import Optional
  3. try:
  4. import merge3
  5. except ImportError:
  6. merge3 = None # type: ignore
  7. from dulwich.attrs import GitAttributes
  8. from dulwich.config import Config
  9. from dulwich.merge_drivers import get_merge_driver_registry
  10. from dulwich.object_store import BaseObjectStore
  11. from dulwich.objects import S_ISGITLINK, Blob, Commit, Tree, is_blob, is_tree
  12. class MergeConflict(Exception):
  13. """Raised when a merge conflict occurs."""
  14. def __init__(self, path: bytes, message: str) -> None:
  15. self.path = path
  16. super().__init__(f"Merge conflict in {path!r}: {message}")
  17. def _can_merge_lines(
  18. base_lines: list[bytes], a_lines: list[bytes], b_lines: list[bytes]
  19. ) -> bool:
  20. """Check if lines can be merged without conflict."""
  21. # If one side is unchanged, we can take the other side
  22. if base_lines == a_lines:
  23. return True
  24. elif base_lines == b_lines:
  25. return True
  26. else:
  27. # For now, treat any difference as a conflict
  28. # A more sophisticated algorithm would check for non-overlapping changes
  29. return False
  30. if merge3 is not None:
  31. def _merge3_to_bytes(m: merge3.Merge3) -> bytes:
  32. """Convert merge3 result to bytes with conflict markers.
  33. Args:
  34. m: Merge3 object
  35. Returns:
  36. Merged content as bytes
  37. """
  38. result = []
  39. for group in m.merge_groups():
  40. if group[0] == "unchanged":
  41. result.extend(group[1])
  42. elif group[0] == "a":
  43. result.extend(group[1])
  44. elif group[0] == "b":
  45. result.extend(group[1])
  46. elif group[0] == "same":
  47. result.extend(group[1])
  48. elif group[0] == "conflict":
  49. # Check if this is a real conflict or just different changes
  50. base_lines, a_lines, b_lines = group[1], group[2], group[3]
  51. # Try to merge line by line
  52. if _can_merge_lines(base_lines, a_lines, b_lines):
  53. merged_lines = _merge_lines(base_lines, a_lines, b_lines)
  54. result.extend(merged_lines)
  55. else:
  56. # Real conflict - add conflict markers
  57. result.append(b"<<<<<<< ours\n")
  58. result.extend(a_lines)
  59. result.append(b"=======\n")
  60. result.extend(b_lines)
  61. result.append(b">>>>>>> theirs\n")
  62. return b"".join(result)
  63. def _merge_lines(
  64. base_lines: list[bytes], a_lines: list[bytes], b_lines: list[bytes]
  65. ) -> list[bytes]:
  66. """Merge lines when possible."""
  67. if base_lines == a_lines:
  68. return b_lines
  69. elif base_lines == b_lines:
  70. return a_lines
  71. else:
  72. # This shouldn't happen if _can_merge_lines returned True
  73. return a_lines
  74. def merge_blobs(
  75. base_blob: Optional[Blob],
  76. ours_blob: Optional[Blob],
  77. theirs_blob: Optional[Blob],
  78. path: Optional[bytes] = None,
  79. gitattributes: Optional[GitAttributes] = None,
  80. config: Optional[Config] = None,
  81. ) -> tuple[bytes, bool]:
  82. """Perform three-way merge on blob contents.
  83. Args:
  84. base_blob: Common ancestor blob (can be None)
  85. ours_blob: Our version of the blob (can be None)
  86. theirs_blob: Their version of the blob (can be None)
  87. path: Optional path of the file being merged
  88. gitattributes: Optional GitAttributes object for checking merge drivers
  89. config: Optional Config object for loading merge driver configuration
  90. Returns:
  91. Tuple of (merged_content, had_conflicts)
  92. """
  93. # Check for merge driver
  94. merge_driver_name = None
  95. if path and gitattributes:
  96. attrs = gitattributes.match_path(path)
  97. merge_value = attrs.get(b"merge")
  98. if merge_value and isinstance(merge_value, bytes) and merge_value != b"text":
  99. merge_driver_name = merge_value.decode("utf-8", errors="replace")
  100. # Use merge driver if found
  101. if merge_driver_name:
  102. registry = get_merge_driver_registry(config)
  103. driver = registry.get_driver(merge_driver_name)
  104. if driver:
  105. # Get content from blobs
  106. base_content = base_blob.data if base_blob else b""
  107. ours_content = ours_blob.data if ours_blob else b""
  108. theirs_content = theirs_blob.data if theirs_blob else b""
  109. # Use merge driver
  110. merged_content, success = driver.merge(
  111. ancestor=base_content,
  112. ours=ours_content,
  113. theirs=theirs_content,
  114. path=path.decode("utf-8", errors="replace") if path else None,
  115. marker_size=7,
  116. )
  117. # Convert success (no conflicts) to had_conflicts (conflicts occurred)
  118. had_conflicts = not success
  119. return merged_content, had_conflicts
  120. # Fall back to default merge behavior
  121. # Handle deletion cases
  122. if ours_blob is None and theirs_blob is None:
  123. return b"", False
  124. if base_blob is None:
  125. # No common ancestor
  126. if ours_blob is None:
  127. assert theirs_blob is not None
  128. return theirs_blob.data, False
  129. elif theirs_blob is None:
  130. return ours_blob.data, False
  131. elif ours_blob.data == theirs_blob.data:
  132. return ours_blob.data, False
  133. else:
  134. # Both added different content - conflict
  135. m = merge3.Merge3(
  136. [],
  137. ours_blob.data.splitlines(True),
  138. theirs_blob.data.splitlines(True),
  139. )
  140. return _merge3_to_bytes(m), True
  141. # Get content for each version
  142. base_content = base_blob.data if base_blob else b""
  143. ours_content = ours_blob.data if ours_blob else b""
  144. theirs_content = theirs_blob.data if theirs_blob else b""
  145. # Check if either side deleted
  146. if ours_blob is None or theirs_blob is None:
  147. if ours_blob is None and theirs_blob is None:
  148. return b"", False
  149. elif ours_blob is None:
  150. # We deleted, check if they modified
  151. if base_content == theirs_content:
  152. return b"", False # They didn't modify, accept deletion
  153. else:
  154. # Conflict: we deleted, they modified
  155. m = merge3.Merge3(
  156. base_content.splitlines(True),
  157. [],
  158. theirs_content.splitlines(True),
  159. )
  160. return _merge3_to_bytes(m), True
  161. else:
  162. # They deleted, check if we modified
  163. if base_content == ours_content:
  164. return b"", False # We didn't modify, accept deletion
  165. else:
  166. # Conflict: they deleted, we modified
  167. m = merge3.Merge3(
  168. base_content.splitlines(True),
  169. ours_content.splitlines(True),
  170. [],
  171. )
  172. return _merge3_to_bytes(m), True
  173. # Both sides exist, check if merge is needed
  174. if ours_content == theirs_content:
  175. return ours_content, False
  176. elif base_content == ours_content:
  177. return theirs_content, False
  178. elif base_content == theirs_content:
  179. return ours_content, False
  180. # Perform three-way merge
  181. m = merge3.Merge3(
  182. base_content.splitlines(True),
  183. ours_content.splitlines(True),
  184. theirs_content.splitlines(True),
  185. )
  186. # Check for conflicts and generate merged content
  187. merged_content = _merge3_to_bytes(m)
  188. has_conflicts = b"<<<<<<< ours" in merged_content
  189. return merged_content, has_conflicts
  190. class Merger:
  191. """Handles git merge operations."""
  192. def __init__(
  193. self,
  194. object_store: BaseObjectStore,
  195. gitattributes: Optional[GitAttributes] = None,
  196. config: Optional[Config] = None,
  197. ) -> None:
  198. """Initialize merger.
  199. Args:
  200. object_store: Object store to read objects from
  201. gitattributes: Optional GitAttributes object for checking merge drivers
  202. config: Optional Config object for loading merge driver configuration
  203. """
  204. self.object_store = object_store
  205. self.gitattributes = gitattributes
  206. self.config = config
  207. def merge_blobs(
  208. self,
  209. base_blob: Optional[Blob],
  210. ours_blob: Optional[Blob],
  211. theirs_blob: Optional[Blob],
  212. path: Optional[bytes] = None,
  213. ) -> tuple[bytes, bool]:
  214. """Perform three-way merge on blob contents.
  215. Args:
  216. base_blob: Common ancestor blob (can be None)
  217. ours_blob: Our version of the blob (can be None)
  218. theirs_blob: Their version of the blob (can be None)
  219. path: Optional path of the file being merged
  220. Returns:
  221. Tuple of (merged_content, had_conflicts)
  222. """
  223. return merge_blobs(
  224. base_blob, ours_blob, theirs_blob, path, self.gitattributes, self.config
  225. )
  226. def merge_trees(
  227. self, base_tree: Optional[Tree], ours_tree: Tree, theirs_tree: Tree
  228. ) -> tuple[Tree, list[bytes]]:
  229. """Perform three-way merge on trees.
  230. Args:
  231. base_tree: Common ancestor tree (can be None for no common ancestor)
  232. ours_tree: Our version of the tree
  233. theirs_tree: Their version of the tree
  234. Returns:
  235. tuple of (merged_tree, list_of_conflicted_paths)
  236. """
  237. conflicts = []
  238. merged_entries = {}
  239. # Get all paths from all trees
  240. all_paths = set()
  241. if base_tree:
  242. for entry in base_tree.items():
  243. all_paths.add(entry.path)
  244. for entry in ours_tree.items():
  245. all_paths.add(entry.path)
  246. for entry in theirs_tree.items():
  247. all_paths.add(entry.path)
  248. # Process each path
  249. for path in sorted(all_paths):
  250. base_entry = None
  251. if base_tree:
  252. try:
  253. base_entry = base_tree.lookup_path(
  254. self.object_store.__getitem__, path
  255. )
  256. except KeyError:
  257. pass
  258. try:
  259. ours_entry = ours_tree.lookup_path(self.object_store.__getitem__, path)
  260. except KeyError:
  261. ours_entry = None
  262. try:
  263. theirs_entry = theirs_tree.lookup_path(
  264. self.object_store.__getitem__, path
  265. )
  266. except KeyError:
  267. theirs_entry = None
  268. # Extract mode and sha
  269. base_mode, base_sha = base_entry if base_entry else (None, None)
  270. ours_mode, ours_sha = ours_entry if ours_entry else (None, None)
  271. theirs_mode, theirs_sha = theirs_entry if theirs_entry else (None, None)
  272. # Handle deletions
  273. if ours_sha is None and theirs_sha is None:
  274. continue # Deleted in both
  275. # Handle additions
  276. if base_sha is None:
  277. if ours_sha == theirs_sha and ours_mode == theirs_mode:
  278. # Same addition in both
  279. merged_entries[path] = (ours_mode, ours_sha)
  280. elif ours_sha is None:
  281. # Added only in theirs
  282. merged_entries[path] = (theirs_mode, theirs_sha)
  283. elif theirs_sha is None:
  284. # Added only in ours
  285. merged_entries[path] = (ours_mode, ours_sha)
  286. else:
  287. # Different additions - conflict
  288. conflicts.append(path)
  289. # For now, keep ours
  290. merged_entries[path] = (ours_mode, ours_sha)
  291. continue
  292. # Check for mode conflicts
  293. if (
  294. ours_mode != theirs_mode
  295. and ours_mode is not None
  296. and theirs_mode is not None
  297. ):
  298. conflicts.append(path)
  299. # For now, keep ours
  300. merged_entries[path] = (ours_mode, ours_sha)
  301. continue
  302. # Handle modifications
  303. if ours_sha == theirs_sha:
  304. # Same modification or no change
  305. if ours_sha is not None:
  306. merged_entries[path] = (ours_mode, ours_sha)
  307. elif base_sha == ours_sha and theirs_sha is not None:
  308. # Only theirs modified
  309. merged_entries[path] = (theirs_mode, theirs_sha)
  310. elif base_sha == theirs_sha and ours_sha is not None:
  311. # Only ours modified
  312. merged_entries[path] = (ours_mode, ours_sha)
  313. elif ours_sha is None:
  314. # We deleted
  315. if base_sha == theirs_sha:
  316. # They didn't modify, accept deletion
  317. pass
  318. else:
  319. # They modified, we deleted - conflict
  320. conflicts.append(path)
  321. elif theirs_sha is None:
  322. # They deleted
  323. if base_sha == ours_sha:
  324. # We didn't modify, accept deletion
  325. pass
  326. else:
  327. # We modified, they deleted - conflict
  328. conflicts.append(path)
  329. merged_entries[path] = (ours_mode, ours_sha)
  330. else:
  331. # Both modified differently
  332. # For trees and submodules, this is a conflict
  333. if S_ISGITLINK(ours_mode or 0) or S_ISGITLINK(theirs_mode or 0):
  334. conflicts.append(path)
  335. merged_entries[path] = (ours_mode, ours_sha)
  336. elif (ours_mode or 0) & 0o170000 == 0o040000 or (
  337. theirs_mode or 0
  338. ) & 0o170000 == 0o040000:
  339. # Tree conflict
  340. conflicts.append(path)
  341. merged_entries[path] = (ours_mode, ours_sha)
  342. else:
  343. # Try to merge blobs
  344. base_blob = None
  345. if base_sha:
  346. base_obj = self.object_store[base_sha]
  347. if is_blob(base_obj):
  348. base_blob = base_obj
  349. else:
  350. raise TypeError(
  351. f"Expected blob for {path!r}, got {base_obj.type_name.decode()}"
  352. )
  353. ours_blob = None
  354. if ours_sha:
  355. ours_obj = self.object_store[ours_sha]
  356. if is_blob(ours_obj):
  357. ours_blob = ours_obj
  358. else:
  359. raise TypeError(
  360. f"Expected blob for {path!r}, got {ours_obj.type_name.decode()}"
  361. )
  362. theirs_blob = None
  363. if theirs_sha:
  364. theirs_obj = self.object_store[theirs_sha]
  365. if is_blob(theirs_obj):
  366. theirs_blob = theirs_obj
  367. else:
  368. raise TypeError(
  369. f"Expected blob for {path!r}, got {theirs_obj.type_name.decode()}"
  370. )
  371. assert isinstance(base_blob, Blob)
  372. assert isinstance(ours_blob, Blob)
  373. assert isinstance(theirs_blob, Blob)
  374. merged_content, had_conflict = self.merge_blobs(
  375. base_blob, ours_blob, theirs_blob, path
  376. )
  377. if had_conflict:
  378. conflicts.append(path)
  379. # Store merged blob
  380. merged_blob = Blob.from_string(merged_content)
  381. self.object_store.add_object(merged_blob)
  382. merged_entries[path] = (ours_mode or theirs_mode, merged_blob.id)
  383. # Build merged tree
  384. merged_tree = Tree()
  385. for path, (mode, sha) in sorted(merged_entries.items()):
  386. if mode is not None and sha is not None:
  387. merged_tree.add(path, mode, sha)
  388. return merged_tree, conflicts
  389. def three_way_merge(
  390. object_store: BaseObjectStore,
  391. base_commit: Optional[Commit],
  392. ours_commit: Commit,
  393. theirs_commit: Commit,
  394. gitattributes: Optional[GitAttributes] = None,
  395. config: Optional[Config] = None,
  396. ) -> tuple[Tree, list[bytes]]:
  397. """Perform a three-way merge between commits.
  398. Args:
  399. object_store: Object store to read/write objects
  400. base_commit: Common ancestor commit (None if no common ancestor)
  401. ours_commit: Our commit
  402. theirs_commit: Their commit
  403. gitattributes: Optional GitAttributes object for checking merge drivers
  404. config: Optional Config object for loading merge driver configuration
  405. Returns:
  406. tuple of (merged_tree, list_of_conflicted_paths)
  407. """
  408. merger = Merger(object_store, gitattributes, config)
  409. base_tree = None
  410. if base_commit:
  411. base_obj = object_store[base_commit.tree]
  412. if is_tree(base_obj):
  413. base_tree = base_obj
  414. else:
  415. raise TypeError(f"Expected tree, got {base_obj.type_name.decode()}")
  416. ours_obj = object_store[ours_commit.tree]
  417. if is_tree(ours_obj):
  418. ours_tree = ours_obj
  419. else:
  420. raise TypeError(f"Expected tree, got {ours_obj.type_name.decode()}")
  421. theirs_obj = object_store[theirs_commit.tree]
  422. if is_tree(theirs_obj):
  423. theirs_tree = theirs_obj
  424. else:
  425. raise TypeError(f"Expected tree, got {theirs_obj.type_name.decode()}")
  426. assert isinstance(base_tree, Tree)
  427. assert isinstance(ours_tree, Tree)
  428. assert isinstance(theirs_tree, Tree)
  429. return merger.merge_trees(base_tree, ours_tree, theirs_tree)