merge.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  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 .diff_tree import (
  23. TreeEntry,
  24. tree_changes,
  25. CHANGE_ADD,
  26. CHANGE_COPY,
  27. CHANGE_DELETE,
  28. CHANGE_MODIFY,
  29. CHANGE_RENAME,
  30. CHANGE_UNCHANGED,
  31. )
  32. from .objects import Blob
  33. class MergeConflict(namedtuple(
  34. 'MergeConflict',
  35. ['this_entry', 'other_entry', 'base_entry', 'message'])):
  36. """A merge conflict."""
  37. def find_merge_base(repo, commit_ids):
  38. """Find a reasonable merge base.
  39. Args:
  40. repo: Repository object
  41. commit_ids: List of commit ids
  42. """
  43. # TODO(jelmer): replace with a pure-python implementation
  44. import subprocess
  45. return subprocess.check_output(
  46. ['git', 'merge-base'] +
  47. [x.decode('ascii') for x in commit_ids],
  48. cwd=repo.path).rstrip(b'\n')
  49. def _merge_entry(new_path, object_store, this_entry, other_entry, base_entry,
  50. file_merger):
  51. """Run a per entry-merge."""
  52. if file_merger is None:
  53. return MergeConflict(
  54. this_entry, other_entry,
  55. other_entry.old,
  56. 'Conflict in %s but no file merger provided'
  57. % new_path)
  58. merged_text = file_merger(
  59. object_store[this_entry.sha].as_raw_string(),
  60. object_store[other_entry.sha].as_raw_string(),
  61. object_store[base_entry.sha].as_raw_string())
  62. merged_text_blob = Blob.from_string(merged_text)
  63. object_store.add(merged_text_blob)
  64. # TODO(jelmer): Report conflicts, if any?
  65. if this_entry.mode in (base_entry.mode, other_entry.mode):
  66. mode = other_entry.mode
  67. else:
  68. if base_entry.mode != other_entry.mode:
  69. # TODO(jelmer): Add a mode conflict
  70. raise NotImplementedError
  71. mode = this_entry.mode
  72. yield TreeEntry(new_path, mode, merged_text_blob.id)
  73. def merge_trees(object_store, this_tree, other_tree, common_tree,
  74. rename_detector=None, file_merger=None):
  75. """Merge two trees.
  76. Args:
  77. object_store: object store to retrieve objects from
  78. this_tree: Tree id of THIS tree
  79. other_tree: Tree id of OTHER tree
  80. common_tree: Tree id of COMMON tree
  81. rename_detector: Rename detector object (see dulwich.diff_tree)
  82. file_merger: Three-way file merge implementation
  83. Returns:
  84. iterator over objects, either TreeEntry (updating an entry)
  85. or MergeConflict (indicating a conflict)
  86. """
  87. changes_this = tree_changes(object_store, common_tree, this_tree)
  88. changes_this_by_common_path = {
  89. change.old.name: change for change in changes_this if change.old}
  90. changes_this_by_this_path = {
  91. change.new.name: change for change in changes_this if change.new}
  92. for other_change in tree_changes(object_store, common_tree, other_tree):
  93. this_change = changes_this_by_common_path.get(other_change.old.name)
  94. if this_change == other_change:
  95. continue
  96. if other_change.type in (CHANGE_ADD, CHANGE_COPY):
  97. try:
  98. this_entry = changes_this_by_this_path[other_change.new.name]
  99. except KeyError:
  100. yield other_change.new.name
  101. else:
  102. if this_entry != other_change.new:
  103. # TODO(jelmer): Three way merge instead, with empty common
  104. # base?
  105. yield MergeConflict(
  106. this_entry, other_change.new, other_change.old,
  107. 'Both this and other add new file %s' %
  108. other_change.new.name)
  109. elif other_change.type == CHANGE_DELETE:
  110. if this_change and this_change.type not in (
  111. CHANGE_DELETE, CHANGE_UNCHANGED):
  112. yield MergeConflict(
  113. this_change.new, other_change.new, other_change.old,
  114. '%s is deleted in other but modified in this' %
  115. other_change.old.name)
  116. else:
  117. yield TreeEntry(other_change.old.name, None, None)
  118. elif other_change.type == CHANGE_RENAME:
  119. if this_change and this_change.type == CHANGE_RENAME:
  120. if this_change.new.name != other_change.new.name:
  121. # TODO(jelmer): Does this need to be a conflict?
  122. yield MergeConflict(
  123. this_change.new, other_change.new, other_change.old,
  124. '%s was renamed by both sides (%s / %s)'
  125. % (other_change.old.name, other_change.new.name,
  126. this_change.new.name))
  127. else:
  128. yield _merge_entry(
  129. other_change.new.name,
  130. object_store, this_change.new, other_change.new,
  131. other_change.old, file_merger=file_merger)
  132. elif this_change and this_change.type == CHANGE_MODIFY:
  133. yield _merge_entry(
  134. other_change.new.name,
  135. object_store, this_change.new, other_change.new,
  136. other_change.old, file_merger=file_merger)
  137. elif this_change and this_change.type == CHANGE_DELETE:
  138. yield MergeConflict(
  139. this_change.new, other_change.new, other_change.old,
  140. '%s is deleted in this but renamed to %s in other' %
  141. (other_change.old.name, other_change.new.name))
  142. elif this_change:
  143. raise NotImplementedError(
  144. '%r and %r' % (this_change, other_change))
  145. else:
  146. yield other_change.new
  147. elif other_change.type == CHANGE_MODIFY:
  148. if this_change and this_change.type == CHANGE_DELETE:
  149. yield MergeConflict(
  150. this_change.new, other_change.new, other_change.old,
  151. '%s is deleted in this but modified in other' %
  152. other_change.old.name)
  153. elif this_change and this_change.type in (
  154. CHANGE_MODIFY, CHANGE_RENAME):
  155. yield _merge_entry(
  156. this_change.new.name,
  157. object_store, this_change.new, other_change.new,
  158. other_change.old, file_merger=file_merger)
  159. elif this_change:
  160. raise NotImplementedError(
  161. '%r and %r' % (this_change, other_change))
  162. else:
  163. yield other_change.new
  164. else:
  165. raise NotImplementedError(
  166. 'unsupported change type: %r' % other_change.type)
  167. class MergeResults(object):
  168. def __init__(self, conflicts):
  169. self.conflicts = conflicts
  170. def merge(repo, commit_ids, rename_detector=None, file_merger=None):
  171. """Perform a merge.
  172. """
  173. conflicts = []
  174. merge_base = find_merge_base(repo, commit_ids)
  175. [other_id] = commit_ids
  176. index = repo.open_index()
  177. this_id = index.commit(repo.object_store)
  178. for entry in merge_trees(
  179. repo.object_store,
  180. repo.object_store[this_id].tree,
  181. repo.object_store[other_id].tree,
  182. repo.object_store[merge_base].tree,
  183. rename_detector=rename_detector):
  184. if isinstance(entry, MergeConflict):
  185. conflicts.append(entry)
  186. return conflicts