13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19
from itertools import chain
19
from bzrlib.lazy_import import lazy_import
20
lazy_import(globals(), """
21
23
from bzrlib import (
22
branch as _mod_branch,
24
conflicts as _mod_conflicts,
27
26
graph as _mod_graph,
31
30
revision as _mod_revision,
47
from bzrlib.symbol_versioning import (
34
from bzrlib.branch import Branch
35
from bzrlib.conflicts import ConflictList, Conflict
36
from bzrlib.errors import (BzrCommandError,
46
WorkingTreeNotRevision,
49
from bzrlib.graph import Graph
50
from bzrlib.merge3 import Merge3
51
from bzrlib.osutils import rename, pathjoin
52
from progress import DummyProgress, ProgressPhase
53
from bzrlib.revision import (NULL_REVISION, ensure_null)
54
from bzrlib.textfile import check_text_lines
55
from bzrlib.trace import mutter, warning, note, is_quiet
56
from bzrlib.transform import (TransformPreview, TreeTransform,
57
resolve_conflicts, cook_conflicts,
58
conflict_pass, FinalPaths, create_by_entry,
59
unique_add, ROOT_PARENT)
60
from bzrlib.versionedfile import PlanWeaveMerge
51
63
# TODO: Report back as changes are merged in
54
66
def transform_tree(from_tree, to_tree, interesting_ids=None):
55
from_tree.lock_tree_write()
56
operation = cleanup.OperationWithCleanups(merge_inner)
57
operation.add_cleanup(from_tree.unlock)
58
operation.run_simple(from_tree.branch, to_tree, from_tree,
59
ignore_zero=True, interesting_ids=interesting_ids, this_tree=from_tree)
62
class MergeHooks(hooks.Hooks):
65
hooks.Hooks.__init__(self, "bzrlib.merge", "Merger.hooks")
66
self.add_hook('merge_file_content',
67
"Called with a bzrlib.merge.Merger object to create a per file "
68
"merge object when starting a merge. "
69
"Should return either None or a subclass of "
70
"``bzrlib.merge.AbstractPerFileMerger``. "
71
"Such objects will then be called per file "
72
"that needs to be merged (including when one "
73
"side has deleted the file and the other has changed it). "
74
"See the AbstractPerFileMerger API docs for details on how it is "
79
class AbstractPerFileMerger(object):
80
"""PerFileMerger objects are used by plugins extending merge for bzrlib.
82
See ``bzrlib.plugins.news_merge.news_merge`` for an example concrete class.
84
:ivar merger: The Merge3Merger performing the merge.
87
def __init__(self, merger):
88
"""Create a PerFileMerger for use with merger."""
91
def merge_contents(self, merge_params):
92
"""Attempt to merge the contents of a single file.
94
:param merge_params: A bzrlib.merge.MergeHookParams
95
:return: A tuple of (status, chunks), where status is one of
96
'not_applicable', 'success', 'conflicted', or 'delete'. If status
97
is 'success' or 'conflicted', then chunks should be an iterable of
98
strings for the new file contents.
100
return ('not applicable', None)
103
class PerFileMerger(AbstractPerFileMerger):
104
"""Merge individual files when self.file_matches returns True.
106
This class is intended to be subclassed. The file_matches and
107
merge_matching methods should be overridden with concrete implementations.
110
def file_matches(self, params):
111
"""Return True if merge_matching should be called on this file.
113
Only called with merges of plain files with no clear winner.
115
Subclasses must override this.
117
raise NotImplementedError(self.file_matches)
119
def get_filename(self, params, tree):
120
"""Lookup the filename (i.e. basename, not path), given a Tree (e.g.
121
self.merger.this_tree) and a MergeHookParams.
123
return osutils.basename(tree.id2path(params.file_id))
125
def get_filepath(self, params, tree):
126
"""Calculate the path to the file in a tree.
128
:param params: A MergeHookParams describing the file to merge
129
:param tree: a Tree, e.g. self.merger.this_tree.
131
return tree.id2path(params.file_id)
133
def merge_contents(self, params):
134
"""Merge the contents of a single file."""
135
# Check whether this custom merge logic should be used.
137
# OTHER is a straight winner, rely on default merge.
138
params.winner == 'other' or
139
# THIS and OTHER aren't both files.
140
not params.is_file_merge() or
141
# The filename doesn't match *.xml
142
not self.file_matches(params)):
143
return 'not_applicable', None
144
return self.merge_matching(params)
146
def merge_matching(self, params):
147
"""Merge the contents of a single file that has matched the criteria
148
in PerFileMerger.merge_contents (is a conflict, is a file,
149
self.file_matches is True).
151
Subclasses must override this.
153
raise NotImplementedError(self.merge_matching)
156
class ConfigurableFileMerger(PerFileMerger):
157
"""Merge individual files when configured via a .conf file.
159
This is a base class for concrete custom file merging logic. Concrete
160
classes should implement ``merge_text``.
162
See ``bzrlib.plugins.news_merge.news_merge`` for an example concrete class.
164
:ivar affected_files: The configured file paths to merge.
166
:cvar name_prefix: The prefix to use when looking up configuration
167
details. <name_prefix>_merge_files describes the files targeted by the
170
:cvar default_files: The default file paths to merge when no configuration
177
def __init__(self, merger):
178
super(ConfigurableFileMerger, self).__init__(merger)
179
self.affected_files = None
180
self.default_files = self.__class__.default_files or []
181
self.name_prefix = self.__class__.name_prefix
182
if self.name_prefix is None:
183
raise ValueError("name_prefix must be set.")
185
def file_matches(self, params):
186
"""Check whether the file should call the merge hook.
188
<name_prefix>_merge_files configuration variable is a list of files
189
that should use the hook.
191
affected_files = self.affected_files
192
if affected_files is None:
193
config = self.merger.this_branch.get_config()
194
# Until bzr provides a better policy for caching the config, we
195
# just add the part we're interested in to the params to avoid
196
# reading the config files repeatedly (bazaar.conf, location.conf,
198
config_key = self.name_prefix + '_merge_files'
199
affected_files = config.get_user_option_as_list(config_key)
200
if affected_files is None:
201
# If nothing was specified in the config, use the default.
202
affected_files = self.default_files
203
self.affected_files = affected_files
205
filepath = self.get_filepath(params, self.merger.this_tree)
206
if filepath in affected_files:
210
def merge_matching(self, params):
211
return self.merge_text(params)
213
def merge_text(self, params):
214
"""Merge the byte contents of a single file.
216
This is called after checking that the merge should be performed in
217
merge_contents, and it should behave as per
218
``bzrlib.merge.AbstractPerFileMerger.merge_contents``.
220
raise NotImplementedError(self.merge_text)
223
class MergeHookParams(object):
224
"""Object holding parameters passed to merge_file_content hooks.
226
There are some fields hooks can access:
228
:ivar file_id: the file ID of the file being merged
229
:ivar trans_id: the transform ID for the merge of this file
230
:ivar this_kind: kind of file_id in 'this' tree
231
:ivar other_kind: kind of file_id in 'other' tree
232
:ivar winner: one of 'this', 'other', 'conflict'
235
def __init__(self, merger, file_id, trans_id, this_kind, other_kind,
237
self._merger = merger
238
self.file_id = file_id
239
self.trans_id = trans_id
240
self.this_kind = this_kind
241
self.other_kind = other_kind
244
def is_file_merge(self):
245
"""True if this_kind and other_kind are both 'file'."""
246
return self.this_kind == 'file' and self.other_kind == 'file'
248
@decorators.cachedproperty
249
def base_lines(self):
250
"""The lines of the 'base' version of the file."""
251
return self._merger.get_lines(self._merger.base_tree, self.file_id)
253
@decorators.cachedproperty
254
def this_lines(self):
255
"""The lines of the 'this' version of the file."""
256
return self._merger.get_lines(self._merger.this_tree, self.file_id)
258
@decorators.cachedproperty
259
def other_lines(self):
260
"""The lines of the 'other' version of the file."""
261
return self._merger.get_lines(self._merger.other_tree, self.file_id)
67
merge_inner(from_tree.branch, to_tree, from_tree, ignore_zero=True,
68
interesting_ids=interesting_ids, this_tree=from_tree)
264
71
class Merger(object):
268
72
def __init__(self, this_branch, other_tree=None, base_tree=None,
269
this_tree=None, pb=None, change_reporter=None,
73
this_tree=None, pb=DummyProgress(), change_reporter=None,
270
74
recurse='down', revision_graph=None):
271
75
object.__init__(self)
272
76
self.this_branch = this_branch
451
237
if self.other_rev_id is None:
452
238
other_basis_tree = self.revision_tree(self.other_basis)
453
if other_basis_tree.has_changes(self.other_tree):
454
raise errors.WorkingTreeNotRevision(self.this_tree)
239
changes = other_basis_tree.changes_from(self.other_tree)
240
if changes.has_changed():
241
raise WorkingTreeNotRevision(self.this_tree)
455
242
other_rev_id = self.other_basis
456
243
self.other_tree = other_basis_tree
458
@deprecated_method(deprecated_in((2, 1, 0)))
459
245
def file_revisions(self, file_id):
460
246
self.ensure_revision_trees()
247
def get_id(tree, file_id):
248
revision_id = tree.inventory[file_id].revision
461
250
if self.this_rev_id is None:
462
251
if self.this_basis_tree.get_file_sha1(file_id) != \
463
252
self.this_tree.get_file_sha1(file_id):
464
raise errors.WorkingTreeNotRevision(self.this_tree)
253
raise WorkingTreeNotRevision(self.this_tree)
466
255
trees = (self.this_basis_tree, self.other_tree)
467
return [tree.get_file_revision(file_id) for tree in trees]
256
return [get_id(tree, file_id) for tree in trees]
469
@deprecated_method(deprecated_in((2, 1, 0)))
470
258
def check_basis(self, check_clean, require_commits=True):
471
259
if self.this_basis is None and require_commits is True:
472
raise errors.BzrCommandError(
473
"This branch has no commits."
474
" (perhaps you would prefer 'bzr pull')")
260
raise BzrCommandError("This branch has no commits."
261
" (perhaps you would prefer 'bzr pull')")
476
263
self.compare_basis()
477
264
if self.this_basis != self.this_rev_id:
478
265
raise errors.UncommittedChanges(self.this_tree)
480
@deprecated_method(deprecated_in((2, 1, 0)))
481
267
def compare_basis(self):
483
269
basis_tree = self.revision_tree(self.this_tree.last_revision())
484
270
except errors.NoSuchRevision:
485
271
basis_tree = self.this_tree.basis_tree()
486
if not self.this_tree.has_changes(basis_tree):
272
changes = self.this_tree.changes_from(basis_tree)
273
if not changes.has_changed():
487
274
self.this_rev_id = self.this_basis
489
276
def set_interesting_files(self, file_list):
490
277
self.interesting_files = file_list
492
279
def set_pending(self):
493
if (not self.base_is_ancestor or not self.base_is_other_ancestor
494
or self.other_rev_id is None):
280
if not self.base_is_ancestor or not self.base_is_other_ancestor or self.other_rev_id is None:
496
282
self._add_parent()
498
284
def _add_parent(self):
499
285
new_parents = self.this_tree.get_parent_ids() + [self.other_rev_id]
500
286
new_parent_trees = []
501
operation = cleanup.OperationWithCleanups(
502
self.this_tree.set_parent_trees)
503
287
for revision_id in new_parents:
505
289
tree = self.revision_tree(revision_id)
592
380
self.base_rev_id = self.revision_graph.find_unique_lca(
594
sorted_lca_keys = self.revision_graph.find_merge_order(
596
if self.base_rev_id == _mod_revision.NULL_REVISION:
597
self.base_rev_id = sorted_lca_keys[0]
599
if self.base_rev_id == _mod_revision.NULL_REVISION:
600
raise errors.UnrelatedBranches()
382
self._is_criss_cross = True
383
if self.base_rev_id == NULL_REVISION:
384
raise UnrelatedBranches()
601
385
if self._is_criss_cross:
602
trace.warning('Warning: criss-cross merge encountered. See bzr'
603
' help criss-cross.')
604
trace.mutter('Criss-cross lcas: %r' % lcas)
605
if self.base_rev_id in lcas:
606
trace.mutter('Unable to find unique lca. '
607
'Fallback %r as best option.' % self.base_rev_id)
608
interesting_revision_ids = set(lcas)
609
interesting_revision_ids.add(self.base_rev_id)
386
warning('Warning: criss-cross merge encountered. See bzr'
387
' help criss-cross.')
388
interesting_revision_ids = [self.base_rev_id]
389
interesting_revision_ids.extend(lcas)
610
390
interesting_trees = dict((t.get_revision_id(), t)
611
391
for t in self.this_branch.repository.revision_trees(
612
392
interesting_revision_ids))
613
393
self._cached_trees.update(interesting_trees)
614
if self.base_rev_id in lcas:
615
self.base_tree = interesting_trees[self.base_rev_id]
617
self.base_tree = interesting_trees.pop(self.base_rev_id)
394
self.base_tree = interesting_trees.pop(self.base_rev_id)
395
sorted_lca_keys = self.revision_graph.find_merge_order(
618
397
self._lca_trees = [interesting_trees[key]
619
398
for key in sorted_lca_keys]
621
400
self.base_tree = self.revision_tree(self.base_rev_id)
622
401
self.base_is_ancestor = True
623
402
self.base_is_other_ancestor = True
624
trace.mutter('Base revid: %r' % self.base_rev_id)
626
404
def set_base(self, base_revision):
627
405
"""Set the base revision to use for the merge.
629
407
:param base_revision: A 2-list containing a path and revision number.
631
trace.mutter("doing merge() with no base_revision specified")
409
mutter("doing merge() with no base_revision specified")
632
410
if base_revision == [None, None]:
747
527
winner_idx = {"this": 2, "other": 1, "conflict": 1}
748
528
supports_lca_trees = True
750
def __init__(self, working_tree, this_tree, base_tree, other_tree,
530
def __init__(self, working_tree, this_tree, base_tree, other_tree,
751
531
interesting_ids=None, reprocess=False, show_base=False,
752
pb=None, pp=None, change_reporter=None,
532
pb=DummyProgress(), pp=None, change_reporter=None,
753
533
interesting_files=None, do_merge=True,
754
cherrypick=False, lca_trees=None, this_branch=None):
534
cherrypick=False, lca_trees=None):
755
535
"""Initialize the merger object and perform the merge.
757
537
:param working_tree: The working tree to apply the merge to
758
538
:param this_tree: The local tree in the merge operation
759
539
:param base_tree: The common tree in the merge operation
760
:param other_tree: The other tree to merge changes from
761
:param this_branch: The branch associated with this_tree. Defaults to
762
this_tree.branch if not supplied.
540
:param other_tree: The other other tree to merge changes from
763
541
:param interesting_ids: The file_ids of files that should be
764
542
participate in the merge. May not be combined with
765
543
interesting_files.
766
544
:param: reprocess If True, perform conflict-reduction processing.
767
545
:param show_base: If True, show the base revision in text conflicts.
768
546
(incompatible with reprocess)
547
:param pb: A Progress bar
770
548
:param pp: A ProgressPhase object
771
549
:param change_reporter: An object that should report changes made
772
550
:param interesting_files: The tree-relative paths of files that should
801
576
# making sure we haven't missed any corner cases.
802
577
# if lca_trees is None:
803
578
# self._lca_trees = [self.base_tree]
804
581
self.change_reporter = change_reporter
805
582
self.cherrypick = cherrypick
584
self.pp = ProgressPhase("Merge phase", 3, self.pb)
809
warnings.warn("pp argument to Merge3Merger is deprecated")
811
warnings.warn("pb argument to Merge3Merger is deprecated")
813
588
def do_merge(self):
814
operation = cleanup.OperationWithCleanups(self._do_merge)
815
589
self.this_tree.lock_tree_write()
816
operation.add_cleanup(self.this_tree.unlock)
817
590
self.base_tree.lock_read()
818
operation.add_cleanup(self.base_tree.unlock)
819
591
self.other_tree.lock_read()
820
operation.add_cleanup(self.other_tree.unlock)
823
def _do_merge(self, operation):
824
self.tt = transform.TreeTransform(self.this_tree, None)
825
operation.add_cleanup(self.tt.finalize)
826
self._compute_transform()
827
results = self.tt.apply(no_conflicts=True)
828
self.write_modified(results)
592
self.tt = TreeTransform(self.this_tree, self.pb)
830
self.this_tree.add_conflicts(self.cooked_conflicts)
831
except errors.UnsupportedOperation:
595
self._compute_transform()
597
results = self.tt.apply(no_conflicts=True)
598
self.write_modified(results)
600
self.this_tree.add_conflicts(self.cooked_conflicts)
601
except UnsupportedOperation:
605
self.other_tree.unlock()
606
self.base_tree.unlock()
607
self.this_tree.unlock()
834
610
def make_preview_transform(self):
835
operation = cleanup.OperationWithCleanups(self._make_preview_transform)
836
611
self.base_tree.lock_read()
837
operation.add_cleanup(self.base_tree.unlock)
838
612
self.other_tree.lock_read()
839
operation.add_cleanup(self.other_tree.unlock)
840
return operation.run_simple()
842
def _make_preview_transform(self):
843
self.tt = transform.TransformPreview(self.this_tree)
844
self._compute_transform()
613
self.tt = TransformPreview(self.this_tree)
616
self._compute_transform()
619
self.other_tree.unlock()
620
self.base_tree.unlock()
847
624
def _compute_transform(self):
1096
@deprecated_method(deprecated_in((2, 4, 0)))
1097
868
def fix_root(self):
1098
if self.tt.final_kind(self.tt.root) is None:
870
self.tt.final_kind(self.tt.root)
1099
872
self.tt.cancel_deletion(self.tt.root)
1100
873
if self.tt.final_file_id(self.tt.root) is None:
1101
self.tt.version_file(self.tt.tree_file_id(self.tt.root),
874
self.tt.version_file(self.tt.tree_file_id(self.tt.root),
876
if self.other_tree.inventory.root is None:
1103
878
other_root_file_id = self.other_tree.get_root_id()
1104
if other_root_file_id is None:
1106
879
other_root = self.tt.trans_id_file_id(other_root_file_id)
1107
880
if other_root == self.tt.root:
1109
if self.this_tree.inventory.has_id(
1110
self.other_tree.inventory.root.file_id):
1111
# the other tree's root is a non-root in the current tree (as
1112
# when a previously unrelated branch is merged into another)
1114
if self.tt.final_kind(other_root) is not None:
1115
other_root_is_present = True
1117
# other_root doesn't have a physical representation. We still need
1118
# to move any references to the actual root of the tree.
1119
other_root_is_present = False
1120
# 'other_tree.inventory.root' is not present in this tree. We are
1121
# calling adjust_path for children which *want* to be present with a
1122
# correct place to go.
1123
for _, child in self.other_tree.inventory.root.children.iteritems():
883
self.tt.final_kind(other_root)
886
if self.other_tree.inventory.root.file_id in self.this_tree.inventory:
887
# the other tree's root is a non-root in the current tree
889
self.reparent_children(self.other_tree.inventory.root, self.tt.root)
890
self.tt.cancel_creation(other_root)
891
self.tt.cancel_versioning(other_root)
893
def reparent_children(self, ie, target):
894
for thing, child in ie.children.iteritems():
1124
895
trans_id = self.tt.trans_id_file_id(child.file_id)
1125
if not other_root_is_present:
1126
if self.tt.final_kind(trans_id) is not None:
1127
# The item exist in the final tree and has a defined place
1130
# Move the item into the root
1132
final_name = self.tt.final_name(trans_id)
1133
except errors.NoFinalPath:
1134
# This file is not present anymore, ignore it.
1136
self.tt.adjust_path(final_name, self.tt.root, trans_id)
1137
if other_root_is_present:
1138
self.tt.cancel_creation(other_root)
1139
self.tt.cancel_versioning(other_root)
896
self.tt.adjust_path(self.tt.final_name(trans_id), target, trans_id)
1141
898
def write_modified(self, results):
1142
899
modified_hashes = {}
1310
1064
parent_id_winner = "other"
1311
1065
if name_winner == "this" and parent_id_winner == "this":
1313
if name_winner == 'conflict' or parent_id_winner == 'conflict':
1314
# Creating helpers (.OTHER or .THIS) here cause problems down the
1315
# road if a ContentConflict needs to be created so we should not do
1317
trans_id = self.tt.trans_id_file_id(file_id)
1318
self._raw_conflicts.append(('path conflict', trans_id, file_id,
1319
this_parent, this_name,
1320
other_parent, other_name))
1321
if not self.other_tree.has_id(file_id):
1322
# it doesn't matter whether the result was 'other' or
1323
# 'conflict'-- if it has no file id, we leave it alone.
1067
if name_winner == "conflict":
1068
trans_id = self.tt.trans_id_file_id(file_id)
1069
self._raw_conflicts.append(('name conflict', trans_id,
1070
this_name, other_name))
1071
if parent_id_winner == "conflict":
1072
trans_id = self.tt.trans_id_file_id(file_id)
1073
self._raw_conflicts.append(('parent conflict', trans_id,
1074
this_parent, other_parent))
1075
if other_name is None:
1076
# it doesn't matter whether the result was 'other' or
1077
# 'conflict'-- if there's no 'other', we leave it alone.
1079
# if we get here, name_winner and parent_winner are set to safe values.
1080
trans_id = self.tt.trans_id_file_id(file_id)
1325
1081
parent_id = parents[self.winner_idx[parent_id_winner]]
1326
name = names[self.winner_idx[name_winner]]
1327
if parent_id is not None or name is not None:
1328
# if we get here, name_winner and parent_winner are set to safe
1330
if parent_id is None and name is not None:
1331
# if parent_id is None and name is non-None, current file is
1333
if names[self.winner_idx[parent_id_winner]] != '':
1334
raise AssertionError(
1335
'File looks like a root, but named %s' %
1336
names[self.winner_idx[parent_id_winner]])
1337
parent_trans_id = transform.ROOT_PARENT
1339
parent_trans_id = self.tt.trans_id_file_id(parent_id)
1340
self.tt.adjust_path(name, parent_trans_id,
1341
self.tt.trans_id_file_id(file_id))
1082
if parent_id is not None:
1083
parent_trans_id = self.tt.trans_id_file_id(parent_id)
1084
self.tt.adjust_path(names[self.winner_idx[name_winner]],
1085
parent_trans_id, trans_id)
1343
def _do_merge_contents(self, file_id):
1344
"""Performs a merge on file_id contents."""
1087
def merge_contents(self, file_id):
1088
"""Performa a merge on file_id contents."""
1345
1089
def contents_pair(tree):
1346
if not tree.has_id(file_id):
1090
if file_id not in tree:
1347
1091
return (None, None)
1348
1092
kind = tree.kind(file_id)
1349
1093
if kind == "file":
1354
1098
contents = None
1355
1099
return kind, contents
1101
def contents_conflict():
1102
trans_id = self.tt.trans_id_file_id(file_id)
1103
name = self.tt.final_name(trans_id)
1104
parent_id = self.tt.final_parent(trans_id)
1105
if file_id in self.this_tree.inventory:
1106
self.tt.unversion_file(trans_id)
1107
if file_id in self.this_tree:
1108
self.tt.delete_contents(trans_id)
1109
file_group = self._dump_conflicts(name, parent_id, file_id,
1111
self._raw_conflicts.append(('contents conflict', file_group))
1357
1113
# See SPOT run. run, SPOT, run.
1358
1114
# So we're not QUITE repeating ourselves; we do tricky things with
1360
1116
base_pair = contents_pair(self.base_tree)
1361
1117
other_pair = contents_pair(self.other_tree)
1363
this_pair = contents_pair(self.this_tree)
1364
lca_pairs = [contents_pair(tree) for tree in self._lca_trees]
1365
winner = self._lca_multi_way((base_pair, lca_pairs), other_pair,
1366
this_pair, allow_overriding_lca=False)
1368
if base_pair == other_pair:
1371
# We delayed evaluating this_pair as long as we can to avoid
1372
# unnecessary sha1 calculation
1373
this_pair = contents_pair(self.this_tree)
1374
winner = self._three_way(base_pair, other_pair, this_pair)
1375
if winner == 'this':
1376
# No interesting changes introduced by OTHER
1378
# We have a hypothetical conflict, but if we have files, then we
1379
# can try to merge the content
1380
trans_id = self.tt.trans_id_file_id(file_id)
1381
params = MergeHookParams(self, file_id, trans_id, this_pair[0],
1382
other_pair[0], winner)
1383
hooks = self.active_hooks
1384
hook_status = 'not_applicable'
1386
hook_status, lines = hook.merge_contents(params)
1387
if hook_status != 'not_applicable':
1388
# Don't try any more hooks, this one applies.
1391
if hook_status == 'not_applicable':
1392
# This is a contents conflict, because none of the available
1393
# functions could merge it.
1395
name = self.tt.final_name(trans_id)
1396
parent_id = self.tt.final_parent(trans_id)
1397
if self.this_tree.has_id(file_id):
1398
self.tt.unversion_file(trans_id)
1399
file_group = self._dump_conflicts(name, parent_id, file_id,
1401
self._raw_conflicts.append(('contents conflict', file_group))
1402
elif hook_status == 'success':
1403
self.tt.create_file(lines, trans_id)
1404
elif hook_status == 'conflicted':
1405
# XXX: perhaps the hook should be able to provide
1406
# the BASE/THIS/OTHER files?
1407
self.tt.create_file(lines, trans_id)
1408
self._raw_conflicts.append(('text conflict', trans_id))
1409
name = self.tt.final_name(trans_id)
1410
parent_id = self.tt.final_parent(trans_id)
1411
self._dump_conflicts(name, parent_id, file_id)
1412
elif hook_status == 'delete':
1413
self.tt.unversion_file(trans_id)
1415
elif hook_status == 'done':
1416
# The hook function did whatever it needs to do directly, no
1417
# further action needed here.
1420
raise AssertionError('unknown hook_status: %r' % (hook_status,))
1421
if not self.this_tree.has_id(file_id) and result == "modified":
1422
self.tt.version_file(file_id, trans_id)
1423
# The merge has been performed, so the old contents should not be
1425
self.tt.delete_contents(trans_id)
1428
def _default_other_winner_merge(self, merge_hook_params):
1429
"""Replace this contents with other."""
1430
file_id = merge_hook_params.file_id
1431
trans_id = merge_hook_params.trans_id
1432
file_in_this = self.this_tree.has_id(file_id)
1433
if self.other_tree.has_id(file_id):
1434
# OTHER changed the file
1436
if wt.supports_content_filtering():
1437
# We get the path from the working tree if it exists.
1438
# That fails though when OTHER is adding a file, so
1439
# we fall back to the other tree to find the path if
1440
# it doesn't exist locally.
1442
filter_tree_path = wt.id2path(file_id)
1443
except errors.NoSuchId:
1444
filter_tree_path = self.other_tree.id2path(file_id)
1446
# Skip the id2path lookup for older formats
1447
filter_tree_path = None
1448
transform.create_from_tree(self.tt, trans_id,
1449
self.other_tree, file_id,
1450
filter_tree_path=filter_tree_path)
1453
# OTHER deleted the file
1454
return 'delete', None
1456
raise AssertionError(
1457
'winner is OTHER, but file_id %r not in THIS or OTHER tree'
1460
def merge_contents(self, merge_hook_params):
1461
"""Fallback merge logic after user installed hooks."""
1462
# This function is used in merge hooks as the fallback instance.
1463
# Perhaps making this function and the functions it calls be a
1464
# a separate class would be better.
1465
if merge_hook_params.winner == 'other':
1466
# OTHER is a straight winner, so replace this contents with other
1467
return self._default_other_winner_merge(merge_hook_params)
1468
elif merge_hook_params.is_file_merge():
1469
# THIS and OTHER are both files, so text merge. Either
1470
# BASE is a file, or both converted to files, so at least we
1471
# have agreement that output should be a file.
1473
self.text_merge(merge_hook_params.file_id,
1474
merge_hook_params.trans_id)
1475
except errors.BinaryFile:
1476
return 'not_applicable', None
1479
return 'not_applicable', None
1118
if base_pair == other_pair:
1119
# OTHER introduced no changes
1121
this_pair = contents_pair(self.this_tree)
1122
if this_pair == other_pair:
1123
# THIS and OTHER introduced the same changes
1126
trans_id = self.tt.trans_id_file_id(file_id)
1127
if this_pair == base_pair:
1128
# only OTHER introduced changes
1129
if file_id in self.this_tree:
1130
# Remove any existing contents
1131
self.tt.delete_contents(trans_id)
1132
if file_id in self.other_tree:
1133
# OTHER changed the file
1134
create_by_entry(self.tt,
1135
self.other_tree.inventory[file_id],
1136
self.other_tree, trans_id)
1137
if file_id not in self.this_tree:
1138
self.tt.version_file(file_id, trans_id)
1140
elif file_id in self.this_tree.inventory:
1141
# OTHER deleted the file
1142
self.tt.unversion_file(trans_id)
1144
#BOTH THIS and OTHER introduced changes; scalar conflict
1145
elif this_pair[0] == "file" and other_pair[0] == "file":
1146
# THIS and OTHER are both files, so text merge. Either
1147
# BASE is a file, or both converted to files, so at least we
1148
# have agreement that output should be a file.
1150
self.text_merge(file_id, trans_id)
1152
return contents_conflict()
1153
if file_id not in self.this_tree:
1154
self.tt.version_file(file_id, trans_id)
1156
self.tt.tree_kind(trans_id)
1157
self.tt.delete_contents(trans_id)
1162
# Scalar conflict, can't text merge. Dump conflicts
1163
return contents_conflict()
1481
1165
def get_lines(self, tree, file_id):
1482
1166
"""Return the lines in a file, or an empty list."""
1483
if tree.has_id(file_id):
1484
return tree.get_file_lines(file_id)
1168
return tree.get_file(file_id).readlines()
1537
1221
determined automatically. If set_version is true, the .OTHER, .THIS
1538
1222
or .BASE (in that order) will be created as versioned files.
1540
data = [('OTHER', self.other_tree, other_lines),
1224
data = [('OTHER', self.other_tree, other_lines),
1541
1225
('THIS', self.this_tree, this_lines)]
1542
1226
if not no_base:
1543
1227
data.append(('BASE', self.base_tree, base_lines))
1545
# We need to use the actual path in the working tree of the file here,
1546
# ignoring the conflict suffixes
1548
if wt.supports_content_filtering():
1550
filter_tree_path = wt.id2path(file_id)
1551
except errors.NoSuchId:
1552
# file has been deleted
1553
filter_tree_path = None
1555
# Skip the id2path lookup for older formats
1556
filter_tree_path = None
1558
1228
versioned = False
1559
1229
file_group = []
1560
1230
for suffix, tree, lines in data:
1561
if tree.has_id(file_id):
1562
1232
trans_id = self._conflict_file(name, parent_id, tree, file_id,
1563
suffix, lines, filter_tree_path)
1564
1234
file_group.append(trans_id)
1565
1235
if set_version and not versioned:
1566
1236
self.tt.version_file(file_id, trans_id)
1567
1237
versioned = True
1568
1238
return file_group
1570
def _conflict_file(self, name, parent_id, tree, file_id, suffix,
1571
lines=None, filter_tree_path=None):
1240
def _conflict_file(self, name, parent_id, tree, file_id, suffix,
1572
1242
"""Emit a single conflict file."""
1573
1243
name = name + '.' + suffix
1574
1244
trans_id = self.tt.create_path(name, parent_id)
1575
transform.create_from_tree(self.tt, trans_id, tree, file_id, lines,
1245
entry = tree.inventory[file_id]
1246
create_by_entry(self.tt, entry, tree, trans_id, lines)
1577
1247
return trans_id
1579
1249
def merge_executable(self, file_id, file_status):
1618
1291
def cook_conflicts(self, fs_conflicts):
1619
1292
"""Convert all conflicts into a form that doesn't depend on trans_id"""
1620
content_conflict_file_ids = set()
1621
cooked_conflicts = transform.cook_conflicts(fs_conflicts, self.tt)
1622
fp = transform.FinalPaths(self.tt)
1293
from conflicts import Conflict
1295
self.cooked_conflicts.extend(cook_conflicts(fs_conflicts, self.tt))
1296
fp = FinalPaths(self.tt)
1623
1297
for conflict in self._raw_conflicts:
1624
1298
conflict_type = conflict[0]
1625
if conflict_type == 'path conflict':
1627
this_parent, this_name,
1628
other_parent, other_name) = conflict[1:]
1629
if this_parent is None or this_name is None:
1630
this_path = '<deleted>'
1632
parent_path = fp.get_path(
1633
self.tt.trans_id_file_id(this_parent))
1634
this_path = osutils.pathjoin(parent_path, this_name)
1635
if other_parent is None or other_name is None:
1636
other_path = '<deleted>'
1638
if other_parent == self.other_tree.get_root_id():
1639
# The tree transform doesn't know about the other root,
1640
# so we special case here to avoid a NoFinalPath
1644
parent_path = fp.get_path(
1645
self.tt.trans_id_file_id(other_parent))
1646
other_path = osutils.pathjoin(parent_path, other_name)
1647
c = _mod_conflicts.Conflict.factory(
1648
'path conflict', path=this_path,
1649
conflict_path=other_path,
1651
elif conflict_type == 'contents conflict':
1299
if conflict_type in ('name conflict', 'parent conflict'):
1300
trans_id = conflict[1]
1301
conflict_args = conflict[2:]
1302
if trans_id not in name_conflicts:
1303
name_conflicts[trans_id] = {}
1304
unique_add(name_conflicts[trans_id], conflict_type,
1306
if conflict_type == 'contents conflict':
1652
1307
for trans_id in conflict[1]:
1653
1308
file_id = self.tt.final_file_id(trans_id)
1654
1309
if file_id is not None:
1658
1313
if path.endswith(suffix):
1659
1314
path = path[:-len(suffix)]
1661
c = _mod_conflicts.Conflict.factory(conflict_type,
1662
path=path, file_id=file_id)
1663
content_conflict_file_ids.add(file_id)
1664
elif conflict_type == 'text conflict':
1316
c = Conflict.factory(conflict_type, path=path, file_id=file_id)
1317
self.cooked_conflicts.append(c)
1318
if conflict_type == 'text conflict':
1665
1319
trans_id = conflict[1]
1666
1320
path = fp.get_path(trans_id)
1667
1321
file_id = self.tt.final_file_id(trans_id)
1668
c = _mod_conflicts.Conflict.factory(conflict_type,
1669
path=path, file_id=file_id)
1322
c = Conflict.factory(conflict_type, path=path, file_id=file_id)
1323
self.cooked_conflicts.append(c)
1325
for trans_id, conflicts in name_conflicts.iteritems():
1327
this_parent, other_parent = conflicts['parent conflict']
1328
if this_parent == other_parent:
1329
raise AssertionError()
1331
this_parent = other_parent = \
1332
self.tt.final_file_id(self.tt.final_parent(trans_id))
1334
this_name, other_name = conflicts['name conflict']
1335
if this_name == other_name:
1336
raise AssertionError()
1338
this_name = other_name = self.tt.final_name(trans_id)
1339
other_path = fp.get_path(trans_id)
1340
if this_parent is not None and this_name is not None:
1341
this_parent_path = \
1342
fp.get_path(self.tt.trans_id_file_id(this_parent))
1343
this_path = pathjoin(this_parent_path, this_name)
1671
raise AssertionError('bad conflict type: %r' % (conflict,))
1672
cooked_conflicts.append(c)
1674
self.cooked_conflicts = []
1675
# We want to get rid of path conflicts when a corresponding contents
1676
# conflict exists. This can occur when one branch deletes a file while
1677
# the other renames *and* modifies it. In this case, the content
1678
# conflict is enough.
1679
for c in cooked_conflicts:
1680
if (c.typestring == 'path conflict'
1681
and c.file_id in content_conflict_file_ids):
1345
this_path = "<deleted>"
1346
file_id = self.tt.final_file_id(trans_id)
1347
c = Conflict.factory('path conflict', path=this_path,
1348
conflict_path=other_path, file_id=file_id)
1683
1349
self.cooked_conflicts.append(c)
1684
self.cooked_conflicts.sort(key=_mod_conflicts.Conflict.sort_key)
1350
self.cooked_conflicts.sort(key=Conflict.sort_key)
1687
1353
class WeaveMerger(Merge3Merger):
1691
1357
supports_reverse_cherrypick = False
1692
1358
history_based = True
1694
def _generate_merge_plan(self, file_id, base):
1695
return self.this_tree.plan_file_merge(file_id, self.other_tree,
1360
def _merged_lines(self, file_id):
1361
"""Generate the merged lines.
1362
There is no distinction between lines that are meant to contain <<<<<<<
1366
base = self.base_tree
1369
plan = self.this_tree.plan_file_merge(file_id, self.other_tree,
1698
def _merged_lines(self, file_id):
1699
"""Generate the merged lines.
1700
There is no distinction between lines that are meant to contain <<<<<<<
1704
base = self.base_tree
1707
plan = self._generate_merge_plan(file_id, base)
1708
1371
if 'merge' in debug.debug_flags:
1709
1372
plan = list(plan)
1710
1373
trans_id = self.tt.trans_id_file_id(file_id)
1711
1374
name = self.tt.final_name(trans_id) + '.plan'
1712
contents = ('%11s|%s' % l for l in plan)
1375
contents = ('%10s|%s' % l for l in plan)
1713
1376
self.tt.new_file(name, self.tt.final_parent(trans_id), contents)
1714
textmerge = versionedfile.PlanWeaveMerge(plan, '<<<<<<< TREE\n',
1715
'>>>>>>> MERGE-SOURCE\n')
1716
lines, conflicts = textmerge.merge_lines(self.reprocess)
1718
base_lines = textmerge.base_from_plan()
1721
return lines, base_lines
1377
textmerge = PlanWeaveMerge(plan, '<<<<<<< TREE\n',
1378
'>>>>>>> MERGE-SOURCE\n')
1379
return textmerge.merge_lines(self.reprocess)
1723
1381
def text_merge(self, file_id, trans_id):
1724
1382
"""Perform a (weave) text merge for a given file and file-id.
1725
1383
If conflicts are encountered, .THIS and .OTHER files will be emitted,
1726
1384
and a conflict will be noted.
1728
lines, base_lines = self._merged_lines(file_id)
1386
lines, conflicts = self._merged_lines(file_id)
1729
1387
lines = list(lines)
1730
# Note we're checking whether the OUTPUT is binary in this case,
1388
# Note we're checking whether the OUTPUT is binary in this case,
1731
1389
# because we don't want to get into weave merge guts.
1732
textfile.check_text_lines(lines)
1390
check_text_lines(lines)
1733
1391
self.tt.create_file(lines, trans_id)
1734
if base_lines is not None:
1736
1393
self._raw_conflicts.append(('text conflict', trans_id))
1737
1394
name = self.tt.final_name(trans_id)
1738
1395
parent_id = self.tt.final_parent(trans_id)
1739
file_group = self._dump_conflicts(name, parent_id, file_id,
1741
base_lines=base_lines)
1396
file_group = self._dump_conflicts(name, parent_id, file_id,
1742
1398
file_group.append(trans_id)
1745
1401
class LCAMerger(WeaveMerger):
1747
def _generate_merge_plan(self, file_id, base):
1748
return self.this_tree.plan_file_lca_merge(file_id, self.other_tree,
1403
def _merged_lines(self, file_id):
1404
"""Generate the merged lines.
1405
There is no distinction between lines that are meant to contain <<<<<<<
1409
base = self.base_tree
1412
plan = self.this_tree.plan_file_lca_merge(file_id, self.other_tree,
1414
if 'merge' in debug.debug_flags:
1416
trans_id = self.tt.trans_id_file_id(file_id)
1417
name = self.tt.final_name(trans_id) + '.plan'
1418
contents = ('%10s|%s' % l for l in plan)
1419
self.tt.new_file(name, self.tt.final_parent(trans_id), contents)
1420
textmerge = PlanWeaveMerge(plan, '<<<<<<< TREE\n',
1421
'>>>>>>> MERGE-SOURCE\n')
1422
return textmerge.merge_lines(self.reprocess)
1751
1425
class Diff3Merger(Merge3Merger):
1752
1426
"""Three-way merger using external diff3 for text merging"""
1754
1428
def dump_file(self, temp_dir, name, tree, file_id):
1755
out_path = osutils.pathjoin(temp_dir, name)
1429
out_path = pathjoin(temp_dir, name)
1756
1430
out_file = open(out_path, "wb")
1758
1432
in_file = tree.get_file(file_id)
1791
1465
osutils.rmtree(temp_dir)
1794
class PathNotInTree(errors.BzrError):
1796
_fmt = """Merge-into failed because %(tree)s does not contain %(path)s."""
1798
def __init__(self, path, tree):
1799
errors.BzrError.__init__(self, path=path, tree=tree)
1802
class MergeIntoMerger(Merger):
1803
"""Merger that understands other_tree will be merged into a subdir.
1805
This also changes the Merger api so that it uses real Branch, revision_id,
1806
and RevisonTree objects, rather than using revision specs.
1809
def __init__(self, this_tree, other_branch, other_tree, target_subdir,
1810
source_subpath, other_rev_id=None):
1811
"""Create a new MergeIntoMerger object.
1813
source_subpath in other_tree will be effectively copied to
1814
target_subdir in this_tree.
1816
:param this_tree: The tree that we will be merging into.
1817
:param other_branch: The Branch we will be merging from.
1818
:param other_tree: The RevisionTree object we want to merge.
1819
:param target_subdir: The relative path where we want to merge
1820
other_tree into this_tree
1821
:param source_subpath: The relative path specifying the subtree of
1822
other_tree to merge into this_tree.
1824
# It is assumed that we are merging a tree that is not in our current
1825
# ancestry, which means we are using the "EmptyTree" as our basis.
1826
null_ancestor_tree = this_tree.branch.repository.revision_tree(
1827
_mod_revision.NULL_REVISION)
1828
super(MergeIntoMerger, self).__init__(
1829
this_branch=this_tree.branch,
1830
this_tree=this_tree,
1831
other_tree=other_tree,
1832
base_tree=null_ancestor_tree,
1834
self._target_subdir = target_subdir
1835
self._source_subpath = source_subpath
1836
self.other_branch = other_branch
1837
if other_rev_id is None:
1838
other_rev_id = other_tree.get_revision_id()
1839
self.other_rev_id = self.other_basis = other_rev_id
1840
self.base_is_ancestor = True
1841
self.backup_files = True
1842
self.merge_type = Merge3Merger
1843
self.show_base = False
1844
self.reprocess = False
1845
self.interesting_ids = None
1846
self.merge_type = _MergeTypeParameterizer(MergeIntoMergeType,
1847
target_subdir=self._target_subdir,
1848
source_subpath=self._source_subpath)
1849
if self._source_subpath != '':
1850
# If this isn't a partial merge make sure the revisions will be
1852
self._maybe_fetch(self.other_branch, self.this_branch,
1855
def set_pending(self):
1856
if self._source_subpath != '':
1858
Merger.set_pending(self)
1861
class _MergeTypeParameterizer(object):
1862
"""Wrap a merge-type class to provide extra parameters.
1864
This is hack used by MergeIntoMerger to pass some extra parameters to its
1865
merge_type. Merger.do_merge() sets up its own set of parameters to pass to
1866
the 'merge_type' member. It is difficult override do_merge without
1867
re-writing the whole thing, so instead we create a wrapper which will pass
1868
the extra parameters.
1871
def __init__(self, merge_type, **kwargs):
1872
self._extra_kwargs = kwargs
1873
self._merge_type = merge_type
1875
def __call__(self, *args, **kwargs):
1876
kwargs.update(self._extra_kwargs)
1877
return self._merge_type(*args, **kwargs)
1879
def __getattr__(self, name):
1880
return getattr(self._merge_type, name)
1883
class MergeIntoMergeType(Merge3Merger):
1884
"""Merger that incorporates a tree (or part of a tree) into another."""
1886
def __init__(self, *args, **kwargs):
1887
"""Initialize the merger object.
1889
:param args: See Merge3Merger.__init__'s args.
1890
:param kwargs: See Merge3Merger.__init__'s keyword args, except for
1891
source_subpath and target_subdir.
1892
:keyword source_subpath: The relative path specifying the subtree of
1893
other_tree to merge into this_tree.
1894
:keyword target_subdir: The relative path where we want to merge
1895
other_tree into this_tree
1897
# All of the interesting work happens during Merge3Merger.__init__(),
1898
# so we have have to hack in to get our extra parameters set.
1899
self._source_subpath = kwargs.pop('source_subpath')
1900
self._target_subdir = kwargs.pop('target_subdir')
1901
super(MergeIntoMergeType, self).__init__(*args, **kwargs)
1903
def _compute_transform(self):
1904
child_pb = ui.ui_factory.nested_progress_bar()
1906
entries = self._entries_to_incorporate()
1907
entries = list(entries)
1908
for num, (entry, parent_id) in enumerate(entries):
1909
child_pb.update('Preparing file merge', num, len(entries))
1910
parent_trans_id = self.tt.trans_id_file_id(parent_id)
1911
trans_id = transform.new_by_entry(self.tt, entry,
1912
parent_trans_id, self.other_tree)
1915
self._finish_computing_transform()
1917
def _entries_to_incorporate(self):
1918
"""Yields pairs of (inventory_entry, new_parent)."""
1919
other_inv = self.other_tree.inventory
1920
subdir_id = other_inv.path2id(self._source_subpath)
1921
if subdir_id is None:
1922
# XXX: The error would be clearer if it gave the URL of the source
1923
# branch, but we don't have a reference to that here.
1924
raise PathNotInTree(self._source_subpath, "Source tree")
1925
subdir = other_inv[subdir_id]
1926
parent_in_target = osutils.dirname(self._target_subdir)
1927
target_id = self.this_tree.inventory.path2id(parent_in_target)
1928
if target_id is None:
1929
raise PathNotInTree(self._target_subdir, "Target tree")
1930
name_in_target = osutils.basename(self._target_subdir)
1931
merge_into_root = subdir.copy()
1932
merge_into_root.name = name_in_target
1933
if self.this_tree.inventory.has_id(merge_into_root.file_id):
1934
# Give the root a new file-id.
1935
# This can happen fairly easily if the directory we are
1936
# incorporating is the root, and both trees have 'TREE_ROOT' as
1937
# their root_id. Users will expect this to Just Work, so we
1938
# change the file-id here.
1939
# Non-root file-ids could potentially conflict too. That's really
1940
# an edge case, so we don't do anything special for those. We let
1941
# them cause conflicts.
1942
merge_into_root.file_id = generate_ids.gen_file_id(name_in_target)
1943
yield (merge_into_root, target_id)
1944
if subdir.kind != 'directory':
1945
# No children, so we are done.
1947
for ignored_path, entry in other_inv.iter_entries_by_dir(subdir_id):
1948
parent_id = entry.parent_id
1949
if parent_id == subdir.file_id:
1950
# The root's parent ID has changed, so make sure children of
1951
# the root refer to the new ID.
1952
parent_id = merge_into_root.file_id
1953
yield (entry, parent_id)
1956
1468
def merge_inner(this_branch, other_tree, base_tree, ignore_zero=False,
1957
1469
backup_files=False,
1958
1470
merge_type=Merge3Merger,