merge.py 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. # merge.py -- Merge support in Dulwich
  2. # Copyright (C) 2020 Jelmer Vernooij <jelmer@jelmer.uk>
  3. #
  4. # Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
  5. # General Public License as public by the Free Software Foundation; version 2.0
  6. # or (at your option) any later version. You can redistribute it and/or
  7. # modify it under the terms of either of these two licenses.
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. #
  15. # You should have received a copy of the licenses; if not, see
  16. # <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
  17. # and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
  18. # License, Version 2.0.
  19. #
  20. """Merge support."""
  21. from collections import namedtuple
  22. from typing import Union, Iterable, List, Callable, Optional
  23. from .diff_tree import (
  24. TreeEntry,
  25. tree_changes,
  26. CHANGE_ADD,
  27. CHANGE_COPY,
  28. CHANGE_DELETE,
  29. CHANGE_MODIFY,
  30. CHANGE_RENAME,
  31. CHANGE_UNCHANGED,
  32. )
  33. from .merge_base import find_merge_base
  34. from .objects import Blob, Tree
  35. FileMerger = Callable[[bytes, bytes, bytes], bytes]
  36. class MergeConflict(namedtuple(
  37. 'MergeConflict',
  38. ['this_entry', 'other_entry', 'base_entry', 'message'])):
  39. """A merge conflict."""
  40. def _merge_entry(
  41. new_path: str, object_store, this_entry: TreeEntry,
  42. other_entry: TreeEntry, base_entry: TreeEntry,
  43. file_merger: Optional[FileMerger]):
  44. """Run a per entry-merge."""
  45. if file_merger is None:
  46. return MergeConflict(
  47. this_entry, other_entry, base_entry,
  48. 'Conflict in %s but no file merger provided'
  49. % new_path)
  50. merged_text = file_merger(
  51. object_store[this_entry.sha].as_raw_string(),
  52. object_store[other_entry.sha].as_raw_string(),
  53. object_store[base_entry.sha].as_raw_string())
  54. merged_text_blob = Blob.from_string(merged_text)
  55. object_store.add_object(merged_text_blob)
  56. # TODO(jelmer): Report conflicts, if any?
  57. if this_entry.mode in (base_entry.mode, other_entry.mode):
  58. mode = other_entry.mode
  59. else:
  60. if base_entry.mode != other_entry.mode:
  61. # TODO(jelmer): Add a mode conflict
  62. raise NotImplementedError
  63. mode = this_entry.mode
  64. return TreeEntry(new_path, mode, merged_text_blob.id)
  65. def merge_tree(
  66. object_store, this_tree: bytes, other_tree: bytes, common_tree: bytes,
  67. rename_detector=None,
  68. file_merger: Optional[FileMerger] = None) -> Iterable[
  69. Union[TreeEntry, MergeConflict]]:
  70. """Merge two trees.
  71. Args:
  72. object_store: object store to retrieve objects from
  73. this_tree: Tree id of THIS tree
  74. other_tree: Tree id of OTHER tree
  75. common_tree: Tree id of COMMON tree
  76. rename_detector: Rename detector object (see dulwich.diff_tree)
  77. file_merger: Three-way file merge implementation
  78. Returns:
  79. iterator over objects, either TreeEntry (updating an entry)
  80. or MergeConflict (indicating a conflict)
  81. """
  82. changes_this = tree_changes(
  83. object_store, common_tree, this_tree, rename_detector=rename_detector)
  84. changes_this_by_common_path = {
  85. change.old.path: change for change in changes_this if change.old}
  86. changes_this_by_this_path = {
  87. change.new.path: change for change in changes_this if change.new}
  88. for other_change in tree_changes(
  89. object_store, common_tree, other_tree,
  90. rename_detector=rename_detector):
  91. this_change = changes_this_by_common_path.get(other_change.old.path)
  92. if this_change == other_change:
  93. continue
  94. if other_change.type in (CHANGE_ADD, CHANGE_COPY):
  95. try:
  96. this_entry = changes_this_by_this_path[other_change.new.path]
  97. except KeyError:
  98. yield other_change.new.path
  99. else:
  100. if this_entry != other_change.new:
  101. # TODO(jelmer): Three way merge instead, with empty common
  102. # base?
  103. yield MergeConflict(
  104. this_entry, other_change.new, other_change.old,
  105. 'Both this and other add new file %s' %
  106. other_change.new.path)
  107. elif other_change.type == CHANGE_DELETE:
  108. if this_change and this_change.type not in (
  109. CHANGE_DELETE, CHANGE_UNCHANGED):
  110. yield MergeConflict(
  111. this_change.new, other_change.new, other_change.old,
  112. '%s is deleted in other but modified in this' %
  113. other_change.old.path)
  114. else:
  115. yield TreeEntry(other_change.old.path, None, None)
  116. elif other_change.type == CHANGE_RENAME:
  117. if this_change and this_change.type == CHANGE_RENAME:
  118. if this_change.new.path != other_change.new.path:
  119. # TODO(jelmer): Does this need to be a conflict?
  120. yield MergeConflict(
  121. this_change.new, other_change.new, other_change.old,
  122. '%s was renamed by both sides (%s / %s)'
  123. % (other_change.old.path, other_change.new.path,
  124. this_change.new.path))
  125. else:
  126. yield _merge_entry(
  127. other_change.new.path,
  128. object_store, this_change.new, other_change.new,
  129. other_change.old, file_merger=file_merger)
  130. elif this_change and this_change.type == CHANGE_MODIFY:
  131. yield _merge_entry(
  132. other_change.new.path,
  133. object_store, this_change.new, other_change.new,
  134. other_change.old, file_merger=file_merger)
  135. elif this_change and this_change.type == CHANGE_DELETE:
  136. yield MergeConflict(
  137. this_change.new, other_change.new, other_change.old,
  138. '%s is deleted in this but renamed to %s in other' %
  139. (other_change.old.path, other_change.new.path))
  140. elif this_change:
  141. raise NotImplementedError(
  142. '%r and %r' % (this_change, other_change))
  143. else:
  144. yield other_change.new
  145. elif other_change.type == CHANGE_MODIFY:
  146. if this_change and this_change.type == CHANGE_DELETE:
  147. yield MergeConflict(
  148. this_change.new, other_change.new, other_change.old,
  149. '%s is deleted in this but modified in other' %
  150. other_change.old.path)
  151. elif this_change and this_change.type in (
  152. CHANGE_MODIFY, CHANGE_RENAME):
  153. yield _merge_entry(
  154. this_change.new.path,
  155. object_store, this_change.new, other_change.new,
  156. other_change.old, file_merger=file_merger)
  157. elif this_change:
  158. raise NotImplementedError(
  159. '%r and %r' % (this_change, other_change))
  160. else:
  161. yield other_change.new
  162. else:
  163. raise NotImplementedError(
  164. 'unsupported change type: %r' % other_change.type)
  165. class MergeResults(object):
  166. def __init__(self, conflicts):
  167. self.conflicts = conflicts
  168. def merge(
  169. repo, commit_ids: List[bytes], rename_detector=None,
  170. file_merger: Optional[FileMerger] = None) -> MergeResults:
  171. """Perform a merge.
  172. """
  173. conflicts = []
  174. [merge_base] = find_merge_base(repo, [repo.head()] + commit_ids)
  175. [other_id] = commit_ids
  176. index = repo.open_index()
  177. this_tree = index.commit(repo.object_store)
  178. for entry in merge_tree(
  179. repo.object_store,
  180. this_tree,
  181. repo.object_store[other_id].tree,
  182. repo.object_store[merge_base].tree,
  183. rename_detector=rename_detector,
  184. file_merger=file_merger):
  185. if isinstance(entry, MergeConflict):
  186. conflicts.append(entry)
  187. # TODO(jelmer): apply the change to the tree
  188. return MergeResults(conflicts=conflicts)