1
# Copyright (C) 2005-2011 Canonical Ltd
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
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
17
from __future__ import absolute_import
21
from .lazy_import import lazy_import
22
lazy_import(globals(), """
24
branch as _mod_branch,
26
conflicts as _mod_conflicts,
33
revision as _mod_revision,
42
from breezy.bzr import (
45
from breezy.i18n import gettext
56
# TODO: Report back as changes are merged in
59
def transform_tree(from_tree, to_tree, interesting_ids=None):
60
from_tree.lock_tree_write()
61
operation = cleanup.OperationWithCleanups(merge_inner)
62
operation.add_cleanup(from_tree.unlock)
63
operation.run_simple(from_tree.branch, to_tree, from_tree,
64
ignore_zero=True, interesting_ids=interesting_ids, this_tree=from_tree)
67
class MergeHooks(hooks.Hooks):
70
hooks.Hooks.__init__(self, "breezy.merge", "Merger.hooks")
71
self.add_hook('merge_file_content',
72
"Called with a breezy.merge.Merger object to create a per file "
73
"merge object when starting a merge. "
74
"Should return either None or a subclass of "
75
"``breezy.merge.AbstractPerFileMerger``. "
76
"Such objects will then be called per file "
77
"that needs to be merged (including when one "
78
"side has deleted the file and the other has changed it). "
79
"See the AbstractPerFileMerger API docs for details on how it is "
82
self.add_hook('pre_merge',
83
'Called before a merge. '
84
'Receives a Merger object as the single argument.',
86
self.add_hook('post_merge',
87
'Called after a merge. '
88
'Receives a Merger object as the single argument. '
89
'The return value is ignored.',
93
class AbstractPerFileMerger(object):
94
"""PerFileMerger objects are used by plugins extending merge for breezy.
96
See ``breezy.plugins.news_merge.news_merge`` for an example concrete class.
98
:ivar merger: The Merge3Merger performing the merge.
101
def __init__(self, merger):
102
"""Create a PerFileMerger for use with merger."""
105
def merge_contents(self, merge_params):
106
"""Attempt to merge the contents of a single file.
108
:param merge_params: A breezy.merge.MergeFileHookParams
109
:return: A tuple of (status, chunks), where status is one of
110
'not_applicable', 'success', 'conflicted', or 'delete'. If status
111
is 'success' or 'conflicted', then chunks should be an iterable of
112
strings for the new file contents.
114
return ('not applicable', None)
117
class PerFileMerger(AbstractPerFileMerger):
118
"""Merge individual files when self.file_matches returns True.
120
This class is intended to be subclassed. The file_matches and
121
merge_matching methods should be overridden with concrete implementations.
124
def file_matches(self, params):
125
"""Return True if merge_matching should be called on this file.
127
Only called with merges of plain files with no clear winner.
129
Subclasses must override this.
131
raise NotImplementedError(self.file_matches)
133
def get_filename(self, params, tree):
134
"""Lookup the filename (i.e. basename, not path), given a Tree (e.g.
135
self.merger.this_tree) and a MergeFileHookParams.
137
return osutils.basename(tree.id2path(params.file_id))
139
def get_filepath(self, params, tree):
140
"""Calculate the path to the file in a tree.
142
:param params: A MergeFileHookParams describing the file to merge
143
:param tree: a Tree, e.g. self.merger.this_tree.
145
return tree.id2path(params.file_id)
147
def merge_contents(self, params):
148
"""Merge the contents of a single file."""
149
# Check whether this custom merge logic should be used.
151
# OTHER is a straight winner, rely on default merge.
152
params.winner == 'other' or
153
# THIS and OTHER aren't both files.
154
not params.is_file_merge() or
155
# The filename doesn't match
156
not self.file_matches(params)):
157
return 'not_applicable', None
158
return self.merge_matching(params)
160
def merge_matching(self, params):
161
"""Merge the contents of a single file that has matched the criteria
162
in PerFileMerger.merge_contents (is a conflict, is a file,
163
self.file_matches is True).
165
Subclasses must override this.
167
raise NotImplementedError(self.merge_matching)
170
class ConfigurableFileMerger(PerFileMerger):
171
"""Merge individual files when configured via a .conf file.
173
This is a base class for concrete custom file merging logic. Concrete
174
classes should implement ``merge_text``.
176
See ``breezy.plugins.news_merge.news_merge`` for an example concrete class.
178
:ivar affected_files: The configured file paths to merge.
180
:cvar name_prefix: The prefix to use when looking up configuration
181
details. <name_prefix>_merge_files describes the files targeted by the
184
:cvar default_files: The default file paths to merge when no configuration
191
def __init__(self, merger):
192
super(ConfigurableFileMerger, self).__init__(merger)
193
self.affected_files = None
194
self.default_files = self.__class__.default_files or []
195
self.name_prefix = self.__class__.name_prefix
196
if self.name_prefix is None:
197
raise ValueError("name_prefix must be set.")
199
def file_matches(self, params):
200
"""Check whether the file should call the merge hook.
202
<name_prefix>_merge_files configuration variable is a list of files
203
that should use the hook.
205
affected_files = self.affected_files
206
if affected_files is None:
207
config = self.merger.this_branch.get_config()
208
# Until bzr provides a better policy for caching the config, we
209
# just add the part we're interested in to the params to avoid
210
# reading the config files repeatedly (breezy.conf, location.conf,
212
config_key = self.name_prefix + '_merge_files'
213
affected_files = config.get_user_option_as_list(config_key)
214
if affected_files is None:
215
# If nothing was specified in the config, use the default.
216
affected_files = self.default_files
217
self.affected_files = affected_files
219
filepath = self.get_filepath(params, self.merger.this_tree)
220
if filepath in affected_files:
224
def merge_matching(self, params):
225
return self.merge_text(params)
227
def merge_text(self, params):
228
"""Merge the byte contents of a single file.
230
This is called after checking that the merge should be performed in
231
merge_contents, and it should behave as per
232
``breezy.merge.AbstractPerFileMerger.merge_contents``.
234
raise NotImplementedError(self.merge_text)
237
class MergeFileHookParams(object):
238
"""Object holding parameters passed to merge_file_content hooks.
240
There are some fields hooks can access:
242
:ivar file_id: the file ID of the file being merged
243
:ivar trans_id: the transform ID for the merge of this file
244
:ivar this_kind: kind of file_id in 'this' tree
245
:ivar other_kind: kind of file_id in 'other' tree
246
:ivar winner: one of 'this', 'other', 'conflict'
249
def __init__(self, merger, file_id, trans_id, this_kind, other_kind,
251
self._merger = merger
252
self.file_id = file_id
253
self.trans_id = trans_id
254
self.this_kind = this_kind
255
self.other_kind = other_kind
258
def is_file_merge(self):
259
"""True if this_kind and other_kind are both 'file'."""
260
return self.this_kind == 'file' and self.other_kind == 'file'
262
@decorators.cachedproperty
263
def base_lines(self):
264
"""The lines of the 'base' version of the file."""
265
return self._merger.get_lines(self._merger.base_tree, self.file_id)
267
@decorators.cachedproperty
268
def this_lines(self):
269
"""The lines of the 'this' version of the file."""
270
return self._merger.get_lines(self._merger.this_tree, self.file_id)
272
@decorators.cachedproperty
273
def other_lines(self):
274
"""The lines of the 'other' version of the file."""
275
return self._merger.get_lines(self._merger.other_tree, self.file_id)
278
class Merger(object):
282
def __init__(self, this_branch, other_tree=None, base_tree=None,
283
this_tree=None, change_reporter=None,
284
recurse='down', revision_graph=None):
285
object.__init__(self)
286
self.this_branch = this_branch
287
self.this_basis = _mod_revision.ensure_null(
288
this_branch.last_revision())
289
self.this_rev_id = None
290
self.this_tree = this_tree
291
self.this_revision_tree = None
292
self.this_basis_tree = None
293
self.other_tree = other_tree
294
self.other_branch = None
295
self.base_tree = base_tree
296
self.ignore_zero = False
297
self.backup_files = False
298
self.interesting_ids = None
299
self.interesting_files = None
300
self.show_base = False
301
self.reprocess = False
303
self.recurse = recurse
304
self.change_reporter = change_reporter
305
self._cached_trees = {}
306
self._revision_graph = revision_graph
307
self._base_is_ancestor = None
308
self._base_is_other_ancestor = None
309
self._is_criss_cross = None
310
self._lca_trees = None
312
def cache_trees_with_revision_ids(self, trees):
313
"""Cache any tree in trees if it has a revision_id."""
314
for maybe_tree in trees:
315
if maybe_tree is None:
318
rev_id = maybe_tree.get_revision_id()
319
except AttributeError:
321
self._cached_trees[rev_id] = maybe_tree
324
def revision_graph(self):
325
if self._revision_graph is None:
326
self._revision_graph = self.this_branch.repository.get_graph()
327
return self._revision_graph
329
def _set_base_is_ancestor(self, value):
330
self._base_is_ancestor = value
332
def _get_base_is_ancestor(self):
333
if self._base_is_ancestor is None:
334
self._base_is_ancestor = self.revision_graph.is_ancestor(
335
self.base_rev_id, self.this_basis)
336
return self._base_is_ancestor
338
base_is_ancestor = property(_get_base_is_ancestor, _set_base_is_ancestor)
340
def _set_base_is_other_ancestor(self, value):
341
self._base_is_other_ancestor = value
343
def _get_base_is_other_ancestor(self):
344
if self._base_is_other_ancestor is None:
345
if self.other_basis is None:
347
self._base_is_other_ancestor = self.revision_graph.is_ancestor(
348
self.base_rev_id, self.other_basis)
349
return self._base_is_other_ancestor
351
base_is_other_ancestor = property(_get_base_is_other_ancestor,
352
_set_base_is_other_ancestor)
355
def from_uncommitted(tree, other_tree, base_tree=None):
356
"""Return a Merger for uncommitted changes in other_tree.
358
:param tree: The tree to merge into
359
:param other_tree: The tree to get uncommitted changes from
360
:param base_tree: The basis to use for the merge. If unspecified,
361
other_tree.basis_tree() will be used.
363
if base_tree is None:
364
base_tree = other_tree.basis_tree()
365
merger = Merger(tree.branch, other_tree, base_tree, tree)
366
merger.base_rev_id = merger.base_tree.get_revision_id()
367
merger.other_rev_id = None
368
merger.other_basis = merger.base_rev_id
372
def from_mergeable(klass, tree, mergeable):
373
"""Return a Merger for a bundle or merge directive.
375
:param tree: The tree to merge changes into
376
:param mergeable: A merge directive or bundle
378
mergeable.install_revisions(tree.branch.repository)
379
base_revision_id, other_revision_id, verified =\
380
mergeable.get_merge_request(tree.branch.repository)
381
revision_graph = tree.branch.repository.get_graph()
382
if base_revision_id is not None:
383
if (base_revision_id != _mod_revision.NULL_REVISION and
384
revision_graph.is_ancestor(
385
base_revision_id, tree.branch.last_revision())):
386
base_revision_id = None
388
trace.warning('Performing cherrypick')
389
merger = klass.from_revision_ids(tree, other_revision_id,
390
base_revision_id, revision_graph=
392
return merger, verified
395
def from_revision_ids(tree, other, base=None, other_branch=None,
396
base_branch=None, revision_graph=None,
398
"""Return a Merger for revision-ids.
400
:param tree: The tree to merge changes into
401
:param other: The revision-id to use as OTHER
402
:param base: The revision-id to use as BASE. If not specified, will
404
:param other_branch: A branch containing the other revision-id. If
405
not supplied, tree.branch is used.
406
:param base_branch: A branch containing the base revision-id. If
407
not supplied, other_branch or tree.branch will be used.
408
:param revision_graph: If you have a revision_graph precomputed, pass
409
it in, otherwise it will be created for you.
410
:param tree_branch: The branch associated with tree. If not supplied,
411
tree.branch will be used.
413
if tree_branch is None:
414
tree_branch = tree.branch
415
merger = Merger(tree_branch, this_tree=tree,
416
revision_graph=revision_graph)
417
if other_branch is None:
418
other_branch = tree.branch
419
merger.set_other_revision(other, other_branch)
423
if base_branch is None:
424
base_branch = other_branch
425
merger.set_base_revision(base, base_branch)
428
def revision_tree(self, revision_id, branch=None):
429
if revision_id not in self._cached_trees:
431
branch = self.this_branch
433
tree = self.this_tree.revision_tree(revision_id)
434
except errors.NoSuchRevisionInTree:
435
tree = branch.repository.revision_tree(revision_id)
436
self._cached_trees[revision_id] = tree
437
return self._cached_trees[revision_id]
439
def _get_tree(self, treespec, possible_transports=None):
440
location, revno = treespec
442
tree = workingtree.WorkingTree.open_containing(location)[0]
443
return tree.branch, tree
444
branch = _mod_branch.Branch.open_containing(
445
location, possible_transports)[0]
447
revision_id = branch.last_revision()
449
revision_id = branch.get_rev_id(revno)
450
revision_id = _mod_revision.ensure_null(revision_id)
451
return branch, self.revision_tree(revision_id, branch)
453
def set_interesting_files(self, file_list):
454
self.interesting_files = file_list
456
def set_pending(self):
457
if (not self.base_is_ancestor or not self.base_is_other_ancestor
458
or self.other_rev_id is None):
462
def _add_parent(self):
463
new_parents = self.this_tree.get_parent_ids() + [self.other_rev_id]
464
new_parent_trees = []
465
operation = cleanup.OperationWithCleanups(
466
self.this_tree.set_parent_trees)
467
for revision_id in new_parents:
469
tree = self.revision_tree(revision_id)
470
except errors.NoSuchRevision:
474
operation.add_cleanup(tree.unlock)
475
new_parent_trees.append((revision_id, tree))
476
operation.run_simple(new_parent_trees, allow_leftmost_as_ghost=True)
478
def set_other(self, other_revision, possible_transports=None):
479
"""Set the revision and tree to merge from.
481
This sets the other_tree, other_rev_id, other_basis attributes.
483
:param other_revision: The [path, revision] list to merge from.
485
self.other_branch, self.other_tree = self._get_tree(other_revision,
487
if other_revision[1] == -1:
488
self.other_rev_id = _mod_revision.ensure_null(
489
self.other_branch.last_revision())
490
if _mod_revision.is_null(self.other_rev_id):
491
raise errors.NoCommits(self.other_branch)
492
self.other_basis = self.other_rev_id
493
elif other_revision[1] is not None:
494
self.other_rev_id = self.other_branch.get_rev_id(other_revision[1])
495
self.other_basis = self.other_rev_id
497
self.other_rev_id = None
498
self.other_basis = self.other_branch.last_revision()
499
if self.other_basis is None:
500
raise errors.NoCommits(self.other_branch)
501
if self.other_rev_id is not None:
502
self._cached_trees[self.other_rev_id] = self.other_tree
503
self._maybe_fetch(self.other_branch, self.this_branch, self.other_basis)
505
def set_other_revision(self, revision_id, other_branch):
506
"""Set 'other' based on a branch and revision id
508
:param revision_id: The revision to use for a tree
509
:param other_branch: The branch containing this tree
511
self.other_rev_id = revision_id
512
self.other_branch = other_branch
513
self._maybe_fetch(other_branch, self.this_branch, self.other_rev_id)
514
self.other_tree = self.revision_tree(revision_id)
515
self.other_basis = revision_id
517
def set_base_revision(self, revision_id, branch):
518
"""Set 'base' based on a branch and revision id
520
:param revision_id: The revision to use for a tree
521
:param branch: The branch containing this tree
523
self.base_rev_id = revision_id
524
self.base_branch = branch
525
self._maybe_fetch(branch, self.this_branch, revision_id)
526
self.base_tree = self.revision_tree(revision_id)
528
def _maybe_fetch(self, source, target, revision_id):
529
if not source.repository.has_same_location(target.repository):
530
target.fetch(source, revision_id)
533
revisions = [_mod_revision.ensure_null(self.this_basis),
534
_mod_revision.ensure_null(self.other_basis)]
535
if _mod_revision.NULL_REVISION in revisions:
536
self.base_rev_id = _mod_revision.NULL_REVISION
537
self.base_tree = self.revision_tree(self.base_rev_id)
538
self._is_criss_cross = False
540
lcas = self.revision_graph.find_lca(revisions[0], revisions[1])
541
self._is_criss_cross = False
543
self.base_rev_id = _mod_revision.NULL_REVISION
545
self.base_rev_id = list(lcas)[0]
546
else: # len(lcas) > 1
547
self._is_criss_cross = True
549
# find_unique_lca can only handle 2 nodes, so we have to
550
# start back at the beginning. It is a shame to traverse
551
# the graph again, but better than re-implementing
553
self.base_rev_id = self.revision_graph.find_unique_lca(
554
revisions[0], revisions[1])
556
self.base_rev_id = self.revision_graph.find_unique_lca(
558
sorted_lca_keys = self.revision_graph.find_merge_order(
560
if self.base_rev_id == _mod_revision.NULL_REVISION:
561
self.base_rev_id = sorted_lca_keys[0]
563
if self.base_rev_id == _mod_revision.NULL_REVISION:
564
raise errors.UnrelatedBranches()
565
if self._is_criss_cross:
566
trace.warning('Warning: criss-cross merge encountered. See bzr'
567
' help criss-cross.')
568
trace.mutter('Criss-cross lcas: %r' % lcas)
569
if self.base_rev_id in lcas:
570
trace.mutter('Unable to find unique lca. '
571
'Fallback %r as best option.'
573
interesting_revision_ids = set(lcas)
574
interesting_revision_ids.add(self.base_rev_id)
575
interesting_trees = dict((t.get_revision_id(), t)
576
for t in self.this_branch.repository.revision_trees(
577
interesting_revision_ids))
578
self._cached_trees.update(interesting_trees)
579
if self.base_rev_id in lcas:
580
self.base_tree = interesting_trees[self.base_rev_id]
582
self.base_tree = interesting_trees.pop(self.base_rev_id)
583
self._lca_trees = [interesting_trees[key]
584
for key in sorted_lca_keys]
586
self.base_tree = self.revision_tree(self.base_rev_id)
587
self.base_is_ancestor = True
588
self.base_is_other_ancestor = True
589
trace.mutter('Base revid: %r' % self.base_rev_id)
591
def set_base(self, base_revision):
592
"""Set the base revision to use for the merge.
594
:param base_revision: A 2-list containing a path and revision number.
596
trace.mutter("doing merge() with no base_revision specified")
597
if base_revision == [None, None]:
600
base_branch, self.base_tree = self._get_tree(base_revision)
601
if base_revision[1] == -1:
602
self.base_rev_id = base_branch.last_revision()
603
elif base_revision[1] is None:
604
self.base_rev_id = _mod_revision.NULL_REVISION
606
self.base_rev_id = _mod_revision.ensure_null(
607
base_branch.get_rev_id(base_revision[1]))
608
self._maybe_fetch(base_branch, self.this_branch, self.base_rev_id)
610
def make_merger(self):
611
kwargs = {'working_tree': self.this_tree, 'this_tree': self.this_tree,
612
'other_tree': self.other_tree,
613
'interesting_ids': self.interesting_ids,
614
'interesting_files': self.interesting_files,
615
'this_branch': self.this_branch,
616
'other_branch': self.other_branch,
618
if self.merge_type.requires_base:
619
kwargs['base_tree'] = self.base_tree
620
if self.merge_type.supports_reprocess:
621
kwargs['reprocess'] = self.reprocess
623
raise errors.BzrError(
624
"Conflict reduction is not supported for merge"
625
" type %s." % self.merge_type)
626
if self.merge_type.supports_show_base:
627
kwargs['show_base'] = self.show_base
629
raise errors.BzrError("Showing base is not supported for this"
630
" merge type. %s" % self.merge_type)
631
if (not getattr(self.merge_type, 'supports_reverse_cherrypick', True)
632
and not self.base_is_other_ancestor):
633
raise errors.CannotReverseCherrypick()
634
if self.merge_type.supports_cherrypick:
635
kwargs['cherrypick'] = (not self.base_is_ancestor or
636
not self.base_is_other_ancestor)
637
if self._is_criss_cross and getattr(self.merge_type,
638
'supports_lca_trees', False):
639
kwargs['lca_trees'] = self._lca_trees
640
return self.merge_type(change_reporter=self.change_reporter,
643
def _do_merge_to(self):
644
merge = self.make_merger()
645
if self.other_branch is not None:
646
self.other_branch.update_references(self.this_branch)
647
for hook in Merger.hooks['pre_merge']:
650
for hook in Merger.hooks['post_merge']:
652
if self.recurse == 'down':
653
for relpath, file_id in self.this_tree.iter_references():
654
sub_tree = self.this_tree.get_nested_tree(relpath, file_id)
655
other_revision = self.other_tree.get_reference_revision(
657
if other_revision == sub_tree.last_revision():
659
sub_merge = Merger(sub_tree.branch, this_tree=sub_tree)
660
sub_merge.merge_type = self.merge_type
661
other_branch = self.other_branch.reference_parent(file_id,
663
sub_merge.set_other_revision(other_revision, other_branch)
664
base_tree_path = self.base_tree.id2path(file_id)
665
base_revision = self.base_tree.get_reference_revision(
666
base_tree_path, file_id)
667
sub_merge.base_tree = \
668
sub_tree.branch.repository.revision_tree(base_revision)
669
sub_merge.base_rev_id = base_revision
674
operation = cleanup.OperationWithCleanups(self._do_merge_to)
675
self.this_tree.lock_tree_write()
676
operation.add_cleanup(self.this_tree.unlock)
677
if self.base_tree is not None:
678
self.base_tree.lock_read()
679
operation.add_cleanup(self.base_tree.unlock)
680
if self.other_tree is not None:
681
self.other_tree.lock_read()
682
operation.add_cleanup(self.other_tree.unlock)
683
merge = operation.run_simple()
684
if len(merge.cooked_conflicts) == 0:
685
if not self.ignore_zero and not trace.is_quiet():
686
trace.note(gettext("All changes applied successfully."))
688
trace.note(gettext("%d conflicts encountered.")
689
% len(merge.cooked_conflicts))
691
return len(merge.cooked_conflicts)
694
class _InventoryNoneEntry(object):
695
"""This represents an inventory entry which *isn't there*.
697
It simplifies the merging logic if we always have an InventoryEntry, even
698
if it isn't actually present
705
symlink_target = None
708
_none_entry = _InventoryNoneEntry()
711
class Merge3Merger(object):
712
"""Three-way merger that uses the merge3 text merger"""
714
supports_reprocess = True
715
supports_show_base = True
716
history_based = False
717
supports_cherrypick = True
718
supports_reverse_cherrypick = True
719
winner_idx = {"this": 2, "other": 1, "conflict": 1}
720
supports_lca_trees = True
722
def __init__(self, working_tree, this_tree, base_tree, other_tree,
723
interesting_ids=None, reprocess=False, show_base=False,
724
change_reporter=None, interesting_files=None, do_merge=True,
725
cherrypick=False, lca_trees=None, this_branch=None,
727
"""Initialize the merger object and perform the merge.
729
:param working_tree: The working tree to apply the merge to
730
:param this_tree: The local tree in the merge operation
731
:param base_tree: The common tree in the merge operation
732
:param other_tree: The other tree to merge changes from
733
:param this_branch: The branch associated with this_tree. Defaults to
734
this_tree.branch if not supplied.
735
:param other_branch: The branch associated with other_tree, if any.
736
:param interesting_ids: The file_ids of files that should be
737
participate in the merge. May not be combined with
739
:param: reprocess If True, perform conflict-reduction processing.
740
:param show_base: If True, show the base revision in text conflicts.
741
(incompatible with reprocess)
742
:param change_reporter: An object that should report changes made
743
:param interesting_files: The tree-relative paths of files that should
744
participate in the merge. If these paths refer to directories,
745
the contents of those directories will also be included. May not
746
be combined with interesting_ids. If neither interesting_files nor
747
interesting_ids is specified, all files may participate in the
749
:param lca_trees: Can be set to a dictionary of {revision_id:rev_tree}
750
if the ancestry was found to include a criss-cross merge.
751
Otherwise should be None.
753
object.__init__(self)
754
if interesting_files is not None and interesting_ids is not None:
756
'specify either interesting_ids or interesting_files')
757
if this_branch is None:
758
this_branch = this_tree.branch
759
self.interesting_ids = interesting_ids
760
self.interesting_files = interesting_files
761
self.working_tree = working_tree
762
self.this_tree = this_tree
763
self.base_tree = base_tree
764
self.other_tree = other_tree
765
self.this_branch = this_branch
766
self.other_branch = other_branch
767
self._raw_conflicts = []
768
self.cooked_conflicts = []
769
self.reprocess = reprocess
770
self.show_base = show_base
771
self._lca_trees = lca_trees
772
# Uncommenting this will change the default algorithm to always use
773
# _entries_lca. This can be useful for running the test suite and
774
# making sure we haven't missed any corner cases.
775
# if lca_trees is None:
776
# self._lca_trees = [self.base_tree]
777
self.change_reporter = change_reporter
778
self.cherrypick = cherrypick
783
operation = cleanup.OperationWithCleanups(self._do_merge)
784
self.working_tree.lock_tree_write()
785
operation.add_cleanup(self.working_tree.unlock)
786
self.this_tree.lock_read()
787
operation.add_cleanup(self.this_tree.unlock)
788
self.base_tree.lock_read()
789
operation.add_cleanup(self.base_tree.unlock)
790
self.other_tree.lock_read()
791
operation.add_cleanup(self.other_tree.unlock)
794
def _do_merge(self, operation):
795
self.tt = transform.TreeTransform(self.working_tree, None)
796
operation.add_cleanup(self.tt.finalize)
797
self._compute_transform()
798
results = self.tt.apply(no_conflicts=True)
799
self.write_modified(results)
801
self.working_tree.add_conflicts(self.cooked_conflicts)
802
except errors.UnsupportedOperation:
805
def make_preview_transform(self):
806
operation = cleanup.OperationWithCleanups(self._make_preview_transform)
807
self.base_tree.lock_read()
808
operation.add_cleanup(self.base_tree.unlock)
809
self.other_tree.lock_read()
810
operation.add_cleanup(self.other_tree.unlock)
811
return operation.run_simple()
813
def _make_preview_transform(self):
814
self.tt = transform.TransformPreview(self.working_tree)
815
self._compute_transform()
818
def _compute_transform(self):
819
if self._lca_trees is None:
820
entries = self._entries3()
821
resolver = self._three_way
823
entries = self._entries_lca()
824
resolver = self._lca_multi_way
825
# Prepare merge hooks
826
factories = Merger.hooks['merge_file_content']
827
# One hook for each registered one plus our default merger
828
hooks = [factory(self) for factory in factories] + [self]
829
self.active_hooks = [hook for hook in hooks if hook is not None]
830
child_pb = ui.ui_factory.nested_progress_bar()
832
for num, (file_id, changed, parents3, names3,
833
executable3) in enumerate(entries):
834
# Try merging each entry
835
child_pb.update(gettext('Preparing file merge'),
837
self._merge_names(file_id, parents3, names3, resolver=resolver)
839
file_status = self._do_merge_contents(file_id)
841
file_status = 'unmodified'
842
self._merge_executable(file_id,
843
executable3, file_status, resolver=resolver)
846
self.tt.fixup_new_roots()
847
self._finish_computing_transform()
849
def _finish_computing_transform(self):
850
"""Finalize the transform and report the changes.
852
This is the second half of _compute_transform.
854
child_pb = ui.ui_factory.nested_progress_bar()
856
fs_conflicts = transform.resolve_conflicts(self.tt, child_pb,
857
lambda t, c: transform.conflict_pass(t, c, self.other_tree))
860
if self.change_reporter is not None:
861
from breezy import delta
862
delta.report_changes(
863
self.tt.iter_changes(), self.change_reporter)
864
self.cook_conflicts(fs_conflicts)
865
for conflict in self.cooked_conflicts:
866
trace.warning(unicode(conflict))
869
"""Gather data about files modified between three trees.
871
Return a list of tuples of file_id, changed, parents3, names3,
872
executable3. changed is a boolean indicating whether the file contents
873
or kind were changed. parents3 is a tuple of parent ids for base,
874
other and this. names3 is a tuple of names for base, other and this.
875
executable3 is a tuple of execute-bit values for base, other and this.
878
iterator = self.other_tree.iter_changes(self.base_tree,
879
specific_files=self.interesting_files,
880
extra_trees=[self.this_tree])
881
this_entries = dict((e.file_id, e) for p, e in
882
self.this_tree.iter_entries_by_dir(
883
self.interesting_ids))
884
for (file_id, paths, changed, versioned, parents, names, kind,
885
executable) in iterator:
886
if (self.interesting_ids is not None and
887
file_id not in self.interesting_ids):
889
entry = this_entries.get(file_id)
890
if entry is not None:
891
this_name = entry.name
892
this_parent = entry.parent_id
893
this_executable = entry.executable
897
this_executable = None
898
parents3 = parents + (this_parent,)
899
names3 = names + (this_name,)
900
executable3 = executable + (this_executable,)
901
result.append((file_id, changed, parents3, names3, executable3))
904
def _entries_lca(self):
905
"""Gather data about files modified between multiple trees.
907
This compares OTHER versus all LCA trees, and for interesting entries,
908
it then compares with THIS and BASE.
910
For the multi-valued entries, the format will be (BASE, [lca1, lca2])
912
:return: [(file_id, changed, parents, names, executable)], where:
914
* file_id: Simple file_id of the entry
915
* changed: Boolean, True if the kind or contents changed else False
916
* parents: ((base, [parent_id, in, lcas]), parent_id_other,
918
* names: ((base, [name, in, lcas]), name_in_other, name_in_this)
919
* executable: ((base, [exec, in, lcas]), exec_in_other,
922
if self.interesting_files is not None:
923
lookup_trees = [self.this_tree, self.base_tree]
924
lookup_trees.extend(self._lca_trees)
925
# I think we should include the lca trees as well
926
interesting_ids = self.other_tree.paths2ids(self.interesting_files,
929
interesting_ids = self.interesting_ids
931
walker = _mod_tree.MultiWalker(self.other_tree, self._lca_trees)
933
base_inventory = self.base_tree.root_inventory
934
this_inventory = self.this_tree.root_inventory
935
for path, file_id, other_ie, lca_values in walker.iter_all():
936
# Is this modified at all from any of the other trees?
938
other_ie = _none_entry
939
if interesting_ids is not None and file_id not in interesting_ids:
942
# If other_revision is found in any of the lcas, that means this
943
# node is uninteresting. This is because when merging, if there are
944
# multiple heads(), we have to create a new node. So if we didn't,
945
# we know that the ancestry is linear, and that OTHER did not
947
# See doc/developers/lca_merge_resolution.txt for details
948
other_revision = other_ie.revision
949
if other_revision is not None:
950
# We can't use this shortcut when other_revision is None,
951
# because it may be None because things are WorkingTrees, and
952
# not because it is *actually* None.
953
is_unmodified = False
954
for lca_path, ie in lca_values:
955
if ie is not None and ie.revision == other_revision:
962
for lca_path, lca_ie in lca_values:
964
lca_entries.append(_none_entry)
966
lca_entries.append(lca_ie)
968
if base_inventory.has_id(file_id):
969
base_ie = base_inventory[file_id]
971
base_ie = _none_entry
973
if this_inventory.has_id(file_id):
974
this_ie = this_inventory[file_id]
976
this_ie = _none_entry
982
for lca_ie in lca_entries:
983
lca_kinds.append(lca_ie.kind)
984
lca_parent_ids.append(lca_ie.parent_id)
985
lca_names.append(lca_ie.name)
986
lca_executable.append(lca_ie.executable)
988
kind_winner = self._lca_multi_way(
989
(base_ie.kind, lca_kinds),
990
other_ie.kind, this_ie.kind)
991
parent_id_winner = self._lca_multi_way(
992
(base_ie.parent_id, lca_parent_ids),
993
other_ie.parent_id, this_ie.parent_id)
994
name_winner = self._lca_multi_way(
995
(base_ie.name, lca_names),
996
other_ie.name, this_ie.name)
998
content_changed = True
999
if kind_winner == 'this':
1000
# No kind change in OTHER, see if there are *any* changes
1001
if other_ie.kind == 'directory':
1002
if parent_id_winner == 'this' and name_winner == 'this':
1003
# No change for this directory in OTHER, skip
1005
content_changed = False
1006
elif other_ie.kind is None or other_ie.kind == 'file':
1007
def get_sha1(ie, tree):
1008
if ie.kind != 'file':
1010
return tree.get_file_sha1(tree.id2path(file_id), file_id)
1011
base_sha1 = get_sha1(base_ie, self.base_tree)
1012
lca_sha1s = [get_sha1(ie, tree) for ie, tree
1013
in zip(lca_entries, self._lca_trees)]
1014
this_sha1 = get_sha1(this_ie, self.this_tree)
1015
other_sha1 = get_sha1(other_ie, self.other_tree)
1016
sha1_winner = self._lca_multi_way(
1017
(base_sha1, lca_sha1s), other_sha1, this_sha1,
1018
allow_overriding_lca=False)
1019
exec_winner = self._lca_multi_way(
1020
(base_ie.executable, lca_executable),
1021
other_ie.executable, this_ie.executable)
1022
if (parent_id_winner == 'this' and name_winner == 'this'
1023
and sha1_winner == 'this' and exec_winner == 'this'):
1024
# No kind, parent, name, exec, or content change for
1025
# OTHER, so this node is not considered interesting
1027
if sha1_winner == 'this':
1028
content_changed = False
1029
elif other_ie.kind == 'symlink':
1030
def get_target(ie, tree):
1031
if ie.kind != 'symlink':
1033
path = tree.id2path(file_id)
1034
return tree.get_symlink_target(path, file_id)
1035
base_target = get_target(base_ie, self.base_tree)
1036
lca_targets = [get_target(ie, tree) for ie, tree
1037
in zip(lca_entries, self._lca_trees)]
1038
this_target = get_target(this_ie, self.this_tree)
1039
other_target = get_target(other_ie, self.other_tree)
1040
target_winner = self._lca_multi_way(
1041
(base_target, lca_targets),
1042
other_target, this_target)
1043
if (parent_id_winner == 'this' and name_winner == 'this'
1044
and target_winner == 'this'):
1045
# No kind, parent, name, or symlink target change
1048
if target_winner == 'this':
1049
content_changed = False
1050
elif other_ie.kind == 'tree-reference':
1051
# The 'changed' information seems to be handled at a higher
1052
# level. At least, _entries3 returns False for content
1053
# changed, even when at a new revision_id.
1054
content_changed = False
1055
if (parent_id_winner == 'this' and name_winner == 'this'):
1056
# Nothing interesting
1059
raise AssertionError('unhandled kind: %s' % other_ie.kind)
1061
# If we have gotten this far, that means something has changed
1062
result.append((file_id, content_changed,
1063
((base_ie.parent_id, lca_parent_ids),
1064
other_ie.parent_id, this_ie.parent_id),
1065
((base_ie.name, lca_names),
1066
other_ie.name, this_ie.name),
1067
((base_ie.executable, lca_executable),
1068
other_ie.executable, this_ie.executable)
1072
def write_modified(self, results):
1073
modified_hashes = {}
1074
for path in results.modified_paths:
1075
wt_relpath = self.working_tree.relpath(path)
1076
file_id = self.working_tree.path2id(wt_relpath)
1079
hash = self.working_tree.get_file_sha1(wt_relpath, file_id)
1082
modified_hashes[file_id] = hash
1083
self.working_tree.set_merge_modified(modified_hashes)
1086
def parent(entry, file_id):
1087
"""Determine the parent for a file_id (used as a key method)"""
1090
return entry.parent_id
1093
def name(entry, file_id):
1094
"""Determine the name for a file_id (used as a key method)"""
1100
def contents_sha1(tree, file_id):
1101
"""Determine the sha1 of the file contents (used as a key method)."""
1103
path = tree.id2path(file_id)
1104
except errors.NoSuchId:
1106
return tree.get_file_sha1(path, file_id)
1109
def executable(tree, file_id):
1110
"""Determine the executability of a file-id (used as a key method)."""
1112
path = tree.id2path(file_id)
1113
except errors.NoSuchId:
1115
if tree.kind(path, file_id) != "file":
1117
return tree.is_executable(path, file_id)
1120
def kind(tree, path, file_id):
1121
"""Determine the kind of a file-id (used as a key method)."""
1123
path = tree.id2path(file_id)
1124
except errors.NoSuchId:
1126
return tree.kind(path, file_id)
1129
def _three_way(base, other, this):
1131
# if 'base == other', either they all agree, or only 'this' has
1134
elif this not in (base, other):
1135
# 'this' is neither 'base' nor 'other', so both sides changed
1138
# "Ambiguous clean merge" -- both sides have made the same change.
1141
# this == base: only other has changed.
1145
def _lca_multi_way(bases, other, this, allow_overriding_lca=True):
1146
"""Consider LCAs when determining whether a change has occurred.
1148
If LCAS are all identical, this is the same as a _three_way comparison.
1150
:param bases: value in (BASE, [LCAS])
1151
:param other: value in OTHER
1152
:param this: value in THIS
1153
:param allow_overriding_lca: If there is more than one unique lca
1154
value, allow OTHER to override THIS if it has a new value, and
1155
THIS only has an lca value, or vice versa. This is appropriate for
1156
truly scalar values, not as much for non-scalars.
1157
:return: 'this', 'other', or 'conflict' depending on whether an entry
1160
# See doc/developers/lca_tree_merging.txt for details about this
1163
# Either Ambiguously clean, or nothing was actually changed. We
1166
base_val, lca_vals = bases
1167
# Remove 'base_val' from the lca_vals, because it is not interesting
1168
filtered_lca_vals = [lca_val for lca_val in lca_vals
1169
if lca_val != base_val]
1170
if len(filtered_lca_vals) == 0:
1171
return Merge3Merger._three_way(base_val, other, this)
1173
unique_lca_vals = set(filtered_lca_vals)
1174
if len(unique_lca_vals) == 1:
1175
return Merge3Merger._three_way(unique_lca_vals.pop(), other, this)
1177
if allow_overriding_lca:
1178
if other in unique_lca_vals:
1179
if this in unique_lca_vals:
1180
# Each side picked a different lca, conflict
1183
# This has a value which supersedes both lca values, and
1184
# other only has an lca value
1186
elif this in unique_lca_vals:
1187
# OTHER has a value which supersedes both lca values, and this
1188
# only has an lca value
1191
# At this point, the lcas disagree, and the tip disagree
1194
def merge_names(self, file_id):
1195
def get_entry(tree):
1197
return tree.root_inventory[file_id]
1198
except errors.NoSuchId:
1200
this_entry = get_entry(self.this_tree)
1201
other_entry = get_entry(self.other_tree)
1202
base_entry = get_entry(self.base_tree)
1203
entries = (base_entry, other_entry, this_entry)
1206
for entry in entries:
1209
parents.append(None)
1211
names.append(entry.name)
1212
parents.append(entry.parent_id)
1213
return self._merge_names(file_id, parents, names,
1214
resolver=self._three_way)
1216
def _merge_names(self, file_id, parents, names, resolver):
1217
"""Perform a merge on file_id names and parents"""
1218
base_name, other_name, this_name = names
1219
base_parent, other_parent, this_parent = parents
1221
name_winner = resolver(*names)
1223
parent_id_winner = resolver(*parents)
1224
if this_name is None:
1225
if name_winner == "this":
1226
name_winner = "other"
1227
if parent_id_winner == "this":
1228
parent_id_winner = "other"
1229
if name_winner == "this" and parent_id_winner == "this":
1231
if name_winner == 'conflict' or parent_id_winner == 'conflict':
1232
# Creating helpers (.OTHER or .THIS) here cause problems down the
1233
# road if a ContentConflict needs to be created so we should not do
1235
trans_id = self.tt.trans_id_file_id(file_id)
1236
self._raw_conflicts.append(('path conflict', trans_id, file_id,
1237
this_parent, this_name,
1238
other_parent, other_name))
1239
if not self.other_tree.has_id(file_id):
1240
# it doesn't matter whether the result was 'other' or
1241
# 'conflict'-- if it has no file id, we leave it alone.
1243
parent_id = parents[self.winner_idx[parent_id_winner]]
1244
name = names[self.winner_idx[name_winner]]
1245
if parent_id is not None or name is not None:
1246
# if we get here, name_winner and parent_winner are set to safe
1248
if parent_id is None and name is not None:
1249
# if parent_id is None and name is non-None, current file is
1251
if names[self.winner_idx[parent_id_winner]] != '':
1252
raise AssertionError(
1253
'File looks like a root, but named %s' %
1254
names[self.winner_idx[parent_id_winner]])
1255
parent_trans_id = transform.ROOT_PARENT
1257
parent_trans_id = self.tt.trans_id_file_id(parent_id)
1258
self.tt.adjust_path(name, parent_trans_id,
1259
self.tt.trans_id_file_id(file_id))
1261
def _do_merge_contents(self, file_id):
1262
"""Performs a merge on file_id contents."""
1263
def contents_pair(tree):
1265
path = tree.id2path(file_id)
1266
except errors.NoSuchId:
1269
kind = tree.kind(path, file_id)
1270
except errors.NoSuchFile:
1273
contents = tree.get_file_sha1(path, file_id)
1274
elif kind == "symlink":
1275
contents = tree.get_symlink_target(path, file_id)
1278
return kind, contents
1280
# See SPOT run. run, SPOT, run.
1281
# So we're not QUITE repeating ourselves; we do tricky things with
1283
base_pair = contents_pair(self.base_tree)
1284
other_pair = contents_pair(self.other_tree)
1286
this_pair = contents_pair(self.this_tree)
1287
lca_pairs = [contents_pair(tree) for tree in self._lca_trees]
1288
winner = self._lca_multi_way((base_pair, lca_pairs), other_pair,
1289
this_pair, allow_overriding_lca=False)
1291
if base_pair == other_pair:
1294
# We delayed evaluating this_pair as long as we can to avoid
1295
# unnecessary sha1 calculation
1296
this_pair = contents_pair(self.this_tree)
1297
winner = self._three_way(base_pair, other_pair, this_pair)
1298
if winner == 'this':
1299
# No interesting changes introduced by OTHER
1301
# We have a hypothetical conflict, but if we have files, then we
1302
# can try to merge the content
1303
trans_id = self.tt.trans_id_file_id(file_id)
1304
params = MergeFileHookParams(self, file_id, trans_id, this_pair[0],
1305
other_pair[0], winner)
1306
hooks = self.active_hooks
1307
hook_status = 'not_applicable'
1309
hook_status, lines = hook.merge_contents(params)
1310
if hook_status != 'not_applicable':
1311
# Don't try any more hooks, this one applies.
1313
# If the merge ends up replacing the content of the file, we get rid of
1314
# it at the end of this method (this variable is used to track the
1315
# exceptions to this rule).
1318
if hook_status == 'not_applicable':
1319
# No merge hook was able to resolve the situation. Two cases exist:
1320
# a content conflict or a duplicate one.
1322
name = self.tt.final_name(trans_id)
1323
parent_id = self.tt.final_parent(trans_id)
1325
inhibit_content_conflict = False
1326
if params.this_kind is None: # file_id is not in THIS
1327
# Is the name used for a different file_id ?
1328
dupe_path = self.other_tree.id2path(file_id)
1329
this_id = self.this_tree.path2id(dupe_path)
1330
if this_id is not None:
1331
# Two entries for the same path
1333
# versioning the merged file will trigger a duplicate
1335
self.tt.version_file(file_id, trans_id)
1336
transform.create_from_tree(
1337
self.tt, trans_id, self.other_tree,
1338
self.other_tree.id2path(file_id), file_id=file_id,
1339
filter_tree_path=self._get_filter_tree_path(file_id))
1340
inhibit_content_conflict = True
1341
elif params.other_kind is None: # file_id is not in OTHER
1342
# Is the name used for a different file_id ?
1343
dupe_path = self.this_tree.id2path(file_id)
1344
other_id = self.other_tree.path2id(dupe_path)
1345
if other_id is not None:
1346
# Two entries for the same path again, but here, the other
1347
# entry will also be merged. We simply inhibit the
1348
# 'content' conflict creation because we know OTHER will
1349
# create (or has already created depending on ordering) an
1350
# entry at the same path. This will trigger a 'duplicate'
1353
inhibit_content_conflict = True
1354
if not inhibit_content_conflict:
1355
if params.this_kind is not None:
1356
self.tt.unversion_file(trans_id)
1357
# This is a contents conflict, because none of the available
1358
# functions could merge it.
1359
file_group = self._dump_conflicts(name, parent_id, file_id,
1361
self._raw_conflicts.append(('contents conflict', file_group))
1362
elif hook_status == 'success':
1363
self.tt.create_file(lines, trans_id)
1364
elif hook_status == 'conflicted':
1365
# XXX: perhaps the hook should be able to provide
1366
# the BASE/THIS/OTHER files?
1367
self.tt.create_file(lines, trans_id)
1368
self._raw_conflicts.append(('text conflict', trans_id))
1369
name = self.tt.final_name(trans_id)
1370
parent_id = self.tt.final_parent(trans_id)
1371
self._dump_conflicts(name, parent_id, file_id)
1372
elif hook_status == 'delete':
1373
self.tt.unversion_file(trans_id)
1375
elif hook_status == 'done':
1376
# The hook function did whatever it needs to do directly, no
1377
# further action needed here.
1380
raise AssertionError('unknown hook_status: %r' % (hook_status,))
1381
if not self.this_tree.has_id(file_id) and result == "modified":
1382
self.tt.version_file(file_id, trans_id)
1384
# The merge has been performed and produced a new content, so the
1385
# old contents should not be retained.
1386
self.tt.delete_contents(trans_id)
1389
def _default_other_winner_merge(self, merge_hook_params):
1390
"""Replace this contents with other."""
1391
file_id = merge_hook_params.file_id
1392
trans_id = merge_hook_params.trans_id
1393
if self.other_tree.has_id(file_id):
1394
# OTHER changed the file
1395
transform.create_from_tree(
1396
self.tt, trans_id, self.other_tree,
1397
self.other_tree.id2path(file_id), file_id=file_id,
1398
filter_tree_path=self._get_filter_tree_path(file_id))
1400
elif self.this_tree.has_id(file_id):
1401
# OTHER deleted the file
1402
return 'delete', None
1404
raise AssertionError(
1405
'winner is OTHER, but file_id %r not in THIS or OTHER tree'
1408
def merge_contents(self, merge_hook_params):
1409
"""Fallback merge logic after user installed hooks."""
1410
# This function is used in merge hooks as the fallback instance.
1411
# Perhaps making this function and the functions it calls be a
1412
# a separate class would be better.
1413
if merge_hook_params.winner == 'other':
1414
# OTHER is a straight winner, so replace this contents with other
1415
return self._default_other_winner_merge(merge_hook_params)
1416
elif merge_hook_params.is_file_merge():
1417
# THIS and OTHER are both files, so text merge. Either
1418
# BASE is a file, or both converted to files, so at least we
1419
# have agreement that output should be a file.
1421
self.text_merge(merge_hook_params.file_id,
1422
merge_hook_params.trans_id)
1423
except errors.BinaryFile:
1424
return 'not_applicable', None
1427
return 'not_applicable', None
1429
def get_lines(self, tree, file_id):
1430
"""Return the lines in a file, or an empty list."""
1432
path = tree.id2path(file_id)
1433
except errors.NoSuchId:
1436
return tree.get_file_lines(path, file_id)
1438
def text_merge(self, file_id, trans_id):
1439
"""Perform a three-way text merge on a file_id"""
1440
# it's possible that we got here with base as a different type.
1441
# if so, we just want two-way text conflicts.
1443
base_path = self.base_tree.id2path(file_id)
1444
except errors.NoSuchId:
1447
if self.base_tree.kind(base_path, file_id) == "file":
1448
base_lines = self.get_lines(self.base_tree, file_id)
1451
other_lines = self.get_lines(self.other_tree, file_id)
1452
this_lines = self.get_lines(self.this_tree, file_id)
1453
m3 = merge3.Merge3(base_lines, this_lines, other_lines,
1454
is_cherrypick=self.cherrypick)
1455
start_marker = "!START OF MERGE CONFLICT!" + "I HOPE THIS IS UNIQUE"
1456
if self.show_base is True:
1457
base_marker = '|' * 7
1461
def iter_merge3(retval):
1462
retval["text_conflicts"] = False
1463
for line in m3.merge_lines(name_a = "TREE",
1464
name_b = "MERGE-SOURCE",
1465
name_base = "BASE-REVISION",
1466
start_marker=start_marker,
1467
base_marker=base_marker,
1468
reprocess=self.reprocess):
1469
if line.startswith(start_marker):
1470
retval["text_conflicts"] = True
1471
yield line.replace(start_marker, '<' * 7)
1475
merge3_iterator = iter_merge3(retval)
1476
self.tt.create_file(merge3_iterator, trans_id)
1477
if retval["text_conflicts"] is True:
1478
self._raw_conflicts.append(('text conflict', trans_id))
1479
name = self.tt.final_name(trans_id)
1480
parent_id = self.tt.final_parent(trans_id)
1481
file_group = self._dump_conflicts(name, parent_id, file_id,
1482
this_lines, base_lines,
1484
file_group.append(trans_id)
1487
def _get_filter_tree_path(self, file_id):
1488
if self.this_tree.supports_content_filtering():
1489
# We get the path from the working tree if it exists.
1490
# That fails though when OTHER is adding a file, so
1491
# we fall back to the other tree to find the path if
1492
# it doesn't exist locally.
1494
return self.this_tree.id2path(file_id)
1495
except errors.NoSuchId:
1496
return self.other_tree.id2path(file_id)
1497
# Skip the id2path lookup for older formats
1500
def _dump_conflicts(self, name, parent_id, file_id, this_lines=None,
1501
base_lines=None, other_lines=None, set_version=False,
1503
"""Emit conflict files.
1504
If this_lines, base_lines, or other_lines are omitted, they will be
1505
determined automatically. If set_version is true, the .OTHER, .THIS
1506
or .BASE (in that order) will be created as versioned files.
1508
data = [('OTHER', self.other_tree, other_lines),
1509
('THIS', self.this_tree, this_lines)]
1511
data.append(('BASE', self.base_tree, base_lines))
1513
# We need to use the actual path in the working tree of the file here,
1514
# ignoring the conflict suffixes
1516
if wt.supports_content_filtering():
1518
filter_tree_path = wt.id2path(file_id)
1519
except errors.NoSuchId:
1520
# file has been deleted
1521
filter_tree_path = None
1523
# Skip the id2path lookup for older formats
1524
filter_tree_path = None
1528
for suffix, tree, lines in data:
1530
path = tree.id2path(file_id)
1531
except errors.NoSuchId:
1534
trans_id = self._conflict_file(
1535
name, parent_id, path, tree, file_id, suffix, lines,
1537
file_group.append(trans_id)
1538
if set_version and not versioned:
1539
self.tt.version_file(file_id, trans_id)
1543
def _conflict_file(self, name, parent_id, path, tree, file_id, suffix,
1544
lines=None, filter_tree_path=None):
1545
"""Emit a single conflict file."""
1546
name = name + '.' + suffix
1547
trans_id = self.tt.create_path(name, parent_id)
1548
transform.create_from_tree(
1549
self.tt, trans_id, tree, path,
1550
file_id=file_id, bytes=lines,
1551
filter_tree_path=filter_tree_path)
1554
def merge_executable(self, file_id, file_status):
1555
"""Perform a merge on the execute bit."""
1556
executable = [self.executable(t, file_id) for t in (self.base_tree,
1557
self.other_tree, self.this_tree)]
1558
self._merge_executable(file_id, executable, file_status,
1559
resolver=self._three_way)
1561
def _merge_executable(self, file_id, executable, file_status,
1563
"""Perform a merge on the execute bit."""
1564
base_executable, other_executable, this_executable = executable
1565
if file_status == "deleted":
1567
winner = resolver(*executable)
1568
if winner == "conflict":
1569
# There must be a None in here, if we have a conflict, but we
1570
# need executability since file status was not deleted.
1571
if self.executable(self.other_tree, file_id) is None:
1575
if winner == 'this' and file_status != "modified":
1577
trans_id = self.tt.trans_id_file_id(file_id)
1578
if self.tt.final_kind(trans_id) != "file":
1580
if winner == "this":
1581
executability = this_executable
1583
if self.other_tree.has_id(file_id):
1584
executability = other_executable
1585
elif self.this_tree.has_id(file_id):
1586
executability = this_executable
1587
elif self.base_tree_has_id(file_id):
1588
executability = base_executable
1589
if executability is not None:
1590
trans_id = self.tt.trans_id_file_id(file_id)
1591
self.tt.set_executability(executability, trans_id)
1593
def cook_conflicts(self, fs_conflicts):
1594
"""Convert all conflicts into a form that doesn't depend on trans_id"""
1595
content_conflict_file_ids = set()
1596
cooked_conflicts = transform.cook_conflicts(fs_conflicts, self.tt)
1597
fp = transform.FinalPaths(self.tt)
1598
for conflict in self._raw_conflicts:
1599
conflict_type = conflict[0]
1600
if conflict_type == 'path conflict':
1602
this_parent, this_name,
1603
other_parent, other_name) = conflict[1:]
1604
if this_parent is None or this_name is None:
1605
this_path = '<deleted>'
1607
parent_path = fp.get_path(
1608
self.tt.trans_id_file_id(this_parent))
1609
this_path = osutils.pathjoin(parent_path, this_name)
1610
if other_parent is None or other_name is None:
1611
other_path = '<deleted>'
1613
if other_parent == self.other_tree.get_root_id():
1614
# The tree transform doesn't know about the other root,
1615
# so we special case here to avoid a NoFinalPath
1619
parent_path = fp.get_path(
1620
self.tt.trans_id_file_id(other_parent))
1621
other_path = osutils.pathjoin(parent_path, other_name)
1622
c = _mod_conflicts.Conflict.factory(
1623
'path conflict', path=this_path,
1624
conflict_path=other_path,
1626
elif conflict_type == 'contents conflict':
1627
for trans_id in conflict[1]:
1628
file_id = self.tt.final_file_id(trans_id)
1629
if file_id is not None:
1630
# Ok we found the relevant file-id
1632
path = fp.get_path(trans_id)
1633
for suffix in ('.BASE', '.THIS', '.OTHER'):
1634
if path.endswith(suffix):
1635
# Here is the raw path
1636
path = path[:-len(suffix)]
1638
c = _mod_conflicts.Conflict.factory(conflict_type,
1639
path=path, file_id=file_id)
1640
content_conflict_file_ids.add(file_id)
1641
elif conflict_type == 'text conflict':
1642
trans_id = conflict[1]
1643
path = fp.get_path(trans_id)
1644
file_id = self.tt.final_file_id(trans_id)
1645
c = _mod_conflicts.Conflict.factory(conflict_type,
1646
path=path, file_id=file_id)
1648
raise AssertionError('bad conflict type: %r' % (conflict,))
1649
cooked_conflicts.append(c)
1651
self.cooked_conflicts = []
1652
# We want to get rid of path conflicts when a corresponding contents
1653
# conflict exists. This can occur when one branch deletes a file while
1654
# the other renames *and* modifies it. In this case, the content
1655
# conflict is enough.
1656
for c in cooked_conflicts:
1657
if (c.typestring == 'path conflict'
1658
and c.file_id in content_conflict_file_ids):
1660
self.cooked_conflicts.append(c)
1661
self.cooked_conflicts.sort(key=_mod_conflicts.Conflict.sort_key)
1664
class WeaveMerger(Merge3Merger):
1665
"""Three-way tree merger, text weave merger."""
1666
supports_reprocess = True
1667
supports_show_base = False
1668
supports_reverse_cherrypick = False
1669
history_based = True
1671
def _generate_merge_plan(self, file_id, base):
1672
return self.this_tree.plan_file_merge(file_id, self.other_tree,
1675
def _merged_lines(self, file_id):
1676
"""Generate the merged lines.
1677
There is no distinction between lines that are meant to contain <<<<<<<
1681
base = self.base_tree
1684
plan = self._generate_merge_plan(file_id, base)
1685
if 'merge' in debug.debug_flags:
1687
trans_id = self.tt.trans_id_file_id(file_id)
1688
name = self.tt.final_name(trans_id) + '.plan'
1689
contents = ('%11s|%s' % l for l in plan)
1690
self.tt.new_file(name, self.tt.final_parent(trans_id), contents)
1691
textmerge = versionedfile.PlanWeaveMerge(plan, '<<<<<<< TREE\n',
1692
'>>>>>>> MERGE-SOURCE\n')
1693
lines, conflicts = textmerge.merge_lines(self.reprocess)
1695
base_lines = textmerge.base_from_plan()
1698
return lines, base_lines
1700
def text_merge(self, file_id, trans_id):
1701
"""Perform a (weave) text merge for a given file and file-id.
1702
If conflicts are encountered, .THIS and .OTHER files will be emitted,
1703
and a conflict will be noted.
1705
lines, base_lines = self._merged_lines(file_id)
1707
# Note we're checking whether the OUTPUT is binary in this case,
1708
# because we don't want to get into weave merge guts.
1709
textfile.check_text_lines(lines)
1710
self.tt.create_file(lines, trans_id)
1711
if base_lines is not None:
1713
self._raw_conflicts.append(('text conflict', trans_id))
1714
name = self.tt.final_name(trans_id)
1715
parent_id = self.tt.final_parent(trans_id)
1716
file_group = self._dump_conflicts(name, parent_id, file_id,
1718
base_lines=base_lines)
1719
file_group.append(trans_id)
1722
class LCAMerger(WeaveMerger):
1724
def _generate_merge_plan(self, file_id, base):
1725
return self.this_tree.plan_file_lca_merge(file_id, self.other_tree,
1728
class Diff3Merger(Merge3Merger):
1729
"""Three-way merger using external diff3 for text merging"""
1731
def dump_file(self, temp_dir, name, tree, file_id):
1732
out_path = osutils.pathjoin(temp_dir, name)
1733
out_file = open(out_path, "wb")
1735
in_file = tree.get_file(tree.id2path(file_id), file_id)
1736
for line in in_file:
1737
out_file.write(line)
1742
def text_merge(self, file_id, trans_id):
1743
"""Perform a diff3 merge using a specified file-id and trans-id.
1744
If conflicts are encountered, .BASE, .THIS. and .OTHER conflict files
1745
will be dumped, and a will be conflict noted.
1748
temp_dir = osutils.mkdtemp(prefix="bzr-")
1750
new_file = osutils.pathjoin(temp_dir, "new")
1751
this = self.dump_file(temp_dir, "this", self.this_tree, file_id)
1752
base = self.dump_file(temp_dir, "base", self.base_tree, file_id)
1753
other = self.dump_file(temp_dir, "other", self.other_tree, file_id)
1754
status = breezy.patch.diff3(new_file, this, base, other)
1755
if status not in (0, 1):
1756
raise errors.BzrError("Unhandled diff3 exit code")
1757
f = open(new_file, 'rb')
1759
self.tt.create_file(f, trans_id)
1763
name = self.tt.final_name(trans_id)
1764
parent_id = self.tt.final_parent(trans_id)
1765
self._dump_conflicts(name, parent_id, file_id)
1766
self._raw_conflicts.append(('text conflict', trans_id))
1768
osutils.rmtree(temp_dir)
1771
class PathNotInTree(errors.BzrError):
1773
_fmt = """Merge-into failed because %(tree)s does not contain %(path)s."""
1775
def __init__(self, path, tree):
1776
errors.BzrError.__init__(self, path=path, tree=tree)
1779
class MergeIntoMerger(Merger):
1780
"""Merger that understands other_tree will be merged into a subdir.
1782
This also changes the Merger api so that it uses real Branch, revision_id,
1783
and RevisonTree objects, rather than using revision specs.
1786
def __init__(self, this_tree, other_branch, other_tree, target_subdir,
1787
source_subpath, other_rev_id=None):
1788
"""Create a new MergeIntoMerger object.
1790
source_subpath in other_tree will be effectively copied to
1791
target_subdir in this_tree.
1793
:param this_tree: The tree that we will be merging into.
1794
:param other_branch: The Branch we will be merging from.
1795
:param other_tree: The RevisionTree object we want to merge.
1796
:param target_subdir: The relative path where we want to merge
1797
other_tree into this_tree
1798
:param source_subpath: The relative path specifying the subtree of
1799
other_tree to merge into this_tree.
1801
# It is assumed that we are merging a tree that is not in our current
1802
# ancestry, which means we are using the "EmptyTree" as our basis.
1803
null_ancestor_tree = this_tree.branch.repository.revision_tree(
1804
_mod_revision.NULL_REVISION)
1805
super(MergeIntoMerger, self).__init__(
1806
this_branch=this_tree.branch,
1807
this_tree=this_tree,
1808
other_tree=other_tree,
1809
base_tree=null_ancestor_tree,
1811
self._target_subdir = target_subdir
1812
self._source_subpath = source_subpath
1813
self.other_branch = other_branch
1814
if other_rev_id is None:
1815
other_rev_id = other_tree.get_revision_id()
1816
self.other_rev_id = self.other_basis = other_rev_id
1817
self.base_is_ancestor = True
1818
self.backup_files = True
1819
self.merge_type = Merge3Merger
1820
self.show_base = False
1821
self.reprocess = False
1822
self.interesting_ids = None
1823
self.merge_type = _MergeTypeParameterizer(MergeIntoMergeType,
1824
target_subdir=self._target_subdir,
1825
source_subpath=self._source_subpath)
1826
if self._source_subpath != '':
1827
# If this isn't a partial merge make sure the revisions will be
1829
self._maybe_fetch(self.other_branch, self.this_branch,
1832
def set_pending(self):
1833
if self._source_subpath != '':
1835
Merger.set_pending(self)
1838
class _MergeTypeParameterizer(object):
1839
"""Wrap a merge-type class to provide extra parameters.
1841
This is hack used by MergeIntoMerger to pass some extra parameters to its
1842
merge_type. Merger.do_merge() sets up its own set of parameters to pass to
1843
the 'merge_type' member. It is difficult override do_merge without
1844
re-writing the whole thing, so instead we create a wrapper which will pass
1845
the extra parameters.
1848
def __init__(self, merge_type, **kwargs):
1849
self._extra_kwargs = kwargs
1850
self._merge_type = merge_type
1852
def __call__(self, *args, **kwargs):
1853
kwargs.update(self._extra_kwargs)
1854
return self._merge_type(*args, **kwargs)
1856
def __getattr__(self, name):
1857
return getattr(self._merge_type, name)
1860
class MergeIntoMergeType(Merge3Merger):
1861
"""Merger that incorporates a tree (or part of a tree) into another."""
1863
def __init__(self, *args, **kwargs):
1864
"""Initialize the merger object.
1866
:param args: See Merge3Merger.__init__'s args.
1867
:param kwargs: See Merge3Merger.__init__'s keyword args, except for
1868
source_subpath and target_subdir.
1869
:keyword source_subpath: The relative path specifying the subtree of
1870
other_tree to merge into this_tree.
1871
:keyword target_subdir: The relative path where we want to merge
1872
other_tree into this_tree
1874
# All of the interesting work happens during Merge3Merger.__init__(),
1875
# so we have have to hack in to get our extra parameters set.
1876
self._source_subpath = kwargs.pop('source_subpath')
1877
self._target_subdir = kwargs.pop('target_subdir')
1878
super(MergeIntoMergeType, self).__init__(*args, **kwargs)
1880
def _compute_transform(self):
1881
child_pb = ui.ui_factory.nested_progress_bar()
1883
entries = self._entries_to_incorporate()
1884
entries = list(entries)
1885
for num, (entry, parent_id, path) in enumerate(entries):
1886
child_pb.update(gettext('Preparing file merge'), num, len(entries))
1887
parent_trans_id = self.tt.trans_id_file_id(parent_id)
1888
trans_id = transform.new_by_entry(path, self.tt, entry,
1889
parent_trans_id, self.other_tree)
1892
self._finish_computing_transform()
1894
def _entries_to_incorporate(self):
1895
"""Yields pairs of (inventory_entry, new_parent)."""
1896
other_inv = self.other_tree.root_inventory
1897
subdir_id = other_inv.path2id(self._source_subpath)
1898
if subdir_id is None:
1899
# XXX: The error would be clearer if it gave the URL of the source
1900
# branch, but we don't have a reference to that here.
1901
raise PathNotInTree(self._source_subpath, "Source tree")
1902
subdir = other_inv[subdir_id]
1903
parent_in_target = osutils.dirname(self._target_subdir)
1904
target_id = self.this_tree.path2id(parent_in_target)
1905
if target_id is None:
1906
raise PathNotInTree(self._target_subdir, "Target tree")
1907
name_in_target = osutils.basename(self._target_subdir)
1908
merge_into_root = subdir.copy()
1909
merge_into_root.name = name_in_target
1910
if self.this_tree.has_id(merge_into_root.file_id):
1911
# Give the root a new file-id.
1912
# This can happen fairly easily if the directory we are
1913
# incorporating is the root, and both trees have 'TREE_ROOT' as
1914
# their root_id. Users will expect this to Just Work, so we
1915
# change the file-id here.
1916
# Non-root file-ids could potentially conflict too. That's really
1917
# an edge case, so we don't do anything special for those. We let
1918
# them cause conflicts.
1919
merge_into_root.file_id = generate_ids.gen_file_id(name_in_target)
1920
yield (merge_into_root, target_id, '')
1921
if subdir.kind != 'directory':
1922
# No children, so we are done.
1924
for path, entry in other_inv.iter_entries_by_dir(subdir_id):
1925
parent_id = entry.parent_id
1926
if parent_id == subdir.file_id:
1927
# The root's parent ID has changed, so make sure children of
1928
# the root refer to the new ID.
1929
parent_id = merge_into_root.file_id
1930
yield (entry, parent_id, path)
1933
def merge_inner(this_branch, other_tree, base_tree, ignore_zero=False,
1935
merge_type=Merge3Merger,
1936
interesting_ids=None,
1940
interesting_files=None,
1942
change_reporter=None):
1943
"""Primary interface for merging.
1945
Typical use is probably::
1947
merge_inner(branch, branch.get_revision_tree(other_revision),
1948
branch.get_revision_tree(base_revision))
1950
if this_tree is None:
1951
raise errors.BzrError("breezy.merge.merge_inner requires a this_tree "
1953
merger = Merger(this_branch, other_tree, base_tree, this_tree=this_tree,
1954
change_reporter=change_reporter)
1955
merger.backup_files = backup_files
1956
merger.merge_type = merge_type
1957
merger.interesting_ids = interesting_ids
1958
merger.ignore_zero = ignore_zero
1959
if interesting_files:
1961
raise ValueError('Only supply interesting_ids'
1962
' or interesting_files')
1963
merger.interesting_files = interesting_files
1964
merger.show_base = show_base
1965
merger.reprocess = reprocess
1966
merger.other_rev_id = other_rev_id
1967
merger.other_basis = other_rev_id
1968
get_revision_id = getattr(base_tree, 'get_revision_id', None)
1969
if get_revision_id is None:
1970
get_revision_id = base_tree.last_revision
1971
merger.cache_trees_with_revision_ids([other_tree, base_tree, this_tree])
1972
merger.set_base_revision(get_revision_id(), this_branch)
1973
return merger.do_merge()
1976
merge_type_registry = registry.Registry()
1977
merge_type_registry.register('diff3', Diff3Merger,
1978
"Merge using external diff3.")
1979
merge_type_registry.register('lca', LCAMerger,
1980
"LCA-newness merge.")
1981
merge_type_registry.register('merge3', Merge3Merger,
1982
"Native diff3-style merge.")
1983
merge_type_registry.register('weave', WeaveMerger,
1984
"Weave-based merge.")
1987
def get_merge_type_registry():
1988
"""Merge type registry was previously in breezy.option
1990
This method provides a backwards compatible way to retrieve it.
1992
return merge_type_registry
1995
def _plan_annotate_merge(annotated_a, annotated_b, ancestors_a, ancestors_b):
1996
def status_a(revision, text):
1997
if revision in ancestors_b:
1998
return 'killed-b', text
2000
return 'new-a', text
2002
def status_b(revision, text):
2003
if revision in ancestors_a:
2004
return 'killed-a', text
2006
return 'new-b', text
2008
plain_a = [t for (a, t) in annotated_a]
2009
plain_b = [t for (a, t) in annotated_b]
2010
matcher = patiencediff.PatienceSequenceMatcher(None, plain_a, plain_b)
2011
blocks = matcher.get_matching_blocks()
2014
for ai, bi, l in blocks:
2015
# process all mismatched sections
2016
# (last mismatched section is handled because blocks always
2017
# includes a 0-length last block)
2018
for revision, text in annotated_a[a_cur:ai]:
2019
yield status_a(revision, text)
2020
for revision, text in annotated_b[b_cur:bi]:
2021
yield status_b(revision, text)
2022
# and now the matched section
2025
for text_a in plain_a[ai:a_cur]:
2026
yield "unchanged", text_a
2029
class _PlanMergeBase(object):
2031
def __init__(self, a_rev, b_rev, vf, key_prefix):
2034
:param a_rev: Revision-id of one revision to merge
2035
:param b_rev: Revision-id of the other revision to merge
2036
:param vf: A VersionedFiles containing both revisions
2037
:param key_prefix: A prefix for accessing keys in vf, typically
2043
self._last_lines = None
2044
self._last_lines_revision_id = None
2045
self._cached_matching_blocks = {}
2046
self._key_prefix = key_prefix
2047
self._precache_tip_lines()
2049
def _precache_tip_lines(self):
2050
lines = self.get_lines([self.a_rev, self.b_rev])
2051
self.lines_a = lines[self.a_rev]
2052
self.lines_b = lines[self.b_rev]
2054
def get_lines(self, revisions):
2055
"""Get lines for revisions from the backing VersionedFiles.
2057
:raises RevisionNotPresent: on absent texts.
2059
keys = [(self._key_prefix + (rev,)) for rev in revisions]
2061
for record in self.vf.get_record_stream(keys, 'unordered', True):
2062
if record.storage_kind == 'absent':
2063
raise errors.RevisionNotPresent(record.key, self.vf)
2064
result[record.key[-1]] = osutils.chunks_to_lines(
2065
record.get_bytes_as('chunked'))
2068
def plan_merge(self):
2069
"""Generate a 'plan' for merging the two revisions.
2071
This involves comparing their texts and determining the cause of
2072
differences. If text A has a line and text B does not, then either the
2073
line was added to text A, or it was deleted from B. Once the causes
2074
are combined, they are written out in the format described in
2075
VersionedFile.plan_merge
2077
blocks = self._get_matching_blocks(self.a_rev, self.b_rev)
2078
unique_a, unique_b = self._unique_lines(blocks)
2079
new_a, killed_b = self._determine_status(self.a_rev, unique_a)
2080
new_b, killed_a = self._determine_status(self.b_rev, unique_b)
2081
return self._iter_plan(blocks, new_a, killed_b, new_b, killed_a)
2083
def _iter_plan(self, blocks, new_a, killed_b, new_b, killed_a):
2086
for i, j, n in blocks:
2087
for a_index in range(last_i, i):
2088
if a_index in new_a:
2089
if a_index in killed_b:
2090
yield 'conflicted-a', self.lines_a[a_index]
2092
yield 'new-a', self.lines_a[a_index]
2094
yield 'killed-b', self.lines_a[a_index]
2095
for b_index in range(last_j, j):
2096
if b_index in new_b:
2097
if b_index in killed_a:
2098
yield 'conflicted-b', self.lines_b[b_index]
2100
yield 'new-b', self.lines_b[b_index]
2102
yield 'killed-a', self.lines_b[b_index]
2103
# handle common lines
2104
for a_index in range(i, i+n):
2105
yield 'unchanged', self.lines_a[a_index]
2109
def _get_matching_blocks(self, left_revision, right_revision):
2110
"""Return a description of which sections of two revisions match.
2112
See SequenceMatcher.get_matching_blocks
2114
cached = self._cached_matching_blocks.get((left_revision,
2116
if cached is not None:
2118
if self._last_lines_revision_id == left_revision:
2119
left_lines = self._last_lines
2120
right_lines = self.get_lines([right_revision])[right_revision]
2122
lines = self.get_lines([left_revision, right_revision])
2123
left_lines = lines[left_revision]
2124
right_lines = lines[right_revision]
2125
self._last_lines = right_lines
2126
self._last_lines_revision_id = right_revision
2127
matcher = patiencediff.PatienceSequenceMatcher(None, left_lines,
2129
return matcher.get_matching_blocks()
2131
def _unique_lines(self, matching_blocks):
2132
"""Analyse matching_blocks to determine which lines are unique
2134
:return: a tuple of (unique_left, unique_right), where the values are
2135
sets of line numbers of unique lines.
2141
for i, j, n in matching_blocks:
2142
unique_left.extend(range(last_i, i))
2143
unique_right.extend(range(last_j, j))
2146
return unique_left, unique_right
2149
def _subtract_plans(old_plan, new_plan):
2150
"""Remove changes from new_plan that came from old_plan.
2152
It is assumed that the difference between the old_plan and new_plan
2153
is their choice of 'b' text.
2155
All lines from new_plan that differ from old_plan are emitted
2156
verbatim. All lines from new_plan that match old_plan but are
2157
not about the 'b' revision are emitted verbatim.
2159
Lines that match and are about the 'b' revision are the lines we
2160
don't want, so we convert 'killed-b' -> 'unchanged', and 'new-b'
2161
is skipped entirely.
2163
matcher = patiencediff.PatienceSequenceMatcher(None, old_plan,
2166
for i, j, n in matcher.get_matching_blocks():
2167
for jj in range(last_j, j):
2169
for jj in range(j, j+n):
2170
plan_line = new_plan[jj]
2171
if plan_line[0] == 'new-b':
2173
elif plan_line[0] == 'killed-b':
2174
yield 'unchanged', plan_line[1]
2180
class _PlanMerge(_PlanMergeBase):
2181
"""Plan an annotate merge using on-the-fly annotation"""
2183
def __init__(self, a_rev, b_rev, vf, key_prefix):
2184
super(_PlanMerge, self).__init__(a_rev, b_rev, vf, key_prefix)
2185
self.a_key = self._key_prefix + (self.a_rev,)
2186
self.b_key = self._key_prefix + (self.b_rev,)
2187
self.graph = _mod_graph.Graph(self.vf)
2188
heads = self.graph.heads((self.a_key, self.b_key))
2190
# one side dominates, so we can just return its values, yay for
2192
# Ideally we would know that before we get this far
2193
self._head_key = heads.pop()
2194
if self._head_key == self.a_key:
2198
trace.mutter('found dominating revision for %s\n%s > %s', self.vf,
2199
self._head_key[-1], other)
2202
self._head_key = None
2205
def _precache_tip_lines(self):
2206
# Turn this into a no-op, because we will do this later
2209
def _find_recursive_lcas(self):
2210
"""Find all the ancestors back to a unique lca"""
2211
cur_ancestors = (self.a_key, self.b_key)
2212
# graph.find_lca(uncommon, keys) now returns plain NULL_REVISION,
2213
# rather than a key tuple. We will just map that directly to no common
2217
next_lcas = self.graph.find_lca(*cur_ancestors)
2218
# Map a plain NULL_REVISION to a simple no-ancestors
2219
if next_lcas == {_mod_revision.NULL_REVISION}:
2221
# Order the lca's based on when they were merged into the tip
2222
# While the actual merge portion of weave merge uses a set() of
2223
# active revisions, the order of insertion *does* effect the
2224
# implicit ordering of the texts.
2225
for rev_key in cur_ancestors:
2226
ordered_parents = tuple(self.graph.find_merge_order(rev_key,
2228
parent_map[rev_key] = ordered_parents
2229
if len(next_lcas) == 0:
2231
elif len(next_lcas) == 1:
2232
parent_map[list(next_lcas)[0]] = ()
2234
elif len(next_lcas) > 2:
2235
# More than 2 lca's, fall back to grabbing all nodes between
2236
# this and the unique lca.
2237
trace.mutter('More than 2 LCAs, falling back to all nodes for:'
2239
self.a_key, self.b_key, cur_ancestors)
2240
cur_lcas = next_lcas
2241
while len(cur_lcas) > 1:
2242
cur_lcas = self.graph.find_lca(*cur_lcas)
2243
if len(cur_lcas) == 0:
2244
# No common base to find, use the full ancestry
2247
unique_lca = list(cur_lcas)[0]
2248
if unique_lca == _mod_revision.NULL_REVISION:
2249
# find_lca will return a plain 'NULL_REVISION' rather
2250
# than a key tuple when there is no common ancestor, we
2251
# prefer to just use None, because it doesn't confuse
2252
# _get_interesting_texts()
2254
parent_map.update(self._find_unique_parents(next_lcas,
2257
cur_ancestors = next_lcas
2260
def _find_unique_parents(self, tip_keys, base_key):
2261
"""Find ancestors of tip that aren't ancestors of base.
2263
:param tip_keys: Nodes that are interesting
2264
:param base_key: Cull all ancestors of this node
2265
:return: The parent map for all revisions between tip_keys and
2266
base_key. base_key will be included. References to nodes outside of
2267
the ancestor set will also be removed.
2269
# TODO: this would be simpler if find_unique_ancestors took a list
2270
# instead of a single tip, internally it supports it, but it
2271
# isn't a "backwards compatible" api change.
2272
if base_key is None:
2273
parent_map = dict(self.graph.iter_ancestry(tip_keys))
2274
# We remove NULL_REVISION because it isn't a proper tuple key, and
2275
# thus confuses things like _get_interesting_texts, and our logic
2276
# to add the texts into the memory weave.
2277
if _mod_revision.NULL_REVISION in parent_map:
2278
parent_map.pop(_mod_revision.NULL_REVISION)
2281
for tip in tip_keys:
2283
self.graph.find_unique_ancestors(tip, [base_key]))
2284
parent_map = self.graph.get_parent_map(interesting)
2285
parent_map[base_key] = ()
2286
culled_parent_map, child_map, tails = self._remove_external_references(
2288
# Remove all the tails but base_key
2289
if base_key is not None:
2290
tails.remove(base_key)
2291
self._prune_tails(culled_parent_map, child_map, tails)
2292
# Now remove all the uninteresting 'linear' regions
2293
simple_map = _mod_graph.collapse_linear_regions(culled_parent_map)
2297
def _remove_external_references(parent_map):
2298
"""Remove references that go outside of the parent map.
2300
:param parent_map: Something returned from Graph.get_parent_map(keys)
2301
:return: (filtered_parent_map, child_map, tails)
2302
filtered_parent_map is parent_map without external references
2303
child_map is the {parent_key: [child_keys]} mapping
2304
tails is a list of nodes that do not have any parents in the map
2306
# TODO: The basic effect of this function seems more generic than
2307
# _PlanMerge. But the specific details of building a child_map,
2308
# and computing tails seems very specific to _PlanMerge.
2309
# Still, should this be in Graph land?
2310
filtered_parent_map = {}
2313
for key, parent_keys in viewitems(parent_map):
2314
culled_parent_keys = [p for p in parent_keys if p in parent_map]
2315
if not culled_parent_keys:
2317
for parent_key in culled_parent_keys:
2318
child_map.setdefault(parent_key, []).append(key)
2319
# TODO: Do we want to do this, it adds overhead for every node,
2320
# just to say that the node has no children
2321
child_map.setdefault(key, [])
2322
filtered_parent_map[key] = culled_parent_keys
2323
return filtered_parent_map, child_map, tails
2326
def _prune_tails(parent_map, child_map, tails_to_remove):
2327
"""Remove tails from the parent map.
2329
This will remove the supplied revisions until no more children have 0
2332
:param parent_map: A dict of {child: [parents]}, this dictionary will
2333
be modified in place.
2334
:param tails_to_remove: A list of tips that should be removed,
2335
this list will be consumed
2336
:param child_map: The reverse dict of parent_map ({parent: [children]})
2337
this dict will be modified
2338
:return: None, parent_map will be modified in place.
2340
while tails_to_remove:
2341
next = tails_to_remove.pop()
2342
parent_map.pop(next)
2343
children = child_map.pop(next)
2344
for child in children:
2345
child_parents = parent_map[child]
2346
child_parents.remove(next)
2347
if len(child_parents) == 0:
2348
tails_to_remove.append(child)
2350
def _get_interesting_texts(self, parent_map):
2351
"""Return a dict of texts we are interested in.
2353
Note that the input is in key tuples, but the output is in plain
2356
:param parent_map: The output from _find_recursive_lcas
2357
:return: A dict of {'revision_id':lines} as returned by
2358
_PlanMergeBase.get_lines()
2360
all_revision_keys = set(parent_map)
2361
all_revision_keys.add(self.a_key)
2362
all_revision_keys.add(self.b_key)
2364
# Everything else is in 'keys' but get_lines is in 'revision_ids'
2365
all_texts = self.get_lines([k[-1] for k in all_revision_keys])
2368
def _build_weave(self):
2369
from .bzr import weave
2370
self._weave = weave.Weave(weave_name='in_memory_weave',
2371
allow_reserved=True)
2372
parent_map = self._find_recursive_lcas()
2374
all_texts = self._get_interesting_texts(parent_map)
2376
# Note: Unfortunately, the order given by topo_sort will effect the
2377
# ordering resolution in the output. Specifically, if you add A then B,
2378
# then in the output text A lines will show up before B lines. And, of
2379
# course, topo_sort doesn't guarantee any real ordering.
2380
# So we use merge_sort, and add a fake node on the tip.
2381
# This ensures that left-hand parents will always be inserted into the
2382
# weave before right-hand parents.
2383
tip_key = self._key_prefix + (_mod_revision.CURRENT_REVISION,)
2384
parent_map[tip_key] = (self.a_key, self.b_key)
2386
for seq_num, key, depth, eom in reversed(tsort.merge_sort(parent_map,
2390
# for key in tsort.topo_sort(parent_map):
2391
parent_keys = parent_map[key]
2392
revision_id = key[-1]
2393
parent_ids = [k[-1] for k in parent_keys]
2394
self._weave.add_lines(revision_id, parent_ids,
2395
all_texts[revision_id])
2397
def plan_merge(self):
2398
"""Generate a 'plan' for merging the two revisions.
2400
This involves comparing their texts and determining the cause of
2401
differences. If text A has a line and text B does not, then either the
2402
line was added to text A, or it was deleted from B. Once the causes
2403
are combined, they are written out in the format described in
2404
VersionedFile.plan_merge
2406
if self._head_key is not None: # There was a single head
2407
if self._head_key == self.a_key:
2410
if self._head_key != self.b_key:
2411
raise AssertionError('There was an invalid head: %s != %s'
2412
% (self.b_key, self._head_key))
2414
head_rev = self._head_key[-1]
2415
lines = self.get_lines([head_rev])[head_rev]
2416
return ((plan, line) for line in lines)
2417
return self._weave.plan_merge(self.a_rev, self.b_rev)
2420
class _PlanLCAMerge(_PlanMergeBase):
2422
This merge algorithm differs from _PlanMerge in that:
2424
1. comparisons are done against LCAs only
2425
2. cases where a contested line is new versus one LCA but old versus
2426
another are marked as conflicts, by emitting the line as conflicted-a
2429
This is faster, and hopefully produces more useful output.
2432
def __init__(self, a_rev, b_rev, vf, key_prefix, graph):
2433
_PlanMergeBase.__init__(self, a_rev, b_rev, vf, key_prefix)
2434
lcas = graph.find_lca(key_prefix + (a_rev,), key_prefix + (b_rev,))
2437
if lca == _mod_revision.NULL_REVISION:
2440
self.lcas.add(lca[-1])
2441
for lca in self.lcas:
2442
if _mod_revision.is_null(lca):
2445
lca_lines = self.get_lines([lca])[lca]
2446
matcher = patiencediff.PatienceSequenceMatcher(None, self.lines_a,
2448
blocks = list(matcher.get_matching_blocks())
2449
self._cached_matching_blocks[(a_rev, lca)] = blocks
2450
matcher = patiencediff.PatienceSequenceMatcher(None, self.lines_b,
2452
blocks = list(matcher.get_matching_blocks())
2453
self._cached_matching_blocks[(b_rev, lca)] = blocks
2455
def _determine_status(self, revision_id, unique_line_numbers):
2456
"""Determines the status unique lines versus all lcas.
2458
Basically, determines why the line is unique to this revision.
2460
A line may be determined new, killed, or both.
2462
If a line is determined new, that means it was not present in at least
2463
one LCA, and is not present in the other merge revision.
2465
If a line is determined killed, that means the line was present in
2468
If a line is killed and new, this indicates that the two merge
2469
revisions contain differing conflict resolutions.
2471
:param revision_id: The id of the revision in which the lines are
2473
:param unique_line_numbers: The line numbers of unique lines.
2474
:return: a tuple of (new_this, killed_other)
2478
unique_line_numbers = set(unique_line_numbers)
2479
for lca in self.lcas:
2480
blocks = self._get_matching_blocks(revision_id, lca)
2481
unique_vs_lca, _ignored = self._unique_lines(blocks)
2482
new.update(unique_line_numbers.intersection(unique_vs_lca))
2483
killed.update(unique_line_numbers.difference(unique_vs_lca))