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 (bazaar.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(file_id, relpath)
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_revision = self.base_tree.get_reference_revision(file_id)
665
sub_merge.base_tree = \
666
sub_tree.branch.repository.revision_tree(base_revision)
667
sub_merge.base_rev_id = base_revision
672
operation = cleanup.OperationWithCleanups(self._do_merge_to)
673
self.this_tree.lock_tree_write()
674
operation.add_cleanup(self.this_tree.unlock)
675
if self.base_tree is not None:
676
self.base_tree.lock_read()
677
operation.add_cleanup(self.base_tree.unlock)
678
if self.other_tree is not None:
679
self.other_tree.lock_read()
680
operation.add_cleanup(self.other_tree.unlock)
681
merge = operation.run_simple()
682
if len(merge.cooked_conflicts) == 0:
683
if not self.ignore_zero and not trace.is_quiet():
684
trace.note(gettext("All changes applied successfully."))
686
trace.note(gettext("%d conflicts encountered.")
687
% len(merge.cooked_conflicts))
689
return len(merge.cooked_conflicts)
692
class _InventoryNoneEntry(object):
693
"""This represents an inventory entry which *isn't there*.
695
It simplifies the merging logic if we always have an InventoryEntry, even
696
if it isn't actually present
703
symlink_target = None
706
_none_entry = _InventoryNoneEntry()
709
class Merge3Merger(object):
710
"""Three-way merger that uses the merge3 text merger"""
712
supports_reprocess = True
713
supports_show_base = True
714
history_based = False
715
supports_cherrypick = True
716
supports_reverse_cherrypick = True
717
winner_idx = {"this": 2, "other": 1, "conflict": 1}
718
supports_lca_trees = True
720
def __init__(self, working_tree, this_tree, base_tree, other_tree,
721
interesting_ids=None, reprocess=False, show_base=False,
722
change_reporter=None, interesting_files=None, do_merge=True,
723
cherrypick=False, lca_trees=None, this_branch=None,
725
"""Initialize the merger object and perform the merge.
727
:param working_tree: The working tree to apply the merge to
728
:param this_tree: The local tree in the merge operation
729
:param base_tree: The common tree in the merge operation
730
:param other_tree: The other tree to merge changes from
731
:param this_branch: The branch associated with this_tree. Defaults to
732
this_tree.branch if not supplied.
733
:param other_branch: The branch associated with other_tree, if any.
734
:param interesting_ids: The file_ids of files that should be
735
participate in the merge. May not be combined with
737
:param: reprocess If True, perform conflict-reduction processing.
738
:param show_base: If True, show the base revision in text conflicts.
739
(incompatible with reprocess)
740
:param change_reporter: An object that should report changes made
741
:param interesting_files: The tree-relative paths of files that should
742
participate in the merge. If these paths refer to directories,
743
the contents of those directories will also be included. May not
744
be combined with interesting_ids. If neither interesting_files nor
745
interesting_ids is specified, all files may participate in the
747
:param lca_trees: Can be set to a dictionary of {revision_id:rev_tree}
748
if the ancestry was found to include a criss-cross merge.
749
Otherwise should be None.
751
object.__init__(self)
752
if interesting_files is not None and interesting_ids is not None:
754
'specify either interesting_ids or interesting_files')
755
if this_branch is None:
756
this_branch = this_tree.branch
757
self.interesting_ids = interesting_ids
758
self.interesting_files = interesting_files
759
self.working_tree = working_tree
760
self.this_tree = this_tree
761
self.base_tree = base_tree
762
self.other_tree = other_tree
763
self.this_branch = this_branch
764
self.other_branch = other_branch
765
self._raw_conflicts = []
766
self.cooked_conflicts = []
767
self.reprocess = reprocess
768
self.show_base = show_base
769
self._lca_trees = lca_trees
770
# Uncommenting this will change the default algorithm to always use
771
# _entries_lca. This can be useful for running the test suite and
772
# making sure we haven't missed any corner cases.
773
# if lca_trees is None:
774
# self._lca_trees = [self.base_tree]
775
self.change_reporter = change_reporter
776
self.cherrypick = cherrypick
781
operation = cleanup.OperationWithCleanups(self._do_merge)
782
self.working_tree.lock_tree_write()
783
operation.add_cleanup(self.working_tree.unlock)
784
self.this_tree.lock_read()
785
operation.add_cleanup(self.this_tree.unlock)
786
self.base_tree.lock_read()
787
operation.add_cleanup(self.base_tree.unlock)
788
self.other_tree.lock_read()
789
operation.add_cleanup(self.other_tree.unlock)
792
def _do_merge(self, operation):
793
self.tt = transform.TreeTransform(self.working_tree, None)
794
operation.add_cleanup(self.tt.finalize)
795
self._compute_transform()
796
results = self.tt.apply(no_conflicts=True)
797
self.write_modified(results)
799
self.working_tree.add_conflicts(self.cooked_conflicts)
800
except errors.UnsupportedOperation:
803
def make_preview_transform(self):
804
operation = cleanup.OperationWithCleanups(self._make_preview_transform)
805
self.base_tree.lock_read()
806
operation.add_cleanup(self.base_tree.unlock)
807
self.other_tree.lock_read()
808
operation.add_cleanup(self.other_tree.unlock)
809
return operation.run_simple()
811
def _make_preview_transform(self):
812
self.tt = transform.TransformPreview(self.working_tree)
813
self._compute_transform()
816
def _compute_transform(self):
817
if self._lca_trees is None:
818
entries = self._entries3()
819
resolver = self._three_way
821
entries = self._entries_lca()
822
resolver = self._lca_multi_way
823
# Prepare merge hooks
824
factories = Merger.hooks['merge_file_content']
825
# One hook for each registered one plus our default merger
826
hooks = [factory(self) for factory in factories] + [self]
827
self.active_hooks = [hook for hook in hooks if hook is not None]
828
child_pb = ui.ui_factory.nested_progress_bar()
830
for num, (file_id, changed, parents3, names3,
831
executable3) in enumerate(entries):
832
# Try merging each entry
833
child_pb.update(gettext('Preparing file merge'),
835
self._merge_names(file_id, parents3, names3, resolver=resolver)
837
file_status = self._do_merge_contents(file_id)
839
file_status = 'unmodified'
840
self._merge_executable(file_id,
841
executable3, file_status, resolver=resolver)
844
self.tt.fixup_new_roots()
845
self._finish_computing_transform()
847
def _finish_computing_transform(self):
848
"""Finalize the transform and report the changes.
850
This is the second half of _compute_transform.
852
child_pb = ui.ui_factory.nested_progress_bar()
854
fs_conflicts = transform.resolve_conflicts(self.tt, child_pb,
855
lambda t, c: transform.conflict_pass(t, c, self.other_tree))
858
if self.change_reporter is not None:
859
from breezy import delta
860
delta.report_changes(
861
self.tt.iter_changes(), self.change_reporter)
862
self.cook_conflicts(fs_conflicts)
863
for conflict in self.cooked_conflicts:
864
trace.warning(unicode(conflict))
867
"""Gather data about files modified between three trees.
869
Return a list of tuples of file_id, changed, parents3, names3,
870
executable3. changed is a boolean indicating whether the file contents
871
or kind were changed. parents3 is a tuple of parent ids for base,
872
other and this. names3 is a tuple of names for base, other and this.
873
executable3 is a tuple of execute-bit values for base, other and this.
876
iterator = self.other_tree.iter_changes(self.base_tree,
877
specific_files=self.interesting_files,
878
extra_trees=[self.this_tree])
879
this_entries = dict((e.file_id, e) for p, e in
880
self.this_tree.iter_entries_by_dir(
881
self.interesting_ids))
882
for (file_id, paths, changed, versioned, parents, names, kind,
883
executable) in iterator:
884
if (self.interesting_ids is not None and
885
file_id not in self.interesting_ids):
887
entry = this_entries.get(file_id)
888
if entry is not None:
889
this_name = entry.name
890
this_parent = entry.parent_id
891
this_executable = entry.executable
895
this_executable = None
896
parents3 = parents + (this_parent,)
897
names3 = names + (this_name,)
898
executable3 = executable + (this_executable,)
899
result.append((file_id, changed, parents3, names3, executable3))
902
def _entries_lca(self):
903
"""Gather data about files modified between multiple trees.
905
This compares OTHER versus all LCA trees, and for interesting entries,
906
it then compares with THIS and BASE.
908
For the multi-valued entries, the format will be (BASE, [lca1, lca2])
910
:return: [(file_id, changed, parents, names, executable)], where:
912
* file_id: Simple file_id of the entry
913
* changed: Boolean, True if the kind or contents changed else False
914
* parents: ((base, [parent_id, in, lcas]), parent_id_other,
916
* names: ((base, [name, in, lcas]), name_in_other, name_in_this)
917
* executable: ((base, [exec, in, lcas]), exec_in_other,
920
if self.interesting_files is not None:
921
lookup_trees = [self.this_tree, self.base_tree]
922
lookup_trees.extend(self._lca_trees)
923
# I think we should include the lca trees as well
924
interesting_ids = self.other_tree.paths2ids(self.interesting_files,
927
interesting_ids = self.interesting_ids
929
walker = _mod_tree.MultiWalker(self.other_tree, self._lca_trees)
931
base_inventory = self.base_tree.root_inventory
932
this_inventory = self.this_tree.root_inventory
933
for path, file_id, other_ie, lca_values in walker.iter_all():
934
# Is this modified at all from any of the other trees?
936
other_ie = _none_entry
937
if interesting_ids is not None and file_id not in interesting_ids:
940
# If other_revision is found in any of the lcas, that means this
941
# node is uninteresting. This is because when merging, if there are
942
# multiple heads(), we have to create a new node. So if we didn't,
943
# we know that the ancestry is linear, and that OTHER did not
945
# See doc/developers/lca_merge_resolution.txt for details
946
other_revision = other_ie.revision
947
if other_revision is not None:
948
# We can't use this shortcut when other_revision is None,
949
# because it may be None because things are WorkingTrees, and
950
# not because it is *actually* None.
951
is_unmodified = False
952
for lca_path, ie in lca_values:
953
if ie is not None and ie.revision == other_revision:
960
for lca_path, lca_ie in lca_values:
962
lca_entries.append(_none_entry)
964
lca_entries.append(lca_ie)
966
if base_inventory.has_id(file_id):
967
base_ie = base_inventory[file_id]
969
base_ie = _none_entry
971
if this_inventory.has_id(file_id):
972
this_ie = this_inventory[file_id]
974
this_ie = _none_entry
980
for lca_ie in lca_entries:
981
lca_kinds.append(lca_ie.kind)
982
lca_parent_ids.append(lca_ie.parent_id)
983
lca_names.append(lca_ie.name)
984
lca_executable.append(lca_ie.executable)
986
kind_winner = self._lca_multi_way(
987
(base_ie.kind, lca_kinds),
988
other_ie.kind, this_ie.kind)
989
parent_id_winner = self._lca_multi_way(
990
(base_ie.parent_id, lca_parent_ids),
991
other_ie.parent_id, this_ie.parent_id)
992
name_winner = self._lca_multi_way(
993
(base_ie.name, lca_names),
994
other_ie.name, this_ie.name)
996
content_changed = True
997
if kind_winner == 'this':
998
# No kind change in OTHER, see if there are *any* changes
999
if other_ie.kind == 'directory':
1000
if parent_id_winner == 'this' and name_winner == 'this':
1001
# No change for this directory in OTHER, skip
1003
content_changed = False
1004
elif other_ie.kind is None or other_ie.kind == 'file':
1005
def get_sha1(ie, tree):
1006
if ie.kind != 'file':
1008
return tree.get_file_sha1(file_id)
1009
base_sha1 = get_sha1(base_ie, self.base_tree)
1010
lca_sha1s = [get_sha1(ie, tree) for ie, tree
1011
in zip(lca_entries, self._lca_trees)]
1012
this_sha1 = get_sha1(this_ie, self.this_tree)
1013
other_sha1 = get_sha1(other_ie, self.other_tree)
1014
sha1_winner = self._lca_multi_way(
1015
(base_sha1, lca_sha1s), other_sha1, this_sha1,
1016
allow_overriding_lca=False)
1017
exec_winner = self._lca_multi_way(
1018
(base_ie.executable, lca_executable),
1019
other_ie.executable, this_ie.executable)
1020
if (parent_id_winner == 'this' and name_winner == 'this'
1021
and sha1_winner == 'this' and exec_winner == 'this'):
1022
# No kind, parent, name, exec, or content change for
1023
# OTHER, so this node is not considered interesting
1025
if sha1_winner == 'this':
1026
content_changed = False
1027
elif other_ie.kind == 'symlink':
1028
def get_target(ie, tree):
1029
if ie.kind != 'symlink':
1031
return tree.get_symlink_target(file_id)
1032
base_target = get_target(base_ie, self.base_tree)
1033
lca_targets = [get_target(ie, tree) for ie, tree
1034
in zip(lca_entries, self._lca_trees)]
1035
this_target = get_target(this_ie, self.this_tree)
1036
other_target = get_target(other_ie, self.other_tree)
1037
target_winner = self._lca_multi_way(
1038
(base_target, lca_targets),
1039
other_target, this_target)
1040
if (parent_id_winner == 'this' and name_winner == 'this'
1041
and target_winner == 'this'):
1042
# No kind, parent, name, or symlink target change
1045
if target_winner == 'this':
1046
content_changed = False
1047
elif other_ie.kind == 'tree-reference':
1048
# The 'changed' information seems to be handled at a higher
1049
# level. At least, _entries3 returns False for content
1050
# changed, even when at a new revision_id.
1051
content_changed = False
1052
if (parent_id_winner == 'this' and name_winner == 'this'):
1053
# Nothing interesting
1056
raise AssertionError('unhandled kind: %s' % other_ie.kind)
1058
# If we have gotten this far, that means something has changed
1059
result.append((file_id, content_changed,
1060
((base_ie.parent_id, lca_parent_ids),
1061
other_ie.parent_id, this_ie.parent_id),
1062
((base_ie.name, lca_names),
1063
other_ie.name, this_ie.name),
1064
((base_ie.executable, lca_executable),
1065
other_ie.executable, this_ie.executable)
1069
def write_modified(self, results):
1070
modified_hashes = {}
1071
for path in results.modified_paths:
1072
file_id = self.working_tree.path2id(self.working_tree.relpath(path))
1075
hash = self.working_tree.get_file_sha1(file_id)
1078
modified_hashes[file_id] = hash
1079
self.working_tree.set_merge_modified(modified_hashes)
1082
def parent(entry, file_id):
1083
"""Determine the parent for a file_id (used as a key method)"""
1086
return entry.parent_id
1089
def name(entry, file_id):
1090
"""Determine the name for a file_id (used as a key method)"""
1096
def contents_sha1(tree, file_id):
1097
"""Determine the sha1 of the file contents (used as a key method)."""
1098
if not tree.has_id(file_id):
1100
return tree.get_file_sha1(file_id)
1103
def executable(tree, file_id):
1104
"""Determine the executability of a file-id (used as a key method)."""
1105
if not tree.has_id(file_id):
1107
if tree.kind(file_id) != "file":
1109
return tree.is_executable(file_id)
1112
def kind(tree, file_id):
1113
"""Determine the kind of a file-id (used as a key method)."""
1114
if not tree.has_id(file_id):
1116
return tree.kind(file_id)
1119
def _three_way(base, other, this):
1121
# if 'base == other', either they all agree, or only 'this' has
1124
elif this not in (base, other):
1125
# 'this' is neither 'base' nor 'other', so both sides changed
1128
# "Ambiguous clean merge" -- both sides have made the same change.
1131
# this == base: only other has changed.
1135
def _lca_multi_way(bases, other, this, allow_overriding_lca=True):
1136
"""Consider LCAs when determining whether a change has occurred.
1138
If LCAS are all identical, this is the same as a _three_way comparison.
1140
:param bases: value in (BASE, [LCAS])
1141
:param other: value in OTHER
1142
:param this: value in THIS
1143
:param allow_overriding_lca: If there is more than one unique lca
1144
value, allow OTHER to override THIS if it has a new value, and
1145
THIS only has an lca value, or vice versa. This is appropriate for
1146
truly scalar values, not as much for non-scalars.
1147
:return: 'this', 'other', or 'conflict' depending on whether an entry
1150
# See doc/developers/lca_tree_merging.txt for details about this
1153
# Either Ambiguously clean, or nothing was actually changed. We
1156
base_val, lca_vals = bases
1157
# Remove 'base_val' from the lca_vals, because it is not interesting
1158
filtered_lca_vals = [lca_val for lca_val in lca_vals
1159
if lca_val != base_val]
1160
if len(filtered_lca_vals) == 0:
1161
return Merge3Merger._three_way(base_val, other, this)
1163
unique_lca_vals = set(filtered_lca_vals)
1164
if len(unique_lca_vals) == 1:
1165
return Merge3Merger._three_way(unique_lca_vals.pop(), other, this)
1167
if allow_overriding_lca:
1168
if other in unique_lca_vals:
1169
if this in unique_lca_vals:
1170
# Each side picked a different lca, conflict
1173
# This has a value which supersedes both lca values, and
1174
# other only has an lca value
1176
elif this in unique_lca_vals:
1177
# OTHER has a value which supersedes both lca values, and this
1178
# only has an lca value
1181
# At this point, the lcas disagree, and the tip disagree
1184
def merge_names(self, file_id):
1185
def get_entry(tree):
1187
return tree.root_inventory[file_id]
1188
except errors.NoSuchId:
1190
this_entry = get_entry(self.this_tree)
1191
other_entry = get_entry(self.other_tree)
1192
base_entry = get_entry(self.base_tree)
1193
entries = (base_entry, other_entry, this_entry)
1196
for entry in entries:
1199
parents.append(None)
1201
names.append(entry.name)
1202
parents.append(entry.parent_id)
1203
return self._merge_names(file_id, parents, names,
1204
resolver=self._three_way)
1206
def _merge_names(self, file_id, parents, names, resolver):
1207
"""Perform a merge on file_id names and parents"""
1208
base_name, other_name, this_name = names
1209
base_parent, other_parent, this_parent = parents
1211
name_winner = resolver(*names)
1213
parent_id_winner = resolver(*parents)
1214
if this_name is None:
1215
if name_winner == "this":
1216
name_winner = "other"
1217
if parent_id_winner == "this":
1218
parent_id_winner = "other"
1219
if name_winner == "this" and parent_id_winner == "this":
1221
if name_winner == 'conflict' or parent_id_winner == 'conflict':
1222
# Creating helpers (.OTHER or .THIS) here cause problems down the
1223
# road if a ContentConflict needs to be created so we should not do
1225
trans_id = self.tt.trans_id_file_id(file_id)
1226
self._raw_conflicts.append(('path conflict', trans_id, file_id,
1227
this_parent, this_name,
1228
other_parent, other_name))
1229
if not self.other_tree.has_id(file_id):
1230
# it doesn't matter whether the result was 'other' or
1231
# 'conflict'-- if it has no file id, we leave it alone.
1233
parent_id = parents[self.winner_idx[parent_id_winner]]
1234
name = names[self.winner_idx[name_winner]]
1235
if parent_id is not None or name is not None:
1236
# if we get here, name_winner and parent_winner are set to safe
1238
if parent_id is None and name is not None:
1239
# if parent_id is None and name is non-None, current file is
1241
if names[self.winner_idx[parent_id_winner]] != '':
1242
raise AssertionError(
1243
'File looks like a root, but named %s' %
1244
names[self.winner_idx[parent_id_winner]])
1245
parent_trans_id = transform.ROOT_PARENT
1247
parent_trans_id = self.tt.trans_id_file_id(parent_id)
1248
self.tt.adjust_path(name, parent_trans_id,
1249
self.tt.trans_id_file_id(file_id))
1251
def _do_merge_contents(self, file_id):
1252
"""Performs a merge on file_id contents."""
1253
def contents_pair(tree):
1254
if not tree.has_id(file_id):
1256
kind = tree.kind(file_id)
1258
contents = tree.get_file_sha1(file_id)
1259
elif kind == "symlink":
1260
contents = tree.get_symlink_target(file_id)
1263
return kind, contents
1265
# See SPOT run. run, SPOT, run.
1266
# So we're not QUITE repeating ourselves; we do tricky things with
1268
base_pair = contents_pair(self.base_tree)
1269
other_pair = contents_pair(self.other_tree)
1271
this_pair = contents_pair(self.this_tree)
1272
lca_pairs = [contents_pair(tree) for tree in self._lca_trees]
1273
winner = self._lca_multi_way((base_pair, lca_pairs), other_pair,
1274
this_pair, allow_overriding_lca=False)
1276
if base_pair == other_pair:
1279
# We delayed evaluating this_pair as long as we can to avoid
1280
# unnecessary sha1 calculation
1281
this_pair = contents_pair(self.this_tree)
1282
winner = self._three_way(base_pair, other_pair, this_pair)
1283
if winner == 'this':
1284
# No interesting changes introduced by OTHER
1286
# We have a hypothetical conflict, but if we have files, then we
1287
# can try to merge the content
1288
trans_id = self.tt.trans_id_file_id(file_id)
1289
params = MergeFileHookParams(self, file_id, trans_id, this_pair[0],
1290
other_pair[0], winner)
1291
hooks = self.active_hooks
1292
hook_status = 'not_applicable'
1294
hook_status, lines = hook.merge_contents(params)
1295
if hook_status != 'not_applicable':
1296
# Don't try any more hooks, this one applies.
1298
# If the merge ends up replacing the content of the file, we get rid of
1299
# it at the end of this method (this variable is used to track the
1300
# exceptions to this rule).
1303
if hook_status == 'not_applicable':
1304
# No merge hook was able to resolve the situation. Two cases exist:
1305
# a content conflict or a duplicate one.
1307
name = self.tt.final_name(trans_id)
1308
parent_id = self.tt.final_parent(trans_id)
1310
inhibit_content_conflict = False
1311
if params.this_kind is None: # file_id is not in THIS
1312
# Is the name used for a different file_id ?
1313
dupe_path = self.other_tree.id2path(file_id)
1314
this_id = self.this_tree.path2id(dupe_path)
1315
if this_id is not None:
1316
# Two entries for the same path
1318
# versioning the merged file will trigger a duplicate
1320
self.tt.version_file(file_id, trans_id)
1321
transform.create_from_tree(
1322
self.tt, trans_id, self.other_tree, file_id,
1323
filter_tree_path=self._get_filter_tree_path(file_id))
1324
inhibit_content_conflict = True
1325
elif params.other_kind is None: # file_id is not in OTHER
1326
# Is the name used for a different file_id ?
1327
dupe_path = self.this_tree.id2path(file_id)
1328
other_id = self.other_tree.path2id(dupe_path)
1329
if other_id is not None:
1330
# Two entries for the same path again, but here, the other
1331
# entry will also be merged. We simply inhibit the
1332
# 'content' conflict creation because we know OTHER will
1333
# create (or has already created depending on ordering) an
1334
# entry at the same path. This will trigger a 'duplicate'
1337
inhibit_content_conflict = True
1338
if not inhibit_content_conflict:
1339
if params.this_kind is not None:
1340
self.tt.unversion_file(trans_id)
1341
# This is a contents conflict, because none of the available
1342
# functions could merge it.
1343
file_group = self._dump_conflicts(name, parent_id, file_id,
1345
self._raw_conflicts.append(('contents conflict', file_group))
1346
elif hook_status == 'success':
1347
self.tt.create_file(lines, trans_id)
1348
elif hook_status == 'conflicted':
1349
# XXX: perhaps the hook should be able to provide
1350
# the BASE/THIS/OTHER files?
1351
self.tt.create_file(lines, trans_id)
1352
self._raw_conflicts.append(('text conflict', trans_id))
1353
name = self.tt.final_name(trans_id)
1354
parent_id = self.tt.final_parent(trans_id)
1355
self._dump_conflicts(name, parent_id, file_id)
1356
elif hook_status == 'delete':
1357
self.tt.unversion_file(trans_id)
1359
elif hook_status == 'done':
1360
# The hook function did whatever it needs to do directly, no
1361
# further action needed here.
1364
raise AssertionError('unknown hook_status: %r' % (hook_status,))
1365
if not self.this_tree.has_id(file_id) and result == "modified":
1366
self.tt.version_file(file_id, trans_id)
1368
# The merge has been performed and produced a new content, so the
1369
# old contents should not be retained.
1370
self.tt.delete_contents(trans_id)
1373
def _default_other_winner_merge(self, merge_hook_params):
1374
"""Replace this contents with other."""
1375
file_id = merge_hook_params.file_id
1376
trans_id = merge_hook_params.trans_id
1377
if self.other_tree.has_id(file_id):
1378
# OTHER changed the file
1379
transform.create_from_tree(
1380
self.tt, trans_id, self.other_tree, file_id,
1381
filter_tree_path=self._get_filter_tree_path(file_id))
1383
elif self.this_tree.has_id(file_id):
1384
# OTHER deleted the file
1385
return 'delete', None
1387
raise AssertionError(
1388
'winner is OTHER, but file_id %r not in THIS or OTHER tree'
1391
def merge_contents(self, merge_hook_params):
1392
"""Fallback merge logic after user installed hooks."""
1393
# This function is used in merge hooks as the fallback instance.
1394
# Perhaps making this function and the functions it calls be a
1395
# a separate class would be better.
1396
if merge_hook_params.winner == 'other':
1397
# OTHER is a straight winner, so replace this contents with other
1398
return self._default_other_winner_merge(merge_hook_params)
1399
elif merge_hook_params.is_file_merge():
1400
# THIS and OTHER are both files, so text merge. Either
1401
# BASE is a file, or both converted to files, so at least we
1402
# have agreement that output should be a file.
1404
self.text_merge(merge_hook_params.file_id,
1405
merge_hook_params.trans_id)
1406
except errors.BinaryFile:
1407
return 'not_applicable', None
1410
return 'not_applicable', None
1412
def get_lines(self, tree, file_id):
1413
"""Return the lines in a file, or an empty list."""
1414
if tree.has_id(file_id):
1415
return tree.get_file_lines(file_id)
1419
def text_merge(self, file_id, trans_id):
1420
"""Perform a three-way text merge on a file_id"""
1421
# it's possible that we got here with base as a different type.
1422
# if so, we just want two-way text conflicts.
1423
if self.base_tree.has_id(file_id) and \
1424
self.base_tree.kind(file_id) == "file":
1425
base_lines = self.get_lines(self.base_tree, file_id)
1428
other_lines = self.get_lines(self.other_tree, file_id)
1429
this_lines = self.get_lines(self.this_tree, file_id)
1430
m3 = merge3.Merge3(base_lines, this_lines, other_lines,
1431
is_cherrypick=self.cherrypick)
1432
start_marker = "!START OF MERGE CONFLICT!" + "I HOPE THIS IS UNIQUE"
1433
if self.show_base is True:
1434
base_marker = '|' * 7
1438
def iter_merge3(retval):
1439
retval["text_conflicts"] = False
1440
for line in m3.merge_lines(name_a = "TREE",
1441
name_b = "MERGE-SOURCE",
1442
name_base = "BASE-REVISION",
1443
start_marker=start_marker,
1444
base_marker=base_marker,
1445
reprocess=self.reprocess):
1446
if line.startswith(start_marker):
1447
retval["text_conflicts"] = True
1448
yield line.replace(start_marker, '<' * 7)
1452
merge3_iterator = iter_merge3(retval)
1453
self.tt.create_file(merge3_iterator, trans_id)
1454
if retval["text_conflicts"] is True:
1455
self._raw_conflicts.append(('text conflict', trans_id))
1456
name = self.tt.final_name(trans_id)
1457
parent_id = self.tt.final_parent(trans_id)
1458
file_group = self._dump_conflicts(name, parent_id, file_id,
1459
this_lines, base_lines,
1461
file_group.append(trans_id)
1464
def _get_filter_tree_path(self, file_id):
1465
if self.this_tree.supports_content_filtering():
1466
# We get the path from the working tree if it exists.
1467
# That fails though when OTHER is adding a file, so
1468
# we fall back to the other tree to find the path if
1469
# it doesn't exist locally.
1471
return self.this_tree.id2path(file_id)
1472
except errors.NoSuchId:
1473
return self.other_tree.id2path(file_id)
1474
# Skip the id2path lookup for older formats
1477
def _dump_conflicts(self, name, parent_id, file_id, this_lines=None,
1478
base_lines=None, other_lines=None, set_version=False,
1480
"""Emit conflict files.
1481
If this_lines, base_lines, or other_lines are omitted, they will be
1482
determined automatically. If set_version is true, the .OTHER, .THIS
1483
or .BASE (in that order) will be created as versioned files.
1485
data = [('OTHER', self.other_tree, other_lines),
1486
('THIS', self.this_tree, this_lines)]
1488
data.append(('BASE', self.base_tree, base_lines))
1490
# We need to use the actual path in the working tree of the file here,
1491
# ignoring the conflict suffixes
1493
if wt.supports_content_filtering():
1495
filter_tree_path = wt.id2path(file_id)
1496
except errors.NoSuchId:
1497
# file has been deleted
1498
filter_tree_path = None
1500
# Skip the id2path lookup for older formats
1501
filter_tree_path = None
1505
for suffix, tree, lines in data:
1506
if tree.has_id(file_id):
1507
trans_id = self._conflict_file(name, parent_id, tree, file_id,
1508
suffix, lines, filter_tree_path)
1509
file_group.append(trans_id)
1510
if set_version and not versioned:
1511
self.tt.version_file(file_id, trans_id)
1515
def _conflict_file(self, name, parent_id, tree, file_id, suffix,
1516
lines=None, filter_tree_path=None):
1517
"""Emit a single conflict file."""
1518
name = name + '.' + suffix
1519
trans_id = self.tt.create_path(name, parent_id)
1520
transform.create_from_tree(self.tt, trans_id, tree, file_id, lines,
1524
def merge_executable(self, file_id, file_status):
1525
"""Perform a merge on the execute bit."""
1526
executable = [self.executable(t, file_id) for t in (self.base_tree,
1527
self.other_tree, self.this_tree)]
1528
self._merge_executable(file_id, executable, file_status,
1529
resolver=self._three_way)
1531
def _merge_executable(self, file_id, executable, file_status,
1533
"""Perform a merge on the execute bit."""
1534
base_executable, other_executable, this_executable = executable
1535
if file_status == "deleted":
1537
winner = resolver(*executable)
1538
if winner == "conflict":
1539
# There must be a None in here, if we have a conflict, but we
1540
# need executability since file status was not deleted.
1541
if self.executable(self.other_tree, file_id) is None:
1545
if winner == 'this' and file_status != "modified":
1547
trans_id = self.tt.trans_id_file_id(file_id)
1548
if self.tt.final_kind(trans_id) != "file":
1550
if winner == "this":
1551
executability = this_executable
1553
if self.other_tree.has_id(file_id):
1554
executability = other_executable
1555
elif self.this_tree.has_id(file_id):
1556
executability = this_executable
1557
elif self.base_tree_has_id(file_id):
1558
executability = base_executable
1559
if executability is not None:
1560
trans_id = self.tt.trans_id_file_id(file_id)
1561
self.tt.set_executability(executability, trans_id)
1563
def cook_conflicts(self, fs_conflicts):
1564
"""Convert all conflicts into a form that doesn't depend on trans_id"""
1565
content_conflict_file_ids = set()
1566
cooked_conflicts = transform.cook_conflicts(fs_conflicts, self.tt)
1567
fp = transform.FinalPaths(self.tt)
1568
for conflict in self._raw_conflicts:
1569
conflict_type = conflict[0]
1570
if conflict_type == 'path conflict':
1572
this_parent, this_name,
1573
other_parent, other_name) = conflict[1:]
1574
if this_parent is None or this_name is None:
1575
this_path = '<deleted>'
1577
parent_path = fp.get_path(
1578
self.tt.trans_id_file_id(this_parent))
1579
this_path = osutils.pathjoin(parent_path, this_name)
1580
if other_parent is None or other_name is None:
1581
other_path = '<deleted>'
1583
if other_parent == self.other_tree.get_root_id():
1584
# The tree transform doesn't know about the other root,
1585
# so we special case here to avoid a NoFinalPath
1589
parent_path = fp.get_path(
1590
self.tt.trans_id_file_id(other_parent))
1591
other_path = osutils.pathjoin(parent_path, other_name)
1592
c = _mod_conflicts.Conflict.factory(
1593
'path conflict', path=this_path,
1594
conflict_path=other_path,
1596
elif conflict_type == 'contents conflict':
1597
for trans_id in conflict[1]:
1598
file_id = self.tt.final_file_id(trans_id)
1599
if file_id is not None:
1600
# Ok we found the relevant file-id
1602
path = fp.get_path(trans_id)
1603
for suffix in ('.BASE', '.THIS', '.OTHER'):
1604
if path.endswith(suffix):
1605
# Here is the raw path
1606
path = path[:-len(suffix)]
1608
c = _mod_conflicts.Conflict.factory(conflict_type,
1609
path=path, file_id=file_id)
1610
content_conflict_file_ids.add(file_id)
1611
elif conflict_type == 'text conflict':
1612
trans_id = conflict[1]
1613
path = fp.get_path(trans_id)
1614
file_id = self.tt.final_file_id(trans_id)
1615
c = _mod_conflicts.Conflict.factory(conflict_type,
1616
path=path, file_id=file_id)
1618
raise AssertionError('bad conflict type: %r' % (conflict,))
1619
cooked_conflicts.append(c)
1621
self.cooked_conflicts = []
1622
# We want to get rid of path conflicts when a corresponding contents
1623
# conflict exists. This can occur when one branch deletes a file while
1624
# the other renames *and* modifies it. In this case, the content
1625
# conflict is enough.
1626
for c in cooked_conflicts:
1627
if (c.typestring == 'path conflict'
1628
and c.file_id in content_conflict_file_ids):
1630
self.cooked_conflicts.append(c)
1631
self.cooked_conflicts.sort(key=_mod_conflicts.Conflict.sort_key)
1634
class WeaveMerger(Merge3Merger):
1635
"""Three-way tree merger, text weave merger."""
1636
supports_reprocess = True
1637
supports_show_base = False
1638
supports_reverse_cherrypick = False
1639
history_based = True
1641
def _generate_merge_plan(self, file_id, base):
1642
return self.this_tree.plan_file_merge(file_id, self.other_tree,
1645
def _merged_lines(self, file_id):
1646
"""Generate the merged lines.
1647
There is no distinction between lines that are meant to contain <<<<<<<
1651
base = self.base_tree
1654
plan = self._generate_merge_plan(file_id, base)
1655
if 'merge' in debug.debug_flags:
1657
trans_id = self.tt.trans_id_file_id(file_id)
1658
name = self.tt.final_name(trans_id) + '.plan'
1659
contents = ('%11s|%s' % l for l in plan)
1660
self.tt.new_file(name, self.tt.final_parent(trans_id), contents)
1661
textmerge = versionedfile.PlanWeaveMerge(plan, '<<<<<<< TREE\n',
1662
'>>>>>>> MERGE-SOURCE\n')
1663
lines, conflicts = textmerge.merge_lines(self.reprocess)
1665
base_lines = textmerge.base_from_plan()
1668
return lines, base_lines
1670
def text_merge(self, file_id, trans_id):
1671
"""Perform a (weave) text merge for a given file and file-id.
1672
If conflicts are encountered, .THIS and .OTHER files will be emitted,
1673
and a conflict will be noted.
1675
lines, base_lines = self._merged_lines(file_id)
1677
# Note we're checking whether the OUTPUT is binary in this case,
1678
# because we don't want to get into weave merge guts.
1679
textfile.check_text_lines(lines)
1680
self.tt.create_file(lines, trans_id)
1681
if base_lines is not None:
1683
self._raw_conflicts.append(('text conflict', trans_id))
1684
name = self.tt.final_name(trans_id)
1685
parent_id = self.tt.final_parent(trans_id)
1686
file_group = self._dump_conflicts(name, parent_id, file_id,
1688
base_lines=base_lines)
1689
file_group.append(trans_id)
1692
class LCAMerger(WeaveMerger):
1694
def _generate_merge_plan(self, file_id, base):
1695
return self.this_tree.plan_file_lca_merge(file_id, self.other_tree,
1698
class Diff3Merger(Merge3Merger):
1699
"""Three-way merger using external diff3 for text merging"""
1701
def dump_file(self, temp_dir, name, tree, file_id):
1702
out_path = osutils.pathjoin(temp_dir, name)
1703
out_file = open(out_path, "wb")
1705
in_file = tree.get_file(file_id)
1706
for line in in_file:
1707
out_file.write(line)
1712
def text_merge(self, file_id, trans_id):
1713
"""Perform a diff3 merge using a specified file-id and trans-id.
1714
If conflicts are encountered, .BASE, .THIS. and .OTHER conflict files
1715
will be dumped, and a will be conflict noted.
1718
temp_dir = osutils.mkdtemp(prefix="bzr-")
1720
new_file = osutils.pathjoin(temp_dir, "new")
1721
this = self.dump_file(temp_dir, "this", self.this_tree, file_id)
1722
base = self.dump_file(temp_dir, "base", self.base_tree, file_id)
1723
other = self.dump_file(temp_dir, "other", self.other_tree, file_id)
1724
status = breezy.patch.diff3(new_file, this, base, other)
1725
if status not in (0, 1):
1726
raise errors.BzrError("Unhandled diff3 exit code")
1727
f = open(new_file, 'rb')
1729
self.tt.create_file(f, trans_id)
1733
name = self.tt.final_name(trans_id)
1734
parent_id = self.tt.final_parent(trans_id)
1735
self._dump_conflicts(name, parent_id, file_id)
1736
self._raw_conflicts.append(('text conflict', trans_id))
1738
osutils.rmtree(temp_dir)
1741
class PathNotInTree(errors.BzrError):
1743
_fmt = """Merge-into failed because %(tree)s does not contain %(path)s."""
1745
def __init__(self, path, tree):
1746
errors.BzrError.__init__(self, path=path, tree=tree)
1749
class MergeIntoMerger(Merger):
1750
"""Merger that understands other_tree will be merged into a subdir.
1752
This also changes the Merger api so that it uses real Branch, revision_id,
1753
and RevisonTree objects, rather than using revision specs.
1756
def __init__(self, this_tree, other_branch, other_tree, target_subdir,
1757
source_subpath, other_rev_id=None):
1758
"""Create a new MergeIntoMerger object.
1760
source_subpath in other_tree will be effectively copied to
1761
target_subdir in this_tree.
1763
:param this_tree: The tree that we will be merging into.
1764
:param other_branch: The Branch we will be merging from.
1765
:param other_tree: The RevisionTree object we want to merge.
1766
:param target_subdir: The relative path where we want to merge
1767
other_tree into this_tree
1768
:param source_subpath: The relative path specifying the subtree of
1769
other_tree to merge into this_tree.
1771
# It is assumed that we are merging a tree that is not in our current
1772
# ancestry, which means we are using the "EmptyTree" as our basis.
1773
null_ancestor_tree = this_tree.branch.repository.revision_tree(
1774
_mod_revision.NULL_REVISION)
1775
super(MergeIntoMerger, self).__init__(
1776
this_branch=this_tree.branch,
1777
this_tree=this_tree,
1778
other_tree=other_tree,
1779
base_tree=null_ancestor_tree,
1781
self._target_subdir = target_subdir
1782
self._source_subpath = source_subpath
1783
self.other_branch = other_branch
1784
if other_rev_id is None:
1785
other_rev_id = other_tree.get_revision_id()
1786
self.other_rev_id = self.other_basis = other_rev_id
1787
self.base_is_ancestor = True
1788
self.backup_files = True
1789
self.merge_type = Merge3Merger
1790
self.show_base = False
1791
self.reprocess = False
1792
self.interesting_ids = None
1793
self.merge_type = _MergeTypeParameterizer(MergeIntoMergeType,
1794
target_subdir=self._target_subdir,
1795
source_subpath=self._source_subpath)
1796
if self._source_subpath != '':
1797
# If this isn't a partial merge make sure the revisions will be
1799
self._maybe_fetch(self.other_branch, self.this_branch,
1802
def set_pending(self):
1803
if self._source_subpath != '':
1805
Merger.set_pending(self)
1808
class _MergeTypeParameterizer(object):
1809
"""Wrap a merge-type class to provide extra parameters.
1811
This is hack used by MergeIntoMerger to pass some extra parameters to its
1812
merge_type. Merger.do_merge() sets up its own set of parameters to pass to
1813
the 'merge_type' member. It is difficult override do_merge without
1814
re-writing the whole thing, so instead we create a wrapper which will pass
1815
the extra parameters.
1818
def __init__(self, merge_type, **kwargs):
1819
self._extra_kwargs = kwargs
1820
self._merge_type = merge_type
1822
def __call__(self, *args, **kwargs):
1823
kwargs.update(self._extra_kwargs)
1824
return self._merge_type(*args, **kwargs)
1826
def __getattr__(self, name):
1827
return getattr(self._merge_type, name)
1830
class MergeIntoMergeType(Merge3Merger):
1831
"""Merger that incorporates a tree (or part of a tree) into another."""
1833
def __init__(self, *args, **kwargs):
1834
"""Initialize the merger object.
1836
:param args: See Merge3Merger.__init__'s args.
1837
:param kwargs: See Merge3Merger.__init__'s keyword args, except for
1838
source_subpath and target_subdir.
1839
:keyword source_subpath: The relative path specifying the subtree of
1840
other_tree to merge into this_tree.
1841
:keyword target_subdir: The relative path where we want to merge
1842
other_tree into this_tree
1844
# All of the interesting work happens during Merge3Merger.__init__(),
1845
# so we have have to hack in to get our extra parameters set.
1846
self._source_subpath = kwargs.pop('source_subpath')
1847
self._target_subdir = kwargs.pop('target_subdir')
1848
super(MergeIntoMergeType, self).__init__(*args, **kwargs)
1850
def _compute_transform(self):
1851
child_pb = ui.ui_factory.nested_progress_bar()
1853
entries = self._entries_to_incorporate()
1854
entries = list(entries)
1855
for num, (entry, parent_id) in enumerate(entries):
1856
child_pb.update(gettext('Preparing file merge'), num, len(entries))
1857
parent_trans_id = self.tt.trans_id_file_id(parent_id)
1858
trans_id = transform.new_by_entry(self.tt, entry,
1859
parent_trans_id, self.other_tree)
1862
self._finish_computing_transform()
1864
def _entries_to_incorporate(self):
1865
"""Yields pairs of (inventory_entry, new_parent)."""
1866
other_inv = self.other_tree.root_inventory
1867
subdir_id = other_inv.path2id(self._source_subpath)
1868
if subdir_id is None:
1869
# XXX: The error would be clearer if it gave the URL of the source
1870
# branch, but we don't have a reference to that here.
1871
raise PathNotInTree(self._source_subpath, "Source tree")
1872
subdir = other_inv[subdir_id]
1873
parent_in_target = osutils.dirname(self._target_subdir)
1874
target_id = self.this_tree.path2id(parent_in_target)
1875
if target_id is None:
1876
raise PathNotInTree(self._target_subdir, "Target tree")
1877
name_in_target = osutils.basename(self._target_subdir)
1878
merge_into_root = subdir.copy()
1879
merge_into_root.name = name_in_target
1880
if self.this_tree.has_id(merge_into_root.file_id):
1881
# Give the root a new file-id.
1882
# This can happen fairly easily if the directory we are
1883
# incorporating is the root, and both trees have 'TREE_ROOT' as
1884
# their root_id. Users will expect this to Just Work, so we
1885
# change the file-id here.
1886
# Non-root file-ids could potentially conflict too. That's really
1887
# an edge case, so we don't do anything special for those. We let
1888
# them cause conflicts.
1889
merge_into_root.file_id = generate_ids.gen_file_id(name_in_target)
1890
yield (merge_into_root, target_id)
1891
if subdir.kind != 'directory':
1892
# No children, so we are done.
1894
for ignored_path, entry in other_inv.iter_entries_by_dir(subdir_id):
1895
parent_id = entry.parent_id
1896
if parent_id == subdir.file_id:
1897
# The root's parent ID has changed, so make sure children of
1898
# the root refer to the new ID.
1899
parent_id = merge_into_root.file_id
1900
yield (entry, parent_id)
1903
def merge_inner(this_branch, other_tree, base_tree, ignore_zero=False,
1905
merge_type=Merge3Merger,
1906
interesting_ids=None,
1910
interesting_files=None,
1912
change_reporter=None):
1913
"""Primary interface for merging.
1915
Typical use is probably::
1917
merge_inner(branch, branch.get_revision_tree(other_revision),
1918
branch.get_revision_tree(base_revision))
1920
if this_tree is None:
1921
raise errors.BzrError("breezy.merge.merge_inner requires a this_tree "
1923
merger = Merger(this_branch, other_tree, base_tree, this_tree=this_tree,
1924
change_reporter=change_reporter)
1925
merger.backup_files = backup_files
1926
merger.merge_type = merge_type
1927
merger.interesting_ids = interesting_ids
1928
merger.ignore_zero = ignore_zero
1929
if interesting_files:
1931
raise ValueError('Only supply interesting_ids'
1932
' or interesting_files')
1933
merger.interesting_files = interesting_files
1934
merger.show_base = show_base
1935
merger.reprocess = reprocess
1936
merger.other_rev_id = other_rev_id
1937
merger.other_basis = other_rev_id
1938
get_revision_id = getattr(base_tree, 'get_revision_id', None)
1939
if get_revision_id is None:
1940
get_revision_id = base_tree.last_revision
1941
merger.cache_trees_with_revision_ids([other_tree, base_tree, this_tree])
1942
merger.set_base_revision(get_revision_id(), this_branch)
1943
return merger.do_merge()
1946
merge_type_registry = registry.Registry()
1947
merge_type_registry.register('diff3', Diff3Merger,
1948
"Merge using external diff3.")
1949
merge_type_registry.register('lca', LCAMerger,
1950
"LCA-newness merge.")
1951
merge_type_registry.register('merge3', Merge3Merger,
1952
"Native diff3-style merge.")
1953
merge_type_registry.register('weave', WeaveMerger,
1954
"Weave-based merge.")
1957
def get_merge_type_registry():
1958
"""Merge type registry was previously in breezy.option
1960
This method provides a backwards compatible way to retrieve it.
1962
return merge_type_registry
1965
def _plan_annotate_merge(annotated_a, annotated_b, ancestors_a, ancestors_b):
1966
def status_a(revision, text):
1967
if revision in ancestors_b:
1968
return 'killed-b', text
1970
return 'new-a', text
1972
def status_b(revision, text):
1973
if revision in ancestors_a:
1974
return 'killed-a', text
1976
return 'new-b', text
1978
plain_a = [t for (a, t) in annotated_a]
1979
plain_b = [t for (a, t) in annotated_b]
1980
matcher = patiencediff.PatienceSequenceMatcher(None, plain_a, plain_b)
1981
blocks = matcher.get_matching_blocks()
1984
for ai, bi, l in blocks:
1985
# process all mismatched sections
1986
# (last mismatched section is handled because blocks always
1987
# includes a 0-length last block)
1988
for revision, text in annotated_a[a_cur:ai]:
1989
yield status_a(revision, text)
1990
for revision, text in annotated_b[b_cur:bi]:
1991
yield status_b(revision, text)
1992
# and now the matched section
1995
for text_a in plain_a[ai:a_cur]:
1996
yield "unchanged", text_a
1999
class _PlanMergeBase(object):
2001
def __init__(self, a_rev, b_rev, vf, key_prefix):
2004
:param a_rev: Revision-id of one revision to merge
2005
:param b_rev: Revision-id of the other revision to merge
2006
:param vf: A VersionedFiles containing both revisions
2007
:param key_prefix: A prefix for accessing keys in vf, typically
2013
self._last_lines = None
2014
self._last_lines_revision_id = None
2015
self._cached_matching_blocks = {}
2016
self._key_prefix = key_prefix
2017
self._precache_tip_lines()
2019
def _precache_tip_lines(self):
2020
lines = self.get_lines([self.a_rev, self.b_rev])
2021
self.lines_a = lines[self.a_rev]
2022
self.lines_b = lines[self.b_rev]
2024
def get_lines(self, revisions):
2025
"""Get lines for revisions from the backing VersionedFiles.
2027
:raises RevisionNotPresent: on absent texts.
2029
keys = [(self._key_prefix + (rev,)) for rev in revisions]
2031
for record in self.vf.get_record_stream(keys, 'unordered', True):
2032
if record.storage_kind == 'absent':
2033
raise errors.RevisionNotPresent(record.key, self.vf)
2034
result[record.key[-1]] = osutils.chunks_to_lines(
2035
record.get_bytes_as('chunked'))
2038
def plan_merge(self):
2039
"""Generate a 'plan' for merging the two revisions.
2041
This involves comparing their texts and determining the cause of
2042
differences. If text A has a line and text B does not, then either the
2043
line was added to text A, or it was deleted from B. Once the causes
2044
are combined, they are written out in the format described in
2045
VersionedFile.plan_merge
2047
blocks = self._get_matching_blocks(self.a_rev, self.b_rev)
2048
unique_a, unique_b = self._unique_lines(blocks)
2049
new_a, killed_b = self._determine_status(self.a_rev, unique_a)
2050
new_b, killed_a = self._determine_status(self.b_rev, unique_b)
2051
return self._iter_plan(blocks, new_a, killed_b, new_b, killed_a)
2053
def _iter_plan(self, blocks, new_a, killed_b, new_b, killed_a):
2056
for i, j, n in blocks:
2057
for a_index in range(last_i, i):
2058
if a_index in new_a:
2059
if a_index in killed_b:
2060
yield 'conflicted-a', self.lines_a[a_index]
2062
yield 'new-a', self.lines_a[a_index]
2064
yield 'killed-b', self.lines_a[a_index]
2065
for b_index in range(last_j, j):
2066
if b_index in new_b:
2067
if b_index in killed_a:
2068
yield 'conflicted-b', self.lines_b[b_index]
2070
yield 'new-b', self.lines_b[b_index]
2072
yield 'killed-a', self.lines_b[b_index]
2073
# handle common lines
2074
for a_index in range(i, i+n):
2075
yield 'unchanged', self.lines_a[a_index]
2079
def _get_matching_blocks(self, left_revision, right_revision):
2080
"""Return a description of which sections of two revisions match.
2082
See SequenceMatcher.get_matching_blocks
2084
cached = self._cached_matching_blocks.get((left_revision,
2086
if cached is not None:
2088
if self._last_lines_revision_id == left_revision:
2089
left_lines = self._last_lines
2090
right_lines = self.get_lines([right_revision])[right_revision]
2092
lines = self.get_lines([left_revision, right_revision])
2093
left_lines = lines[left_revision]
2094
right_lines = lines[right_revision]
2095
self._last_lines = right_lines
2096
self._last_lines_revision_id = right_revision
2097
matcher = patiencediff.PatienceSequenceMatcher(None, left_lines,
2099
return matcher.get_matching_blocks()
2101
def _unique_lines(self, matching_blocks):
2102
"""Analyse matching_blocks to determine which lines are unique
2104
:return: a tuple of (unique_left, unique_right), where the values are
2105
sets of line numbers of unique lines.
2111
for i, j, n in matching_blocks:
2112
unique_left.extend(range(last_i, i))
2113
unique_right.extend(range(last_j, j))
2116
return unique_left, unique_right
2119
def _subtract_plans(old_plan, new_plan):
2120
"""Remove changes from new_plan that came from old_plan.
2122
It is assumed that the difference between the old_plan and new_plan
2123
is their choice of 'b' text.
2125
All lines from new_plan that differ from old_plan are emitted
2126
verbatim. All lines from new_plan that match old_plan but are
2127
not about the 'b' revision are emitted verbatim.
2129
Lines that match and are about the 'b' revision are the lines we
2130
don't want, so we convert 'killed-b' -> 'unchanged', and 'new-b'
2131
is skipped entirely.
2133
matcher = patiencediff.PatienceSequenceMatcher(None, old_plan,
2136
for i, j, n in matcher.get_matching_blocks():
2137
for jj in range(last_j, j):
2139
for jj in range(j, j+n):
2140
plan_line = new_plan[jj]
2141
if plan_line[0] == 'new-b':
2143
elif plan_line[0] == 'killed-b':
2144
yield 'unchanged', plan_line[1]
2150
class _PlanMerge(_PlanMergeBase):
2151
"""Plan an annotate merge using on-the-fly annotation"""
2153
def __init__(self, a_rev, b_rev, vf, key_prefix):
2154
super(_PlanMerge, self).__init__(a_rev, b_rev, vf, key_prefix)
2155
self.a_key = self._key_prefix + (self.a_rev,)
2156
self.b_key = self._key_prefix + (self.b_rev,)
2157
self.graph = _mod_graph.Graph(self.vf)
2158
heads = self.graph.heads((self.a_key, self.b_key))
2160
# one side dominates, so we can just return its values, yay for
2162
# Ideally we would know that before we get this far
2163
self._head_key = heads.pop()
2164
if self._head_key == self.a_key:
2168
trace.mutter('found dominating revision for %s\n%s > %s', self.vf,
2169
self._head_key[-1], other)
2172
self._head_key = None
2175
def _precache_tip_lines(self):
2176
# Turn this into a no-op, because we will do this later
2179
def _find_recursive_lcas(self):
2180
"""Find all the ancestors back to a unique lca"""
2181
cur_ancestors = (self.a_key, self.b_key)
2182
# graph.find_lca(uncommon, keys) now returns plain NULL_REVISION,
2183
# rather than a key tuple. We will just map that directly to no common
2187
next_lcas = self.graph.find_lca(*cur_ancestors)
2188
# Map a plain NULL_REVISION to a simple no-ancestors
2189
if next_lcas == {_mod_revision.NULL_REVISION}:
2191
# Order the lca's based on when they were merged into the tip
2192
# While the actual merge portion of weave merge uses a set() of
2193
# active revisions, the order of insertion *does* effect the
2194
# implicit ordering of the texts.
2195
for rev_key in cur_ancestors:
2196
ordered_parents = tuple(self.graph.find_merge_order(rev_key,
2198
parent_map[rev_key] = ordered_parents
2199
if len(next_lcas) == 0:
2201
elif len(next_lcas) == 1:
2202
parent_map[list(next_lcas)[0]] = ()
2204
elif len(next_lcas) > 2:
2205
# More than 2 lca's, fall back to grabbing all nodes between
2206
# this and the unique lca.
2207
trace.mutter('More than 2 LCAs, falling back to all nodes for:'
2209
self.a_key, self.b_key, cur_ancestors)
2210
cur_lcas = next_lcas
2211
while len(cur_lcas) > 1:
2212
cur_lcas = self.graph.find_lca(*cur_lcas)
2213
if len(cur_lcas) == 0:
2214
# No common base to find, use the full ancestry
2217
unique_lca = list(cur_lcas)[0]
2218
if unique_lca == _mod_revision.NULL_REVISION:
2219
# find_lca will return a plain 'NULL_REVISION' rather
2220
# than a key tuple when there is no common ancestor, we
2221
# prefer to just use None, because it doesn't confuse
2222
# _get_interesting_texts()
2224
parent_map.update(self._find_unique_parents(next_lcas,
2227
cur_ancestors = next_lcas
2230
def _find_unique_parents(self, tip_keys, base_key):
2231
"""Find ancestors of tip that aren't ancestors of base.
2233
:param tip_keys: Nodes that are interesting
2234
:param base_key: Cull all ancestors of this node
2235
:return: The parent map for all revisions between tip_keys and
2236
base_key. base_key will be included. References to nodes outside of
2237
the ancestor set will also be removed.
2239
# TODO: this would be simpler if find_unique_ancestors took a list
2240
# instead of a single tip, internally it supports it, but it
2241
# isn't a "backwards compatible" api change.
2242
if base_key is None:
2243
parent_map = dict(self.graph.iter_ancestry(tip_keys))
2244
# We remove NULL_REVISION because it isn't a proper tuple key, and
2245
# thus confuses things like _get_interesting_texts, and our logic
2246
# to add the texts into the memory weave.
2247
if _mod_revision.NULL_REVISION in parent_map:
2248
parent_map.pop(_mod_revision.NULL_REVISION)
2251
for tip in tip_keys:
2253
self.graph.find_unique_ancestors(tip, [base_key]))
2254
parent_map = self.graph.get_parent_map(interesting)
2255
parent_map[base_key] = ()
2256
culled_parent_map, child_map, tails = self._remove_external_references(
2258
# Remove all the tails but base_key
2259
if base_key is not None:
2260
tails.remove(base_key)
2261
self._prune_tails(culled_parent_map, child_map, tails)
2262
# Now remove all the uninteresting 'linear' regions
2263
simple_map = _mod_graph.collapse_linear_regions(culled_parent_map)
2267
def _remove_external_references(parent_map):
2268
"""Remove references that go outside of the parent map.
2270
:param parent_map: Something returned from Graph.get_parent_map(keys)
2271
:return: (filtered_parent_map, child_map, tails)
2272
filtered_parent_map is parent_map without external references
2273
child_map is the {parent_key: [child_keys]} mapping
2274
tails is a list of nodes that do not have any parents in the map
2276
# TODO: The basic effect of this function seems more generic than
2277
# _PlanMerge. But the specific details of building a child_map,
2278
# and computing tails seems very specific to _PlanMerge.
2279
# Still, should this be in Graph land?
2280
filtered_parent_map = {}
2283
for key, parent_keys in viewitems(parent_map):
2284
culled_parent_keys = [p for p in parent_keys if p in parent_map]
2285
if not culled_parent_keys:
2287
for parent_key in culled_parent_keys:
2288
child_map.setdefault(parent_key, []).append(key)
2289
# TODO: Do we want to do this, it adds overhead for every node,
2290
# just to say that the node has no children
2291
child_map.setdefault(key, [])
2292
filtered_parent_map[key] = culled_parent_keys
2293
return filtered_parent_map, child_map, tails
2296
def _prune_tails(parent_map, child_map, tails_to_remove):
2297
"""Remove tails from the parent map.
2299
This will remove the supplied revisions until no more children have 0
2302
:param parent_map: A dict of {child: [parents]}, this dictionary will
2303
be modified in place.
2304
:param tails_to_remove: A list of tips that should be removed,
2305
this list will be consumed
2306
:param child_map: The reverse dict of parent_map ({parent: [children]})
2307
this dict will be modified
2308
:return: None, parent_map will be modified in place.
2310
while tails_to_remove:
2311
next = tails_to_remove.pop()
2312
parent_map.pop(next)
2313
children = child_map.pop(next)
2314
for child in children:
2315
child_parents = parent_map[child]
2316
child_parents.remove(next)
2317
if len(child_parents) == 0:
2318
tails_to_remove.append(child)
2320
def _get_interesting_texts(self, parent_map):
2321
"""Return a dict of texts we are interested in.
2323
Note that the input is in key tuples, but the output is in plain
2326
:param parent_map: The output from _find_recursive_lcas
2327
:return: A dict of {'revision_id':lines} as returned by
2328
_PlanMergeBase.get_lines()
2330
all_revision_keys = set(parent_map)
2331
all_revision_keys.add(self.a_key)
2332
all_revision_keys.add(self.b_key)
2334
# Everything else is in 'keys' but get_lines is in 'revision_ids'
2335
all_texts = self.get_lines([k[-1] for k in all_revision_keys])
2338
def _build_weave(self):
2339
from .bzr import weave
2340
self._weave = weave.Weave(weave_name='in_memory_weave',
2341
allow_reserved=True)
2342
parent_map = self._find_recursive_lcas()
2344
all_texts = self._get_interesting_texts(parent_map)
2346
# Note: Unfortunately, the order given by topo_sort will effect the
2347
# ordering resolution in the output. Specifically, if you add A then B,
2348
# then in the output text A lines will show up before B lines. And, of
2349
# course, topo_sort doesn't guarantee any real ordering.
2350
# So we use merge_sort, and add a fake node on the tip.
2351
# This ensures that left-hand parents will always be inserted into the
2352
# weave before right-hand parents.
2353
tip_key = self._key_prefix + (_mod_revision.CURRENT_REVISION,)
2354
parent_map[tip_key] = (self.a_key, self.b_key)
2356
for seq_num, key, depth, eom in reversed(tsort.merge_sort(parent_map,
2360
# for key in tsort.topo_sort(parent_map):
2361
parent_keys = parent_map[key]
2362
revision_id = key[-1]
2363
parent_ids = [k[-1] for k in parent_keys]
2364
self._weave.add_lines(revision_id, parent_ids,
2365
all_texts[revision_id])
2367
def plan_merge(self):
2368
"""Generate a 'plan' for merging the two revisions.
2370
This involves comparing their texts and determining the cause of
2371
differences. If text A has a line and text B does not, then either the
2372
line was added to text A, or it was deleted from B. Once the causes
2373
are combined, they are written out in the format described in
2374
VersionedFile.plan_merge
2376
if self._head_key is not None: # There was a single head
2377
if self._head_key == self.a_key:
2380
if self._head_key != self.b_key:
2381
raise AssertionError('There was an invalid head: %s != %s'
2382
% (self.b_key, self._head_key))
2384
head_rev = self._head_key[-1]
2385
lines = self.get_lines([head_rev])[head_rev]
2386
return ((plan, line) for line in lines)
2387
return self._weave.plan_merge(self.a_rev, self.b_rev)
2390
class _PlanLCAMerge(_PlanMergeBase):
2392
This merge algorithm differs from _PlanMerge in that:
2394
1. comparisons are done against LCAs only
2395
2. cases where a contested line is new versus one LCA but old versus
2396
another are marked as conflicts, by emitting the line as conflicted-a
2399
This is faster, and hopefully produces more useful output.
2402
def __init__(self, a_rev, b_rev, vf, key_prefix, graph):
2403
_PlanMergeBase.__init__(self, a_rev, b_rev, vf, key_prefix)
2404
lcas = graph.find_lca(key_prefix + (a_rev,), key_prefix + (b_rev,))
2407
if lca == _mod_revision.NULL_REVISION:
2410
self.lcas.add(lca[-1])
2411
for lca in self.lcas:
2412
if _mod_revision.is_null(lca):
2415
lca_lines = self.get_lines([lca])[lca]
2416
matcher = patiencediff.PatienceSequenceMatcher(None, self.lines_a,
2418
blocks = list(matcher.get_matching_blocks())
2419
self._cached_matching_blocks[(a_rev, lca)] = blocks
2420
matcher = patiencediff.PatienceSequenceMatcher(None, self.lines_b,
2422
blocks = list(matcher.get_matching_blocks())
2423
self._cached_matching_blocks[(b_rev, lca)] = blocks
2425
def _determine_status(self, revision_id, unique_line_numbers):
2426
"""Determines the status unique lines versus all lcas.
2428
Basically, determines why the line is unique to this revision.
2430
A line may be determined new, killed, or both.
2432
If a line is determined new, that means it was not present in at least
2433
one LCA, and is not present in the other merge revision.
2435
If a line is determined killed, that means the line was present in
2438
If a line is killed and new, this indicates that the two merge
2439
revisions contain differing conflict resolutions.
2441
:param revision_id: The id of the revision in which the lines are
2443
:param unique_line_numbers: The line numbers of unique lines.
2444
:return: a tuple of (new_this, killed_other)
2448
unique_line_numbers = set(unique_line_numbers)
2449
for lca in self.lcas:
2450
blocks = self._get_matching_blocks(revision_id, lca)
2451
unique_vs_lca, _ignored = self._unique_lines(blocks)
2452
new.update(unique_line_numbers.intersection(unique_vs_lca))
2453
killed.update(unique_line_numbers.difference(unique_vs_lca))