merge.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759
  1. """Git merge implementation."""
  2. from collections.abc import Sequence
  3. from typing import TYPE_CHECKING, Optional
  4. if TYPE_CHECKING:
  5. import merge3
  6. from merge3 import SequenceMatcherProtocol
  7. else:
  8. try:
  9. import merge3
  10. except ImportError:
  11. merge3 = None # type: ignore[assignment]
  12. from dulwich.attrs import GitAttributes
  13. from dulwich.config import Config
  14. from dulwich.merge_drivers import get_merge_driver_registry
  15. from dulwich.object_store import BaseObjectStore
  16. from dulwich.objects import S_ISGITLINK, Blob, Commit, Tree, is_blob, is_tree
  17. def make_merge3(
  18. base: Sequence[bytes],
  19. a: Sequence[bytes],
  20. b: Sequence[bytes],
  21. is_cherrypick: bool = False,
  22. sequence_matcher: Optional[type["SequenceMatcherProtocol[bytes]"]] = None,
  23. ) -> "merge3.Merge3[bytes]":
  24. """Return a Merge3 object, or raise ImportError if merge3 is not installed."""
  25. if merge3 is None:
  26. raise ImportError(
  27. "merge3 module is required for three-way merging. "
  28. "Install it with: pip install merge3"
  29. )
  30. return merge3.Merge3(base, a, b, is_cherrypick, sequence_matcher)
  31. class MergeConflict(Exception):
  32. """Raised when a merge conflict occurs."""
  33. def __init__(self, path: bytes, message: str) -> None:
  34. """Initialize MergeConflict.
  35. Args:
  36. path: Path to the conflicted file
  37. message: Conflict description
  38. """
  39. self.path = path
  40. super().__init__(f"Merge conflict in {path!r}: {message}")
  41. def _can_merge_lines(
  42. base_lines: Sequence[bytes], a_lines: Sequence[bytes], b_lines: Sequence[bytes]
  43. ) -> bool:
  44. """Check if lines can be merged without conflict."""
  45. # If one side is unchanged, we can take the other side
  46. if base_lines == a_lines:
  47. return True
  48. elif base_lines == b_lines:
  49. return True
  50. else:
  51. # For now, treat any difference as a conflict
  52. # A more sophisticated algorithm would check for non-overlapping changes
  53. return False
  54. if merge3 is not None:
  55. def _merge3_to_bytes(m: "merge3.Merge3[bytes]") -> bytes:
  56. """Convert merge3 result to bytes with conflict markers.
  57. Args:
  58. m: Merge3 object
  59. Returns:
  60. Merged content as bytes
  61. """
  62. result: list[bytes] = []
  63. for group in m.merge_groups(): # type: ignore[no-untyped-call,unused-ignore]
  64. if group[0] == "unchanged":
  65. result.extend(group[1])
  66. elif group[0] == "a":
  67. result.extend(group[1])
  68. elif group[0] == "b":
  69. result.extend(group[1])
  70. elif group[0] == "same":
  71. result.extend(group[1])
  72. elif group[0] == "conflict":
  73. # Check if this is a real conflict or just different changes
  74. base_lines, a_lines, b_lines = group[1], group[2], group[3]
  75. # Try to merge line by line
  76. if _can_merge_lines(base_lines, a_lines, b_lines):
  77. merged_lines = _merge_lines(base_lines, a_lines, b_lines)
  78. result.extend(merged_lines)
  79. else:
  80. # Real conflict - add conflict markers
  81. result.append(b"<<<<<<< ours\n")
  82. result.extend(a_lines)
  83. result.append(b"=======\n")
  84. result.extend(b_lines)
  85. result.append(b">>>>>>> theirs\n")
  86. return b"".join(result)
  87. def _merge_lines(
  88. base_lines: Sequence[bytes], a_lines: Sequence[bytes], b_lines: Sequence[bytes]
  89. ) -> Sequence[bytes]:
  90. """Merge lines when possible."""
  91. if base_lines == a_lines:
  92. return b_lines
  93. elif base_lines == b_lines:
  94. return a_lines
  95. else:
  96. # This shouldn't happen if _can_merge_lines returned True
  97. return a_lines
  98. def merge_blobs(
  99. base_blob: Optional[Blob],
  100. ours_blob: Optional[Blob],
  101. theirs_blob: Optional[Blob],
  102. path: Optional[bytes] = None,
  103. gitattributes: Optional[GitAttributes] = None,
  104. config: Optional[Config] = None,
  105. ) -> tuple[bytes, bool]:
  106. """Perform three-way merge on blob contents.
  107. Args:
  108. base_blob: Common ancestor blob (can be None)
  109. ours_blob: Our version of the blob (can be None)
  110. theirs_blob: Their version of the blob (can be None)
  111. path: Optional path of the file being merged
  112. gitattributes: Optional GitAttributes object for checking merge drivers
  113. config: Optional Config object for loading merge driver configuration
  114. Returns:
  115. Tuple of (merged_content, had_conflicts)
  116. """
  117. # Check for merge driver
  118. merge_driver_name = None
  119. if path and gitattributes:
  120. attrs = gitattributes.match_path(path)
  121. merge_value = attrs.get(b"merge")
  122. if merge_value and isinstance(merge_value, bytes) and merge_value != b"text":
  123. merge_driver_name = merge_value.decode("utf-8", errors="replace")
  124. # Use merge driver if found
  125. if merge_driver_name:
  126. registry = get_merge_driver_registry(config)
  127. driver = registry.get_driver(merge_driver_name)
  128. if driver:
  129. # Get content from blobs
  130. base_content = base_blob.data if base_blob else b""
  131. ours_content = ours_blob.data if ours_blob else b""
  132. theirs_content = theirs_blob.data if theirs_blob else b""
  133. # Use merge driver
  134. merged_content, success = driver.merge(
  135. ancestor=base_content,
  136. ours=ours_content,
  137. theirs=theirs_content,
  138. path=path.decode("utf-8", errors="replace") if path else None,
  139. marker_size=7,
  140. )
  141. # Convert success (no conflicts) to had_conflicts (conflicts occurred)
  142. had_conflicts = not success
  143. return merged_content, had_conflicts
  144. # Fall back to default merge behavior
  145. # Handle deletion cases
  146. if ours_blob is None and theirs_blob is None:
  147. return b"", False
  148. if base_blob is None:
  149. # No common ancestor
  150. if ours_blob is None:
  151. assert theirs_blob is not None
  152. return theirs_blob.data, False
  153. elif theirs_blob is None:
  154. return ours_blob.data, False
  155. elif ours_blob.data == theirs_blob.data:
  156. return ours_blob.data, False
  157. else:
  158. # Both added different content - conflict
  159. m = make_merge3(
  160. [],
  161. ours_blob.data.splitlines(True),
  162. theirs_blob.data.splitlines(True),
  163. )
  164. return _merge3_to_bytes(m), True
  165. # Get content for each version
  166. base_content = base_blob.data if base_blob else b""
  167. ours_content = ours_blob.data if ours_blob else b""
  168. theirs_content = theirs_blob.data if theirs_blob else b""
  169. # Check if either side deleted
  170. if ours_blob is None or theirs_blob is None:
  171. if ours_blob is None and theirs_blob is None:
  172. return b"", False
  173. elif ours_blob is None:
  174. # We deleted, check if they modified
  175. if base_content == theirs_content:
  176. return b"", False # They didn't modify, accept deletion
  177. else:
  178. # Conflict: we deleted, they modified
  179. m = make_merge3(
  180. base_content.splitlines(True),
  181. [],
  182. theirs_content.splitlines(True),
  183. )
  184. return _merge3_to_bytes(m), True
  185. else:
  186. # They deleted, check if we modified
  187. if base_content == ours_content:
  188. return b"", False # We didn't modify, accept deletion
  189. else:
  190. # Conflict: they deleted, we modified
  191. m = make_merge3(
  192. base_content.splitlines(True),
  193. ours_content.splitlines(True),
  194. [],
  195. )
  196. return _merge3_to_bytes(m), True
  197. # Both sides exist, check if merge is needed
  198. if ours_content == theirs_content:
  199. return ours_content, False
  200. elif base_content == ours_content:
  201. return theirs_content, False
  202. elif base_content == theirs_content:
  203. return ours_content, False
  204. # Perform three-way merge
  205. m = make_merge3(
  206. base_content.splitlines(True),
  207. ours_content.splitlines(True),
  208. theirs_content.splitlines(True),
  209. )
  210. # Check for conflicts and generate merged content
  211. merged_content = _merge3_to_bytes(m)
  212. has_conflicts = b"<<<<<<< ours" in merged_content
  213. return merged_content, has_conflicts
  214. class Merger:
  215. """Handles git merge operations."""
  216. def __init__(
  217. self,
  218. object_store: BaseObjectStore,
  219. gitattributes: Optional[GitAttributes] = None,
  220. config: Optional[Config] = None,
  221. ) -> None:
  222. """Initialize merger.
  223. Args:
  224. object_store: Object store to read objects from
  225. gitattributes: Optional GitAttributes object for checking merge drivers
  226. config: Optional Config object for loading merge driver configuration
  227. """
  228. self.object_store = object_store
  229. self.gitattributes = gitattributes
  230. self.config = config
  231. def merge_blobs(
  232. self,
  233. base_blob: Optional[Blob],
  234. ours_blob: Optional[Blob],
  235. theirs_blob: Optional[Blob],
  236. path: Optional[bytes] = None,
  237. ) -> tuple[bytes, bool]:
  238. """Perform three-way merge on blob contents.
  239. Args:
  240. base_blob: Common ancestor blob (can be None)
  241. ours_blob: Our version of the blob (can be None)
  242. theirs_blob: Their version of the blob (can be None)
  243. path: Optional path of the file being merged
  244. Returns:
  245. Tuple of (merged_content, had_conflicts)
  246. """
  247. return merge_blobs(
  248. base_blob, ours_blob, theirs_blob, path, self.gitattributes, self.config
  249. )
  250. def merge_trees(
  251. self, base_tree: Optional[Tree], ours_tree: Tree, theirs_tree: Tree
  252. ) -> tuple[Tree, list[bytes]]:
  253. """Perform three-way merge on trees.
  254. Args:
  255. base_tree: Common ancestor tree (can be None for no common ancestor)
  256. ours_tree: Our version of the tree
  257. theirs_tree: Their version of the tree
  258. Returns:
  259. tuple of (merged_tree, list_of_conflicted_paths)
  260. """
  261. conflicts: list[bytes] = []
  262. merged_entries: dict[bytes, tuple[Optional[int], Optional[bytes]]] = {}
  263. # Get all paths from all trees
  264. all_paths = set()
  265. if base_tree:
  266. for entry in base_tree.items():
  267. assert entry.path is not None
  268. all_paths.add(entry.path)
  269. for entry in ours_tree.items():
  270. assert entry.path is not None
  271. all_paths.add(entry.path)
  272. for entry in theirs_tree.items():
  273. assert entry.path is not None
  274. all_paths.add(entry.path)
  275. # Process each path
  276. for path in sorted(all_paths):
  277. base_entry = None
  278. if base_tree:
  279. try:
  280. base_entry = base_tree.lookup_path(
  281. self.object_store.__getitem__, path
  282. )
  283. except KeyError:
  284. pass
  285. try:
  286. ours_entry = ours_tree.lookup_path(self.object_store.__getitem__, path)
  287. except KeyError:
  288. ours_entry = None
  289. try:
  290. theirs_entry = theirs_tree.lookup_path(
  291. self.object_store.__getitem__, path
  292. )
  293. except KeyError:
  294. theirs_entry = None
  295. # Extract mode and sha
  296. _base_mode, base_sha = base_entry if base_entry else (None, None)
  297. ours_mode, ours_sha = ours_entry if ours_entry else (None, None)
  298. theirs_mode, theirs_sha = theirs_entry if theirs_entry else (None, None)
  299. # Handle deletions
  300. if ours_sha is None and theirs_sha is None:
  301. continue # Deleted in both
  302. # Handle additions
  303. if base_sha is None:
  304. if ours_sha == theirs_sha and ours_mode == theirs_mode:
  305. # Same addition in both
  306. merged_entries[path] = (ours_mode, ours_sha)
  307. elif ours_sha is None:
  308. # Added only in theirs
  309. merged_entries[path] = (theirs_mode, theirs_sha)
  310. elif theirs_sha is None:
  311. # Added only in ours
  312. merged_entries[path] = (ours_mode, ours_sha)
  313. else:
  314. # Different additions - conflict
  315. conflicts.append(path)
  316. # For now, keep ours
  317. merged_entries[path] = (ours_mode, ours_sha)
  318. continue
  319. # Check for mode conflicts
  320. if (
  321. ours_mode != theirs_mode
  322. and ours_mode is not None
  323. and theirs_mode is not None
  324. ):
  325. conflicts.append(path)
  326. # For now, keep ours
  327. merged_entries[path] = (ours_mode, ours_sha)
  328. continue
  329. # Handle modifications
  330. if ours_sha == theirs_sha:
  331. # Same modification or no change
  332. if ours_sha is not None:
  333. merged_entries[path] = (ours_mode, ours_sha)
  334. elif base_sha == ours_sha and theirs_sha is not None:
  335. # Only theirs modified
  336. merged_entries[path] = (theirs_mode, theirs_sha)
  337. elif base_sha == theirs_sha and ours_sha is not None:
  338. # Only ours modified
  339. merged_entries[path] = (ours_mode, ours_sha)
  340. elif ours_sha is None:
  341. # We deleted
  342. if base_sha == theirs_sha:
  343. # They didn't modify, accept deletion
  344. pass
  345. else:
  346. # They modified, we deleted - conflict
  347. conflicts.append(path)
  348. elif theirs_sha is None:
  349. # They deleted
  350. if base_sha == ours_sha:
  351. # We didn't modify, accept deletion
  352. pass
  353. else:
  354. # We modified, they deleted - conflict
  355. conflicts.append(path)
  356. merged_entries[path] = (ours_mode, ours_sha)
  357. else:
  358. # Both modified differently
  359. # For trees and submodules, this is a conflict
  360. if S_ISGITLINK(ours_mode or 0) or S_ISGITLINK(theirs_mode or 0):
  361. conflicts.append(path)
  362. merged_entries[path] = (ours_mode, ours_sha)
  363. elif (ours_mode or 0) & 0o170000 == 0o040000 or (
  364. theirs_mode or 0
  365. ) & 0o170000 == 0o040000:
  366. # Tree conflict
  367. conflicts.append(path)
  368. merged_entries[path] = (ours_mode, ours_sha)
  369. else:
  370. # Try to merge blobs
  371. base_blob = None
  372. if base_sha:
  373. base_obj = self.object_store[base_sha]
  374. if is_blob(base_obj):
  375. base_blob = base_obj
  376. else:
  377. raise TypeError(
  378. f"Expected blob for {path!r}, got {base_obj.type_name.decode()}"
  379. )
  380. ours_blob = None
  381. if ours_sha:
  382. ours_obj = self.object_store[ours_sha]
  383. if is_blob(ours_obj):
  384. ours_blob = ours_obj
  385. else:
  386. raise TypeError(
  387. f"Expected blob for {path!r}, got {ours_obj.type_name.decode()}"
  388. )
  389. theirs_blob = None
  390. if theirs_sha:
  391. theirs_obj = self.object_store[theirs_sha]
  392. if is_blob(theirs_obj):
  393. theirs_blob = theirs_obj
  394. else:
  395. raise TypeError(
  396. f"Expected blob for {path!r}, got {theirs_obj.type_name.decode()}"
  397. )
  398. assert isinstance(base_blob, Blob)
  399. assert isinstance(ours_blob, Blob)
  400. assert isinstance(theirs_blob, Blob)
  401. merged_content, had_conflict = self.merge_blobs(
  402. base_blob, ours_blob, theirs_blob, path
  403. )
  404. if had_conflict:
  405. conflicts.append(path)
  406. # Store merged blob
  407. merged_blob = Blob.from_string(merged_content)
  408. self.object_store.add_object(merged_blob)
  409. merged_entries[path] = (ours_mode or theirs_mode, merged_blob.id)
  410. # Build merged tree
  411. merged_tree = Tree()
  412. for path, (mode, sha) in sorted(merged_entries.items()):
  413. if mode is not None and sha is not None:
  414. merged_tree.add(path, mode, sha)
  415. return merged_tree, conflicts
  416. def _create_virtual_commit(
  417. object_store: BaseObjectStore,
  418. tree: Tree,
  419. parents: list[bytes],
  420. message: bytes = b"Virtual merge base",
  421. ) -> Commit:
  422. """Create a virtual commit object for recursive merging.
  423. Args:
  424. object_store: Object store to add the commit to
  425. tree: Tree object for the commit
  426. parents: List of parent commit IDs
  427. message: Commit message
  428. Returns:
  429. The created Commit object
  430. """
  431. # Add the tree to the object store
  432. object_store.add_object(tree)
  433. # Create a virtual commit
  434. commit = Commit()
  435. commit.tree = tree.id
  436. commit.parents = parents
  437. commit.author = b"Dulwich Recursive Merge <dulwich@example.com>"
  438. commit.committer = commit.author
  439. commit.commit_time = 0
  440. commit.author_time = 0
  441. commit.commit_timezone = 0
  442. commit.author_timezone = 0
  443. commit.encoding = b"UTF-8"
  444. commit.message = message
  445. # Add the commit to the object store
  446. object_store.add_object(commit)
  447. return commit
  448. def recursive_merge(
  449. object_store: BaseObjectStore,
  450. merge_bases: list[bytes],
  451. ours_commit: Commit,
  452. theirs_commit: Commit,
  453. gitattributes: Optional[GitAttributes] = None,
  454. config: Optional[Config] = None,
  455. ) -> tuple[Tree, list[bytes]]:
  456. """Perform a recursive merge with multiple merge bases.
  457. This implements Git's recursive merge strategy, which handles cases where
  458. there are multiple common ancestors (criss-cross merges). The algorithm:
  459. 1. If there's 0 or 1 merge base, perform a simple three-way merge
  460. 2. If there are multiple merge bases, merge them recursively to create
  461. a virtual merge base, then use that for the final three-way merge
  462. Args:
  463. object_store: Object store to read/write objects
  464. merge_bases: List of merge base commit IDs
  465. ours_commit: Our commit
  466. theirs_commit: Their commit
  467. gitattributes: Optional GitAttributes object for checking merge drivers
  468. config: Optional Config object for loading merge driver configuration
  469. Returns:
  470. tuple of (merged_tree, list_of_conflicted_paths)
  471. """
  472. if not merge_bases:
  473. # No common ancestor - use None as base
  474. return three_way_merge(
  475. object_store, None, ours_commit, theirs_commit, gitattributes, config
  476. )
  477. elif len(merge_bases) == 1:
  478. # Single merge base - simple three-way merge
  479. base_commit_obj = object_store[merge_bases[0]]
  480. if not isinstance(base_commit_obj, Commit):
  481. raise TypeError(
  482. f"Expected commit, got {base_commit_obj.type_name.decode()}"
  483. )
  484. return three_way_merge(
  485. object_store,
  486. base_commit_obj,
  487. ours_commit,
  488. theirs_commit,
  489. gitattributes,
  490. config,
  491. )
  492. else:
  493. # Multiple merge bases - need to create a virtual merge base
  494. # Start by merging the first two bases
  495. virtual_base_id = merge_bases[0]
  496. virtual_commit_obj = object_store[virtual_base_id]
  497. if not isinstance(virtual_commit_obj, Commit):
  498. raise TypeError(
  499. f"Expected commit, got {virtual_commit_obj.type_name.decode()}"
  500. )
  501. # Recursively merge each additional base
  502. for next_base_id in merge_bases[1:]:
  503. next_base_obj = object_store[next_base_id]
  504. if not isinstance(next_base_obj, Commit):
  505. raise TypeError(
  506. f"Expected commit, got {next_base_obj.type_name.decode()}"
  507. )
  508. # Find merge base of these two bases
  509. # Import here to avoid circular dependency
  510. # We need access to the repo for find_merge_base
  511. # For now, we'll perform a simple three-way merge without recursion
  512. # between the two virtual commits
  513. # A proper implementation would require passing the repo object
  514. # Perform three-way merge of the two bases (using None as their base)
  515. merged_tree, _conflicts = three_way_merge(
  516. object_store,
  517. None, # No common ancestor for virtual merge bases
  518. virtual_commit_obj,
  519. next_base_obj,
  520. gitattributes,
  521. config,
  522. )
  523. # Create a virtual commit with this merged tree
  524. virtual_commit_obj = _create_virtual_commit(
  525. object_store,
  526. merged_tree,
  527. [virtual_base_id, next_base_id],
  528. )
  529. virtual_base_id = virtual_commit_obj.id
  530. # Now use the virtual merge base for the final merge
  531. return three_way_merge(
  532. object_store,
  533. virtual_commit_obj,
  534. ours_commit,
  535. theirs_commit,
  536. gitattributes,
  537. config,
  538. )
  539. def three_way_merge(
  540. object_store: BaseObjectStore,
  541. base_commit: Optional[Commit],
  542. ours_commit: Commit,
  543. theirs_commit: Commit,
  544. gitattributes: Optional[GitAttributes] = None,
  545. config: Optional[Config] = None,
  546. ) -> tuple[Tree, list[bytes]]:
  547. """Perform a three-way merge between commits.
  548. Args:
  549. object_store: Object store to read/write objects
  550. base_commit: Common ancestor commit (None if no common ancestor)
  551. ours_commit: Our commit
  552. theirs_commit: Their commit
  553. gitattributes: Optional GitAttributes object for checking merge drivers
  554. config: Optional Config object for loading merge driver configuration
  555. Returns:
  556. tuple of (merged_tree, list_of_conflicted_paths)
  557. """
  558. merger = Merger(object_store, gitattributes, config)
  559. base_tree = None
  560. if base_commit:
  561. base_obj = object_store[base_commit.tree]
  562. if is_tree(base_obj):
  563. base_tree = base_obj
  564. else:
  565. raise TypeError(f"Expected tree, got {base_obj.type_name.decode()}")
  566. ours_obj = object_store[ours_commit.tree]
  567. if is_tree(ours_obj):
  568. ours_tree = ours_obj
  569. else:
  570. raise TypeError(f"Expected tree, got {ours_obj.type_name.decode()}")
  571. theirs_obj = object_store[theirs_commit.tree]
  572. if is_tree(theirs_obj):
  573. theirs_tree = theirs_obj
  574. else:
  575. raise TypeError(f"Expected tree, got {theirs_obj.type_name.decode()}")
  576. assert base_tree is None or isinstance(base_tree, Tree)
  577. assert isinstance(ours_tree, Tree)
  578. assert isinstance(theirs_tree, Tree)
  579. return merger.merge_trees(base_tree, ours_tree, theirs_tree)
  580. def octopus_merge(
  581. object_store: BaseObjectStore,
  582. merge_bases: list[bytes],
  583. head_commit: Commit,
  584. other_commits: list[Commit],
  585. gitattributes: Optional[GitAttributes] = None,
  586. config: Optional[Config] = None,
  587. ) -> tuple[Tree, list[bytes]]:
  588. """Perform an octopus merge of multiple commits.
  589. The octopus merge strategy merges multiple branches sequentially into a single
  590. commit with multiple parents. It refuses to proceed if any merge would result
  591. in conflicts that require manual resolution.
  592. Args:
  593. object_store: Object store to read/write objects
  594. merge_bases: List of common ancestor commit IDs for all commits
  595. head_commit: Current HEAD commit (ours)
  596. other_commits: List of commits to merge (theirs)
  597. gitattributes: Optional GitAttributes object for checking merge drivers
  598. config: Optional Config object for loading merge driver configuration
  599. Returns:
  600. tuple of (merged_tree, list_of_conflicted_paths)
  601. If any conflicts occur during the sequential merges, the function returns
  602. early with the conflicts list populated.
  603. Raises:
  604. TypeError: If any object is not of the expected type
  605. """
  606. if not other_commits:
  607. raise ValueError("octopus_merge requires at least one commit to merge")
  608. # Start with the head commit's tree as our current state
  609. current_commit = head_commit
  610. # Merge each commit sequentially
  611. for i, other_commit in enumerate(other_commits):
  612. # Find the merge base between current state and the commit we're merging
  613. # For octopus merges, we use the octopus base for all commits
  614. if merge_bases:
  615. base_commit_id = merge_bases[0]
  616. base_commit = object_store[base_commit_id]
  617. if not isinstance(base_commit, Commit):
  618. raise TypeError(f"Expected Commit, got {type(base_commit)}")
  619. else:
  620. base_commit = None
  621. # Perform three-way merge
  622. merged_tree, conflicts = three_way_merge(
  623. object_store,
  624. base_commit,
  625. current_commit,
  626. other_commit,
  627. gitattributes,
  628. config,
  629. )
  630. # Octopus merge refuses to proceed if there are conflicts
  631. if conflicts:
  632. return merged_tree, conflicts
  633. # Add merged tree to object store
  634. object_store.add_object(merged_tree)
  635. # Create a temporary commit object with the merged tree for the next iteration
  636. # This allows us to continue merging additional commits
  637. if i < len(other_commits) - 1:
  638. temp_commit = Commit()
  639. temp_commit.tree = merged_tree.id
  640. # For intermediate merges, we use the same parent as current
  641. temp_commit.parents = (
  642. current_commit.parents
  643. if current_commit.parents
  644. else [current_commit.id]
  645. )
  646. # Set minimal required commit fields
  647. temp_commit.author = current_commit.author
  648. temp_commit.committer = current_commit.committer
  649. temp_commit.author_time = current_commit.author_time
  650. temp_commit.commit_time = current_commit.commit_time
  651. temp_commit.author_timezone = current_commit.author_timezone
  652. temp_commit.commit_timezone = current_commit.commit_timezone
  653. temp_commit.message = b"Temporary octopus merge commit"
  654. object_store.add_object(temp_commit)
  655. current_commit = temp_commit
  656. return merged_tree, []