merge.py 28 KB

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