/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to breezy/merge.py

  • Committer: Breezy landing bot
  • Author(s): Colin Watson
  • Date: 2020-11-16 21:47:08 UTC
  • mfrom: (7521.1.1 remove-lp-workaround)
  • Revision ID: breezy.the.bot@gmail.com-20201116214708-jos209mgxi41oy15
Remove breezy.git workaround for bazaar.launchpad.net.

Merged from https://code.launchpad.net/~cjwatson/brz/remove-lp-workaround/+merge/393710

Show diffs side-by-side

added added

removed removed

Lines of Context:
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
16
 
17
 
from __future__ import absolute_import
18
 
 
19
 
import warnings
20
 
 
21
 
from bzrlib.lazy_import import lazy_import
 
17
import contextlib
 
18
 
 
19
from .lazy_import import lazy_import
22
20
lazy_import(globals(), """
23
 
from bzrlib import (
 
21
import patiencediff
 
22
 
 
23
from breezy import (
24
24
    branch as _mod_branch,
25
 
    cleanup,
26
25
    conflicts as _mod_conflicts,
27
26
    debug,
28
 
    generate_ids,
29
27
    graph as _mod_graph,
30
28
    merge3,
31
29
    osutils,
32
 
    patiencediff,
33
30
    revision as _mod_revision,
34
31
    textfile,
35
32
    trace,
36
 
    transform,
37
33
    tree as _mod_tree,
38
34
    tsort,
39
35
    ui,
 
36
    workingtree,
 
37
    )
 
38
from breezy.bzr import (
 
39
    conflicts as _mod_bzr_conflicts,
 
40
    generate_ids,
40
41
    versionedfile,
41
 
    workingtree,
42
42
    )
43
 
from bzrlib.i18n import gettext
 
43
from breezy.i18n import gettext
44
44
""")
45
 
from bzrlib import (
 
45
from breezy.bzr.conflicts import Conflict as BzrConflict
 
46
from . import (
46
47
    decorators,
47
48
    errors,
48
49
    hooks,
49
50
    registry,
50
 
    )
51
 
from bzrlib.symbol_versioning import (
52
 
    deprecated_in,
53
 
    deprecated_method,
 
51
    transform,
54
52
    )
55
53
# TODO: Report back as changes are merged in
56
54
 
57
55
 
58
 
def transform_tree(from_tree, to_tree, interesting_ids=None):
59
 
    from_tree.lock_tree_write()
60
 
    operation = cleanup.OperationWithCleanups(merge_inner)
61
 
    operation.add_cleanup(from_tree.unlock)
62
 
    operation.run_simple(from_tree.branch, to_tree, from_tree,
63
 
        ignore_zero=True, interesting_ids=interesting_ids, this_tree=from_tree)
 
56
def transform_tree(from_tree, to_tree, interesting_files=None):
 
57
    with from_tree.lock_tree_write():
 
58
        merge_inner(from_tree.branch, to_tree, from_tree,
 
59
                    ignore_zero=True, this_tree=from_tree,
 
60
                    interesting_files=interesting_files)
64
61
 
65
62
 
66
63
class MergeHooks(hooks.Hooks):
67
64
 
68
65
    def __init__(self):
69
 
        hooks.Hooks.__init__(self, "bzrlib.merge", "Merger.hooks")
 
66
        hooks.Hooks.__init__(self, "breezy.merge", "Merger.hooks")
70
67
        self.add_hook('merge_file_content',
71
 
            "Called with a bzrlib.merge.Merger object to create a per file "
72
 
            "merge object when starting a merge. "
73
 
            "Should return either None or a subclass of "
74
 
            "``bzrlib.merge.AbstractPerFileMerger``. "
75
 
            "Such objects will then be called per file "
76
 
            "that needs to be merged (including when one "
77
 
            "side has deleted the file and the other has changed it). "
78
 
            "See the AbstractPerFileMerger API docs for details on how it is "
79
 
            "used by merge.",
80
 
            (2, 1))
 
68
                      "Called with a breezy.merge.Merger object to create a per file "
 
69
                      "merge object when starting a merge. "
 
70
                      "Should return either None or a subclass of "
 
71
                      "``breezy.merge.AbstractPerFileMerger``. "
 
72
                      "Such objects will then be called per file "
 
73
                      "that needs to be merged (including when one "
 
74
                      "side has deleted the file and the other has changed it). "
 
75
                      "See the AbstractPerFileMerger API docs for details on how it is "
 
76
                      "used by merge.",
 
77
                      (2, 1))
81
78
        self.add_hook('pre_merge',
82
 
            'Called before a merge. '
83
 
            'Receives a Merger object as the single argument.',
84
 
            (2, 5))
 
79
                      'Called before a merge. '
 
80
                      'Receives a Merger object as the single argument.',
 
81
                      (2, 5))
85
82
        self.add_hook('post_merge',
86
 
            'Called after a merge. '
87
 
            'Receives a Merger object as the single argument. '
88
 
            'The return value is ignored.',
89
 
            (2, 5))
 
83
                      'Called after a merge. '
 
84
                      'Receives a Merger object as the single argument. '
 
85
                      'The return value is ignored.',
 
86
                      (2, 5))
90
87
 
91
88
 
92
89
class AbstractPerFileMerger(object):
93
 
    """PerFileMerger objects are used by plugins extending merge for bzrlib.
94
 
 
95
 
    See ``bzrlib.plugins.news_merge.news_merge`` for an example concrete class.
96
 
    
 
90
    """PerFileMerger objects are used by plugins extending merge for breezy.
 
91
 
 
92
    See ``breezy.plugins.news_merge.news_merge`` for an example concrete class.
 
93
 
97
94
    :ivar merger: The Merge3Merger performing the merge.
98
95
    """
99
96
 
103
100
 
104
101
    def merge_contents(self, merge_params):
105
102
        """Attempt to merge the contents of a single file.
106
 
        
107
 
        :param merge_params: A bzrlib.merge.MergeFileHookParams
 
103
 
 
104
        :param merge_params: A breezy.merge.MergeFileHookParams
108
105
        :return: A tuple of (status, chunks), where status is one of
109
106
            'not_applicable', 'success', 'conflicted', or 'delete'.  If status
110
107
            is 'success' or 'conflicted', then chunks should be an iterable of
129
126
        """
130
127
        raise NotImplementedError(self.file_matches)
131
128
 
132
 
    def get_filename(self, params, tree):
133
 
        """Lookup the filename (i.e. basename, not path), given a Tree (e.g.
134
 
        self.merger.this_tree) and a MergeFileHookParams.
135
 
        """
136
 
        return osutils.basename(tree.id2path(params.file_id))
137
 
 
138
 
    def get_filepath(self, params, tree):
139
 
        """Calculate the path to the file in a tree.
140
 
 
141
 
        :param params: A MergeFileHookParams describing the file to merge
142
 
        :param tree: a Tree, e.g. self.merger.this_tree.
143
 
        """
144
 
        return tree.id2path(params.file_id)
145
 
 
146
129
    def merge_contents(self, params):
147
130
        """Merge the contents of a single file."""
148
131
        # Check whether this custom merge logic should be used.
152
135
            # THIS and OTHER aren't both files.
153
136
            not params.is_file_merge() or
154
137
            # The filename doesn't match
155
 
            not self.file_matches(params)):
 
138
                not self.file_matches(params)):
156
139
            return 'not_applicable', None
157
140
        return self.merge_matching(params)
158
141
 
172
155
    This is a base class for concrete custom file merging logic. Concrete
173
156
    classes should implement ``merge_text``.
174
157
 
175
 
    See ``bzrlib.plugins.news_merge.news_merge`` for an example concrete class.
176
 
    
 
158
    See ``breezy.plugins.news_merge.news_merge`` for an example concrete class.
 
159
 
177
160
    :ivar affected_files: The configured file paths to merge.
178
161
 
179
162
    :cvar name_prefix: The prefix to use when looking up configuration
180
163
        details. <name_prefix>_merge_files describes the files targeted by the
181
164
        hook for example.
182
 
        
 
165
 
183
166
    :cvar default_files: The default file paths to merge when no configuration
184
167
        is present.
185
168
    """
206
189
            config = self.merger.this_branch.get_config()
207
190
            # Until bzr provides a better policy for caching the config, we
208
191
            # just add the part we're interested in to the params to avoid
209
 
            # reading the config files repeatedly (bazaar.conf, location.conf,
 
192
            # reading the config files repeatedly (breezy.conf, location.conf,
210
193
            # branch.conf).
211
194
            config_key = self.name_prefix + '_merge_files'
212
195
            affected_files = config.get_user_option_as_list(config_key)
215
198
                affected_files = self.default_files
216
199
            self.affected_files = affected_files
217
200
        if affected_files:
218
 
            filepath = self.get_filepath(params, self.merger.this_tree)
 
201
            filepath = params.this_path
219
202
            if filepath in affected_files:
220
203
                return True
221
204
        return False
228
211
 
229
212
        This is called after checking that the merge should be performed in
230
213
        merge_contents, and it should behave as per
231
 
        ``bzrlib.merge.AbstractPerFileMerger.merge_contents``.
 
214
        ``breezy.merge.AbstractPerFileMerger.merge_contents``.
232
215
        """
233
216
        raise NotImplementedError(self.merge_text)
234
217
 
238
221
 
239
222
    There are some fields hooks can access:
240
223
 
241
 
    :ivar file_id: the file ID of the file being merged
 
224
    :ivar base_path: Path in base tree
 
225
    :ivar other_path: Path in other tree
 
226
    :ivar this_path: Path in this tree
242
227
    :ivar trans_id: the transform ID for the merge of this file
243
 
    :ivar this_kind: kind of file_id in 'this' tree
244
 
    :ivar other_kind: kind of file_id in 'other' tree
 
228
    :ivar this_kind: kind of file in 'this' tree
 
229
    :ivar other_kind: kind of file in 'other' tree
245
230
    :ivar winner: one of 'this', 'other', 'conflict'
246
231
    """
247
232
 
248
 
    def __init__(self, merger, file_id, trans_id, this_kind, other_kind,
249
 
            winner):
 
233
    def __init__(self, merger, paths, trans_id, this_kind, other_kind,
 
234
                 winner):
250
235
        self._merger = merger
251
 
        self.file_id = file_id
 
236
        self.paths = paths
 
237
        self.base_path, self.other_path, self.this_path = paths
252
238
        self.trans_id = trans_id
253
239
        self.this_kind = this_kind
254
240
        self.other_kind = other_kind
261
247
    @decorators.cachedproperty
262
248
    def base_lines(self):
263
249
        """The lines of the 'base' version of the file."""
264
 
        return self._merger.get_lines(self._merger.base_tree, self.file_id)
 
250
        return self._merger.get_lines(self._merger.base_tree, self.base_path)
265
251
 
266
252
    @decorators.cachedproperty
267
253
    def this_lines(self):
268
254
        """The lines of the 'this' version of the file."""
269
 
        return self._merger.get_lines(self._merger.this_tree, self.file_id)
 
255
        return self._merger.get_lines(self._merger.this_tree, self.this_path)
270
256
 
271
257
    @decorators.cachedproperty
272
258
    def other_lines(self):
273
259
        """The lines of the 'other' version of the file."""
274
 
        return self._merger.get_lines(self._merger.other_tree, self.file_id)
 
260
        return self._merger.get_lines(self._merger.other_tree, self.other_path)
275
261
 
276
262
 
277
263
class Merger(object):
279
265
    hooks = MergeHooks()
280
266
 
281
267
    def __init__(self, this_branch, other_tree=None, base_tree=None,
282
 
                 this_tree=None, pb=None, change_reporter=None,
 
268
                 this_tree=None, change_reporter=None,
283
269
                 recurse='down', revision_graph=None):
284
270
        object.__init__(self)
285
271
        self.this_branch = this_branch
294
280
        self.base_tree = base_tree
295
281
        self.ignore_zero = False
296
282
        self.backup_files = False
297
 
        self.interesting_ids = None
298
283
        self.interesting_files = None
299
284
        self.show_base = False
300
285
        self.reprocess = False
301
 
        if pb is not None:
302
 
            warnings.warn("pb parameter to Merger() is deprecated and ignored")
303
286
        self.pp = None
304
287
        self.recurse = recurse
305
288
        self.change_reporter = change_reporter
353
336
                                      _set_base_is_other_ancestor)
354
337
 
355
338
    @staticmethod
356
 
    def from_uncommitted(tree, other_tree, pb=None, base_tree=None):
 
339
    def from_uncommitted(tree, other_tree, base_tree=None):
357
340
        """Return a Merger for uncommitted changes in other_tree.
358
341
 
359
342
        :param tree: The tree to merge into
360
343
        :param other_tree: The tree to get uncommitted changes from
361
 
        :param pb: A progress indicator
362
344
        :param base_tree: The basis to use for the merge.  If unspecified,
363
345
            other_tree.basis_tree() will be used.
364
346
        """
365
347
        if base_tree is None:
366
348
            base_tree = other_tree.basis_tree()
367
 
        merger = Merger(tree.branch, other_tree, base_tree, tree, pb)
 
349
        merger = Merger(tree.branch, other_tree, base_tree, tree)
368
350
        merger.base_rev_id = merger.base_tree.get_revision_id()
369
351
        merger.other_rev_id = None
370
352
        merger.other_basis = merger.base_rev_id
371
353
        return merger
372
354
 
373
355
    @classmethod
374
 
    def from_mergeable(klass, tree, mergeable, pb):
 
356
    def from_mergeable(klass, tree, mergeable):
375
357
        """Return a Merger for a bundle or merge directive.
376
358
 
377
359
        :param tree: The tree to merge changes into
378
360
        :param mergeable: A merge directive or bundle
379
 
        :param pb: A progress indicator
380
361
        """
381
362
        mergeable.install_revisions(tree.branch.repository)
382
363
        base_revision_id, other_revision_id, verified =\
385
366
        if base_revision_id is not None:
386
367
            if (base_revision_id != _mod_revision.NULL_REVISION and
387
368
                revision_graph.is_ancestor(
388
 
                base_revision_id, tree.branch.last_revision())):
 
369
                    base_revision_id, tree.branch.last_revision())):
389
370
                base_revision_id = None
390
371
            else:
391
372
                trace.warning('Performing cherrypick')
392
 
        merger = klass.from_revision_ids(pb, tree, other_revision_id,
393
 
                                         base_revision_id, revision_graph=
394
 
                                         revision_graph)
 
373
        merger = klass.from_revision_ids(tree, other_revision_id,
 
374
                                         base_revision_id, revision_graph=revision_graph)
395
375
        return merger, verified
396
376
 
397
377
    @staticmethod
398
 
    def from_revision_ids(pb, tree, other, base=None, other_branch=None,
 
378
    def from_revision_ids(tree, other, base=None, other_branch=None,
399
379
                          base_branch=None, revision_graph=None,
400
380
                          tree_branch=None):
401
381
        """Return a Merger for revision-ids.
402
382
 
403
 
        :param pb: A progress indicator
404
383
        :param tree: The tree to merge changes into
405
384
        :param other: The revision-id to use as OTHER
406
385
        :param base: The revision-id to use as BASE.  If not specified, will
416
395
        """
417
396
        if tree_branch is None:
418
397
            tree_branch = tree.branch
419
 
        merger = Merger(tree_branch, this_tree=tree, pb=pb,
 
398
        merger = Merger(tree_branch, this_tree=tree,
420
399
                        revision_graph=revision_graph)
421
400
        if other_branch is None:
422
401
            other_branch = tree.branch
459
438
 
460
439
    def set_pending(self):
461
440
        if (not self.base_is_ancestor or not self.base_is_other_ancestor
462
 
            or self.other_rev_id is None):
 
441
                or self.other_rev_id is None):
463
442
            return
464
443
        self._add_parent()
465
444
 
466
445
    def _add_parent(self):
467
446
        new_parents = self.this_tree.get_parent_ids() + [self.other_rev_id]
468
447
        new_parent_trees = []
469
 
        operation = cleanup.OperationWithCleanups(
470
 
            self.this_tree.set_parent_trees)
471
 
        for revision_id in new_parents:
472
 
            try:
473
 
                tree = self.revision_tree(revision_id)
474
 
            except errors.NoSuchRevision:
475
 
                tree = None
476
 
            else:
477
 
                tree.lock_read()
478
 
                operation.add_cleanup(tree.unlock)
479
 
            new_parent_trees.append((revision_id, tree))
480
 
        operation.run_simple(new_parent_trees, allow_leftmost_as_ghost=True)
 
448
        with contextlib.ExitStack() as stack:
 
449
            for revision_id in new_parents:
 
450
                try:
 
451
                    tree = self.revision_tree(revision_id)
 
452
                except errors.NoSuchRevision:
 
453
                    tree = None
 
454
                else:
 
455
                    stack.enter_context(tree.lock_read())
 
456
                new_parent_trees.append((revision_id, tree))
 
457
            self.this_tree.set_parent_trees(new_parent_trees, allow_leftmost_as_ghost=True)
481
458
 
482
459
    def set_other(self, other_revision, possible_transports=None):
483
460
        """Set the revision and tree to merge from.
504
481
                raise errors.NoCommits(self.other_branch)
505
482
        if self.other_rev_id is not None:
506
483
            self._cached_trees[self.other_rev_id] = self.other_tree
507
 
        self._maybe_fetch(self.other_branch, self.this_branch, self.other_basis)
 
484
        self._maybe_fetch(self.other_branch,
 
485
                          self.this_branch, self.other_basis)
508
486
 
509
487
    def set_other_revision(self, revision_id, other_branch):
510
488
        """Set 'other' based on a branch and revision id
547
525
                self.base_rev_id = _mod_revision.NULL_REVISION
548
526
            elif len(lcas) == 1:
549
527
                self.base_rev_id = list(lcas)[0]
550
 
            else: # len(lcas) > 1
 
528
            else:  # len(lcas) > 1
551
529
                self._is_criss_cross = True
552
530
                if len(lcas) > 2:
553
531
                    # find_unique_lca can only handle 2 nodes, so we have to
555
533
                    # the graph again, but better than re-implementing
556
534
                    # find_unique_lca.
557
535
                    self.base_rev_id = self.revision_graph.find_unique_lca(
558
 
                                            revisions[0], revisions[1])
 
536
                        revisions[0], revisions[1])
559
537
                else:
560
538
                    self.base_rev_id = self.revision_graph.find_unique_lca(
561
 
                                            *lcas)
 
539
                        *lcas)
562
540
                sorted_lca_keys = self.revision_graph.find_merge_order(
563
541
                    revisions[0], lcas)
564
542
                if self.base_rev_id == _mod_revision.NULL_REVISION:
577
555
                interesting_revision_ids = set(lcas)
578
556
                interesting_revision_ids.add(self.base_rev_id)
579
557
                interesting_trees = dict((t.get_revision_id(), t)
580
 
                    for t in self.this_branch.repository.revision_trees(
581
 
                        interesting_revision_ids))
 
558
                                         for t in self.this_branch.repository.revision_trees(
 
559
                    interesting_revision_ids))
582
560
                self._cached_trees.update(interesting_trees)
583
561
                if self.base_rev_id in lcas:
584
562
                    self.base_tree = interesting_trees[self.base_rev_id]
614
592
    def make_merger(self):
615
593
        kwargs = {'working_tree': self.this_tree, 'this_tree': self.this_tree,
616
594
                  'other_tree': self.other_tree,
617
 
                  'interesting_ids': self.interesting_ids,
618
595
                  'interesting_files': self.interesting_files,
619
596
                  'this_branch': self.this_branch,
620
597
                  'other_branch': self.other_branch,
633
610
            raise errors.BzrError("Showing base is not supported for this"
634
611
                                  " merge type. %s" % self.merge_type)
635
612
        if (not getattr(self.merge_type, 'supports_reverse_cherrypick', True)
636
 
            and not self.base_is_other_ancestor):
 
613
                and not self.base_is_other_ancestor):
637
614
            raise errors.CannotReverseCherrypick()
638
615
        if self.merge_type.supports_cherrypick:
639
616
            kwargs['cherrypick'] = (not self.base_is_ancestor or
641
618
        if self._is_criss_cross and getattr(self.merge_type,
642
619
                                            'supports_lca_trees', False):
643
620
            kwargs['lca_trees'] = self._lca_trees
644
 
        return self.merge_type(pb=None,
645
 
                               change_reporter=self.change_reporter,
 
621
        return self.merge_type(change_reporter=self.change_reporter,
646
622
                               **kwargs)
647
623
 
648
624
    def _do_merge_to(self):
655
631
        for hook in Merger.hooks['post_merge']:
656
632
            hook(merge)
657
633
        if self.recurse == 'down':
658
 
            for relpath, file_id in self.this_tree.iter_references():
659
 
                sub_tree = self.this_tree.get_nested_tree(file_id, relpath)
 
634
            for relpath in self.this_tree.iter_references():
 
635
                sub_tree = self.this_tree.get_nested_tree(relpath)
660
636
                other_revision = self.other_tree.get_reference_revision(
661
 
                    file_id, relpath)
662
 
                if  other_revision == sub_tree.last_revision():
 
637
                    relpath)
 
638
                if other_revision == sub_tree.last_revision():
663
639
                    continue
664
640
                sub_merge = Merger(sub_tree.branch, this_tree=sub_tree)
665
641
                sub_merge.merge_type = self.merge_type
666
 
                other_branch = self.other_branch.reference_parent(file_id,
667
 
                                                                  relpath)
 
642
                other_branch = self.other_tree.reference_parent(relpath)
668
643
                sub_merge.set_other_revision(other_revision, other_branch)
669
 
                base_revision = self.base_tree.get_reference_revision(file_id)
 
644
                base_tree_path = _mod_tree.find_previous_path(
 
645
                    self.this_tree, self.base_tree, relpath)
 
646
                base_revision = self.base_tree.get_reference_revision(
 
647
                    base_tree_path)
670
648
                sub_merge.base_tree = \
671
649
                    sub_tree.branch.repository.revision_tree(base_revision)
672
650
                sub_merge.base_rev_id = base_revision
674
652
        return merge
675
653
 
676
654
    def do_merge(self):
677
 
        operation = cleanup.OperationWithCleanups(self._do_merge_to)
678
 
        self.this_tree.lock_tree_write()
679
 
        operation.add_cleanup(self.this_tree.unlock)
680
 
        if self.base_tree is not None:
681
 
            self.base_tree.lock_read()
682
 
            operation.add_cleanup(self.base_tree.unlock)
683
 
        if self.other_tree is not None:
684
 
            self.other_tree.lock_read()
685
 
            operation.add_cleanup(self.other_tree.unlock)
686
 
        merge = operation.run_simple()
 
655
        with contextlib.ExitStack() as stack:
 
656
            stack.enter_context(self.this_tree.lock_tree_write())
 
657
            if self.base_tree is not None:
 
658
                stack.enter_context(self.base_tree.lock_read())
 
659
            if self.other_tree is not None:
 
660
                stack.enter_context(self.other_tree.lock_read())
 
661
            merge = self._do_merge_to()
687
662
        if len(merge.cooked_conflicts) == 0:
688
663
            if not self.ignore_zero and not trace.is_quiet():
689
664
                trace.note(gettext("All changes applied successfully."))
708
683
    symlink_target = None
709
684
    text_sha1 = None
710
685
 
 
686
    def is_unmodified(self, other):
 
687
        return other is self
 
688
 
 
689
 
711
690
_none_entry = _InventoryNoneEntry()
712
691
 
713
692
 
721
700
    supports_reverse_cherrypick = True
722
701
    winner_idx = {"this": 2, "other": 1, "conflict": 1}
723
702
    supports_lca_trees = True
 
703
    requires_file_merge_plan = False
724
704
 
725
705
    def __init__(self, working_tree, this_tree, base_tree, other_tree,
726
 
                 interesting_ids=None, reprocess=False, show_base=False,
727
 
                 pb=None, pp=None, change_reporter=None,
728
 
                 interesting_files=None, do_merge=True,
 
706
                 reprocess=False, show_base=False,
 
707
                 change_reporter=None, interesting_files=None, do_merge=True,
729
708
                 cherrypick=False, lca_trees=None, this_branch=None,
730
709
                 other_branch=None):
731
710
        """Initialize the merger object and perform the merge.
737
716
        :param this_branch: The branch associated with this_tree.  Defaults to
738
717
            this_tree.branch if not supplied.
739
718
        :param other_branch: The branch associated with other_tree, if any.
740
 
        :param interesting_ids: The file_ids of files that should be
741
 
            participate in the merge.  May not be combined with
742
 
            interesting_files.
743
719
        :param: reprocess If True, perform conflict-reduction processing.
744
720
        :param show_base: If True, show the base revision in text conflicts.
745
721
            (incompatible with reprocess)
746
 
        :param pb: ignored
747
 
        :param pp: A ProgressPhase object
748
722
        :param change_reporter: An object that should report changes made
749
723
        :param interesting_files: The tree-relative paths of files that should
750
724
            participate in the merge.  If these paths refer to directories,
751
 
            the contents of those directories will also be included.  May not
752
 
            be combined with interesting_ids.  If neither interesting_files nor
753
 
            interesting_ids is specified, all files may participate in the
 
725
            the contents of those directories will also be included.  If not
 
726
            specified, all files may participate in the
754
727
            merge.
755
728
        :param lca_trees: Can be set to a dictionary of {revision_id:rev_tree}
756
729
            if the ancestry was found to include a criss-cross merge.
757
730
            Otherwise should be None.
758
731
        """
759
732
        object.__init__(self)
760
 
        if interesting_files is not None and interesting_ids is not None:
761
 
            raise ValueError(
762
 
                'specify either interesting_ids or interesting_files')
763
733
        if this_branch is None:
764
734
            this_branch = this_tree.branch
765
 
        self.interesting_ids = interesting_ids
766
735
        self.interesting_files = interesting_files
767
736
        self.working_tree = working_tree
768
737
        self.this_tree = this_tree
784
753
        self.cherrypick = cherrypick
785
754
        if do_merge:
786
755
            self.do_merge()
787
 
        if pp is not None:
788
 
            warnings.warn("pp argument to Merge3Merger is deprecated")
789
 
        if pb is not None:
790
 
            warnings.warn("pb argument to Merge3Merger is deprecated")
791
756
 
792
757
    def do_merge(self):
793
 
        operation = cleanup.OperationWithCleanups(self._do_merge)
794
 
        self.working_tree.lock_tree_write()
795
 
        operation.add_cleanup(self.working_tree.unlock)
796
 
        self.this_tree.lock_read()
797
 
        operation.add_cleanup(self.this_tree.unlock)
798
 
        self.base_tree.lock_read()
799
 
        operation.add_cleanup(self.base_tree.unlock)
800
 
        self.other_tree.lock_read()
801
 
        operation.add_cleanup(self.other_tree.unlock)
802
 
        operation.run()
803
 
 
804
 
    def _do_merge(self, operation):
805
 
        self.tt = transform.TreeTransform(self.working_tree, None)
806
 
        operation.add_cleanup(self.tt.finalize)
807
 
        self._compute_transform()
808
 
        results = self.tt.apply(no_conflicts=True)
809
 
        self.write_modified(results)
810
 
        try:
811
 
            self.working_tree.add_conflicts(self.cooked_conflicts)
812
 
        except errors.UnsupportedOperation:
813
 
            pass
 
758
        with contextlib.ExitStack() as stack:
 
759
            stack.enter_context(self.working_tree.lock_tree_write())
 
760
            stack.enter_context(self.this_tree.lock_read())
 
761
            stack.enter_context(self.base_tree.lock_read())
 
762
            stack.enter_context(self.other_tree.lock_read())
 
763
            self.tt = self.working_tree.transform()
 
764
            stack.enter_context(self.tt)
 
765
            self._compute_transform()
 
766
            results = self.tt.apply(no_conflicts=True)
 
767
            self.write_modified(results)
 
768
            try:
 
769
                self.working_tree.add_conflicts(self.cooked_conflicts)
 
770
            except errors.UnsupportedOperation:
 
771
                pass
814
772
 
815
773
    def make_preview_transform(self):
816
 
        operation = cleanup.OperationWithCleanups(self._make_preview_transform)
817
 
        self.base_tree.lock_read()
818
 
        operation.add_cleanup(self.base_tree.unlock)
819
 
        self.other_tree.lock_read()
820
 
        operation.add_cleanup(self.other_tree.unlock)
821
 
        return operation.run_simple()
822
 
 
823
 
    def _make_preview_transform(self):
824
 
        self.tt = transform.TransformPreview(self.working_tree)
825
 
        self._compute_transform()
826
 
        return self.tt
 
774
        with self.base_tree.lock_read(), self.other_tree.lock_read():
 
775
            self.tt = self.working_tree.preview_transform()
 
776
            self._compute_transform()
 
777
            return self.tt
827
778
 
828
779
    def _compute_transform(self):
829
780
        if self._lca_trees is None:
830
 
            entries = self._entries3()
 
781
            entries = list(self._entries3())
831
782
            resolver = self._three_way
832
783
        else:
833
 
            entries = self._entries_lca()
 
784
            entries = list(self._entries_lca())
834
785
            resolver = self._lca_multi_way
835
786
        # Prepare merge hooks
836
787
        factories = Merger.hooks['merge_file_content']
837
788
        # One hook for each registered one plus our default merger
838
789
        hooks = [factory(self) for factory in factories] + [self]
839
790
        self.active_hooks = [hook for hook in hooks if hook is not None]
840
 
        child_pb = ui.ui_factory.nested_progress_bar()
841
 
        try:
842
 
            for num, (file_id, changed, parents3, names3,
843
 
                      executable3) in enumerate(entries):
 
791
        with ui.ui_factory.nested_progress_bar() as child_pb:
 
792
            for num, (file_id, changed, paths3, parents3, names3,
 
793
                      executable3, copied) in enumerate(entries):
 
794
                if copied:
 
795
                    # Treat copies as simple adds for now
 
796
                    paths3 = (None, paths3[1], None)
 
797
                    parents3 = (None, parents3[1], None)
 
798
                    names3 = (None, names3[1], None)
 
799
                    executable3 = (None, executable3[1], None)
 
800
                    changed = True
 
801
                    copied = False
 
802
                trans_id = self.tt.trans_id_file_id(file_id)
844
803
                # Try merging each entry
845
804
                child_pb.update(gettext('Preparing file merge'),
846
805
                                num, len(entries))
847
 
                self._merge_names(file_id, parents3, names3, resolver=resolver)
 
806
                self._merge_names(trans_id, file_id, paths3, parents3,
 
807
                                  names3, resolver=resolver)
848
808
                if changed:
849
 
                    file_status = self._do_merge_contents(file_id)
 
809
                    file_status = self._do_merge_contents(paths3, trans_id, file_id)
850
810
                else:
851
811
                    file_status = 'unmodified'
852
 
                self._merge_executable(file_id,
853
 
                    executable3, file_status, resolver=resolver)
854
 
        finally:
855
 
            child_pb.finished()
 
812
                self._merge_executable(paths3, trans_id, executable3,
 
813
                                       file_status, resolver=resolver)
856
814
        self.tt.fixup_new_roots()
857
815
        self._finish_computing_transform()
858
816
 
861
819
 
862
820
        This is the second half of _compute_transform.
863
821
        """
864
 
        child_pb = ui.ui_factory.nested_progress_bar()
865
 
        try:
866
 
            fs_conflicts = transform.resolve_conflicts(self.tt, child_pb,
 
822
        with ui.ui_factory.nested_progress_bar() as child_pb:
 
823
            fs_conflicts = transform.resolve_conflicts(
 
824
                self.tt, child_pb,
867
825
                lambda t, c: transform.conflict_pass(t, c, self.other_tree))
868
 
        finally:
869
 
            child_pb.finished()
870
826
        if self.change_reporter is not None:
871
 
            from bzrlib import delta
 
827
            from breezy import delta
872
828
            delta.report_changes(
873
829
                self.tt.iter_changes(), self.change_reporter)
874
830
        self.cook_conflicts(fs_conflicts)
875
831
        for conflict in self.cooked_conflicts:
876
 
            trace.warning(unicode(conflict))
 
832
            trace.warning('%s', conflict.describe())
877
833
 
878
834
    def _entries3(self):
879
835
        """Gather data about files modified between three trees.
884
840
        other and this.  names3 is a tuple of names for base, other and this.
885
841
        executable3 is a tuple of execute-bit values for base, other and this.
886
842
        """
887
 
        result = []
888
843
        iterator = self.other_tree.iter_changes(self.base_tree,
889
 
                specific_files=self.interesting_files,
890
 
                extra_trees=[self.this_tree])
891
 
        this_entries = dict((e.file_id, e) for p, e in
892
 
                            self.this_tree.iter_entries_by_dir(
893
 
                            self.interesting_ids))
894
 
        for (file_id, paths, changed, versioned, parents, names, kind,
895
 
             executable) in iterator:
896
 
            if (self.interesting_ids is not None and
897
 
                file_id not in self.interesting_ids):
898
 
                continue
899
 
            entry = this_entries.get(file_id)
900
 
            if entry is not None:
901
 
                this_name = entry.name
902
 
                this_parent = entry.parent_id
903
 
                this_executable = entry.executable
 
844
                                                specific_files=self.interesting_files,
 
845
                                                extra_trees=[self.this_tree])
 
846
        this_interesting_files = self.this_tree.find_related_paths_across_trees(
 
847
            self.interesting_files, trees=[self.other_tree])
 
848
        this_entries = dict(self.this_tree.iter_entries_by_dir(
 
849
                            specific_files=this_interesting_files))
 
850
        for change in iterator:
 
851
            if change.path[0] is not None:
 
852
                this_path = _mod_tree.find_previous_path(
 
853
                    self.base_tree, self.this_tree, change.path[0])
 
854
            else:
 
855
                this_path = _mod_tree.find_previous_path(
 
856
                    self.other_tree, self.this_tree, change.path[1])
 
857
            this_entry = this_entries.get(this_path)
 
858
            if this_entry is not None:
 
859
                this_name = this_entry.name
 
860
                this_parent = this_entry.parent_id
 
861
                this_executable = this_entry.executable
904
862
            else:
905
863
                this_name = None
906
864
                this_parent = None
907
865
                this_executable = None
908
 
            parents3 = parents + (this_parent,)
909
 
            names3 = names + (this_name,)
910
 
            executable3 = executable + (this_executable,)
911
 
            result.append((file_id, changed, parents3, names3, executable3))
912
 
        return result
 
866
            parents3 = change.parent_id + (this_parent,)
 
867
            names3 = change.name + (this_name,)
 
868
            paths3 = change.path + (this_path, )
 
869
            executable3 = change.executable + (this_executable,)
 
870
            yield (
 
871
                (change.file_id, change.changed_content, paths3,
 
872
                 parents3, names3, executable3, change.copied))
913
873
 
914
874
    def _entries_lca(self):
915
875
        """Gather data about files modified between multiple trees.
919
879
 
920
880
        For the multi-valued entries, the format will be (BASE, [lca1, lca2])
921
881
 
922
 
        :return: [(file_id, changed, parents, names, executable)], where:
 
882
        :return: [(file_id, changed, paths, parents, names, executable, copied)], where:
923
883
 
924
884
            * file_id: Simple file_id of the entry
925
885
            * changed: Boolean, True if the kind or contents changed else False
 
886
            * paths: ((base, [path, in, lcas]), path_other, path_this)
926
887
            * parents: ((base, [parent_id, in, lcas]), parent_id_other,
927
888
                        parent_id_this)
928
889
            * names:   ((base, [name, in, lcas]), name_in_other, name_in_this)
933
894
            lookup_trees = [self.this_tree, self.base_tree]
934
895
            lookup_trees.extend(self._lca_trees)
935
896
            # I think we should include the lca trees as well
936
 
            interesting_ids = self.other_tree.paths2ids(self.interesting_files,
937
 
                                                        lookup_trees)
 
897
            interesting_files = self.other_tree.find_related_paths_across_trees(
 
898
                self.interesting_files, lookup_trees)
938
899
        else:
939
 
            interesting_ids = self.interesting_ids
940
 
        result = []
941
 
        walker = _mod_tree.MultiWalker(self.other_tree, self._lca_trees)
 
900
            interesting_files = None
 
901
        from .multiwalker import MultiWalker
 
902
        walker = MultiWalker(self.other_tree, self._lca_trees)
942
903
 
943
 
        base_inventory = self.base_tree.root_inventory
944
 
        this_inventory = self.this_tree.root_inventory
945
 
        for path, file_id, other_ie, lca_values in walker.iter_all():
 
904
        for other_path, file_id, other_ie, lca_values in walker.iter_all():
946
905
            # Is this modified at all from any of the other trees?
947
906
            if other_ie is None:
948
907
                other_ie = _none_entry
949
 
            if interesting_ids is not None and file_id not in interesting_ids:
 
908
                other_path = None
 
909
            if interesting_files is not None and other_path not in interesting_files:
950
910
                continue
951
911
 
952
912
            # If other_revision is found in any of the lcas, that means this
955
915
            # we know that the ancestry is linear, and that OTHER did not
956
916
            # modify anything
957
917
            # See doc/developers/lca_merge_resolution.txt for details
958
 
            other_revision = other_ie.revision
959
 
            if other_revision is not None:
960
 
                # We can't use this shortcut when other_revision is None,
961
 
                # because it may be None because things are WorkingTrees, and
962
 
                # not because it is *actually* None.
963
 
                is_unmodified = False
964
 
                for lca_path, ie in lca_values:
965
 
                    if ie is not None and ie.revision == other_revision:
966
 
                        is_unmodified = True
967
 
                        break
968
 
                if is_unmodified:
969
 
                    continue
 
918
            # We can't use this shortcut when other_revision is None,
 
919
            # because it may be None because things are WorkingTrees, and
 
920
            # not because it is *actually* None.
 
921
            is_unmodified = False
 
922
            for lca_path, ie in lca_values:
 
923
                if ie is not None and other_ie.is_unmodified(ie):
 
924
                    is_unmodified = True
 
925
                    break
 
926
            if is_unmodified:
 
927
                continue
970
928
 
971
929
            lca_entries = []
 
930
            lca_paths = []
972
931
            for lca_path, lca_ie in lca_values:
973
932
                if lca_ie is None:
974
933
                    lca_entries.append(_none_entry)
 
934
                    lca_paths.append(None)
975
935
                else:
976
936
                    lca_entries.append(lca_ie)
 
937
                    lca_paths.append(lca_path)
977
938
 
978
 
            if base_inventory.has_id(file_id):
979
 
                base_ie = base_inventory[file_id]
980
 
            else:
 
939
            try:
 
940
                base_path = self.base_tree.id2path(file_id)
 
941
            except errors.NoSuchId:
 
942
                base_path = None
981
943
                base_ie = _none_entry
 
944
            else:
 
945
                base_ie = next(self.base_tree.iter_entries_by_dir(specific_files=[base_path]))[1]
982
946
 
983
 
            if this_inventory.has_id(file_id):
984
 
                this_ie = this_inventory[file_id]
985
 
            else:
 
947
            try:
 
948
                this_path = self.this_tree.id2path(file_id)
 
949
            except errors.NoSuchId:
986
950
                this_ie = _none_entry
 
951
                this_path = None
 
952
            else:
 
953
                this_ie = next(self.this_tree.iter_entries_by_dir(specific_files=[this_path]))[1]
987
954
 
988
955
            lca_kinds = []
989
956
            lca_parent_ids = []
1014
981
                        continue
1015
982
                    content_changed = False
1016
983
                elif other_ie.kind is None or other_ie.kind == 'file':
1017
 
                    def get_sha1(ie, tree):
1018
 
                        if ie.kind != 'file':
1019
 
                            return None
1020
 
                        return tree.get_file_sha1(file_id)
1021
 
                    base_sha1 = get_sha1(base_ie, self.base_tree)
1022
 
                    lca_sha1s = [get_sha1(ie, tree) for ie, tree
1023
 
                                 in zip(lca_entries, self._lca_trees)]
1024
 
                    this_sha1 = get_sha1(this_ie, self.this_tree)
1025
 
                    other_sha1 = get_sha1(other_ie, self.other_tree)
 
984
                    def get_sha1(tree, path):
 
985
                        if path is None:
 
986
                            return None
 
987
                        try:
 
988
                            return tree.get_file_sha1(path)
 
989
                        except errors.NoSuchFile:
 
990
                            return None
 
991
                    base_sha1 = get_sha1(self.base_tree, base_path)
 
992
                    lca_sha1s = [get_sha1(tree, lca_path)
 
993
                                 for tree, lca_path
 
994
                                 in zip(self._lca_trees, lca_paths)]
 
995
                    this_sha1 = get_sha1(self.this_tree, this_path)
 
996
                    other_sha1 = get_sha1(self.other_tree, other_path)
1026
997
                    sha1_winner = self._lca_multi_way(
1027
998
                        (base_sha1, lca_sha1s), other_sha1, this_sha1,
1028
999
                        allow_overriding_lca=False)
1030
1001
                        (base_ie.executable, lca_executable),
1031
1002
                        other_ie.executable, this_ie.executable)
1032
1003
                    if (parent_id_winner == 'this' and name_winner == 'this'
1033
 
                        and sha1_winner == 'this' and exec_winner == 'this'):
 
1004
                            and sha1_winner == 'this' and exec_winner == 'this'):
1034
1005
                        # No kind, parent, name, exec, or content change for
1035
1006
                        # OTHER, so this node is not considered interesting
1036
1007
                        continue
1037
1008
                    if sha1_winner == 'this':
1038
1009
                        content_changed = False
1039
1010
                elif other_ie.kind == 'symlink':
1040
 
                    def get_target(ie, tree):
 
1011
                    def get_target(ie, tree, path):
1041
1012
                        if ie.kind != 'symlink':
1042
1013
                            return None
1043
 
                        return tree.get_symlink_target(file_id)
1044
 
                    base_target = get_target(base_ie, self.base_tree)
1045
 
                    lca_targets = [get_target(ie, tree) for ie, tree
1046
 
                                   in zip(lca_entries, self._lca_trees)]
1047
 
                    this_target = get_target(this_ie, self.this_tree)
1048
 
                    other_target = get_target(other_ie, self.other_tree)
 
1014
                        return tree.get_symlink_target(path)
 
1015
                    base_target = get_target(base_ie, self.base_tree, base_path)
 
1016
                    lca_targets = [get_target(ie, tree, lca_path) for ie, tree, lca_path
 
1017
                                   in zip(lca_entries, self._lca_trees, lca_paths)]
 
1018
                    this_target = get_target(
 
1019
                        this_ie, self.this_tree, this_path)
 
1020
                    other_target = get_target(
 
1021
                        other_ie, self.other_tree, other_path)
1049
1022
                    target_winner = self._lca_multi_way(
1050
1023
                        (base_target, lca_targets),
1051
1024
                        other_target, this_target)
1052
1025
                    if (parent_id_winner == 'this' and name_winner == 'this'
1053
 
                        and target_winner == 'this'):
 
1026
                            and target_winner == 'this'):
1054
1027
                        # No kind, parent, name, or symlink target change
1055
1028
                        # not interesting
1056
1029
                        continue
1068
1041
                    raise AssertionError('unhandled kind: %s' % other_ie.kind)
1069
1042
 
1070
1043
            # If we have gotten this far, that means something has changed
1071
 
            result.append((file_id, content_changed,
 
1044
            yield (file_id, content_changed,
 
1045
                           ((base_path, lca_paths),
 
1046
                            other_path, this_path),
1072
1047
                           ((base_ie.parent_id, lca_parent_ids),
1073
1048
                            other_ie.parent_id, this_ie.parent_id),
1074
1049
                           ((base_ie.name, lca_names),
1075
1050
                            other_ie.name, this_ie.name),
1076
1051
                           ((base_ie.executable, lca_executable),
1077
 
                            other_ie.executable, this_ie.executable)
1078
 
                          ))
1079
 
        return result
 
1052
                            other_ie.executable, this_ie.executable),
 
1053
                           # Copy detection is not yet supported, so nothing is
 
1054
                           # a copy:
 
1055
                           False
 
1056
                           )
1080
1057
 
1081
1058
    def write_modified(self, results):
 
1059
        if not self.working_tree.supports_merge_modified():
 
1060
            return
1082
1061
        modified_hashes = {}
1083
1062
        for path in results.modified_paths:
1084
 
            file_id = self.working_tree.path2id(self.working_tree.relpath(path))
1085
 
            if file_id is None:
 
1063
            wt_relpath = self.working_tree.relpath(path)
 
1064
            if not self.working_tree.is_versioned(wt_relpath):
1086
1065
                continue
1087
 
            hash = self.working_tree.get_file_sha1(file_id)
 
1066
            hash = self.working_tree.get_file_sha1(wt_relpath)
1088
1067
            if hash is None:
1089
1068
                continue
1090
 
            modified_hashes[file_id] = hash
 
1069
            modified_hashes[wt_relpath] = hash
1091
1070
        self.working_tree.set_merge_modified(modified_hashes)
1092
1071
 
1093
1072
    @staticmethod
1094
 
    def parent(entry, file_id):
 
1073
    def parent(entry):
1095
1074
        """Determine the parent for a file_id (used as a key method)"""
1096
1075
        if entry is None:
1097
1076
            return None
1098
1077
        return entry.parent_id
1099
1078
 
1100
1079
    @staticmethod
1101
 
    def name(entry, file_id):
 
1080
    def name(entry):
1102
1081
        """Determine the name for a file_id (used as a key method)"""
1103
1082
        if entry is None:
1104
1083
            return None
1105
1084
        return entry.name
1106
1085
 
1107
1086
    @staticmethod
1108
 
    def contents_sha1(tree, file_id):
 
1087
    def contents_sha1(tree, path):
1109
1088
        """Determine the sha1 of the file contents (used as a key method)."""
1110
 
        if not tree.has_id(file_id):
 
1089
        try:
 
1090
            return tree.get_file_sha1(path)
 
1091
        except errors.NoSuchFile:
1111
1092
            return None
1112
 
        return tree.get_file_sha1(file_id)
1113
1093
 
1114
1094
    @staticmethod
1115
 
    def executable(tree, file_id):
 
1095
    def executable(tree, path):
1116
1096
        """Determine the executability of a file-id (used as a key method)."""
1117
 
        if not tree.has_id(file_id):
 
1097
        try:
 
1098
            if tree.kind(path) != "file":
 
1099
                return False
 
1100
        except errors.NoSuchFile:
1118
1101
            return None
1119
 
        if tree.kind(file_id) != "file":
1120
 
            return False
1121
 
        return tree.is_executable(file_id)
 
1102
        return tree.is_executable(path)
1122
1103
 
1123
1104
    @staticmethod
1124
 
    def kind(tree, file_id):
 
1105
    def kind(tree, path):
1125
1106
        """Determine the kind of a file-id (used as a key method)."""
1126
 
        if not tree.has_id(file_id):
 
1107
        try:
 
1108
            return tree.kind(path)
 
1109
        except errors.NoSuchFile:
1127
1110
            return None
1128
 
        return tree.kind(file_id)
1129
1111
 
1130
1112
    @staticmethod
1131
1113
    def _three_way(base, other, this):
1168
1150
        base_val, lca_vals = bases
1169
1151
        # Remove 'base_val' from the lca_vals, because it is not interesting
1170
1152
        filtered_lca_vals = [lca_val for lca_val in lca_vals
1171
 
                                      if lca_val != base_val]
 
1153
                             if lca_val != base_val]
1172
1154
        if len(filtered_lca_vals) == 0:
1173
1155
            return Merge3Merger._three_way(base_val, other, this)
1174
1156
 
1193
1175
        # At this point, the lcas disagree, and the tip disagree
1194
1176
        return 'conflict'
1195
1177
 
1196
 
    def merge_names(self, file_id):
1197
 
        def get_entry(tree):
1198
 
            try:
1199
 
                return tree.root_inventory[file_id]
1200
 
            except errors.NoSuchId:
1201
 
                return None
1202
 
        this_entry = get_entry(self.this_tree)
1203
 
        other_entry = get_entry(self.other_tree)
1204
 
        base_entry = get_entry(self.base_tree)
1205
 
        entries = (base_entry, other_entry, this_entry)
1206
 
        names = []
1207
 
        parents = []
1208
 
        for entry in entries:
1209
 
            if entry is None:
1210
 
                names.append(None)
1211
 
                parents.append(None)
1212
 
            else:
1213
 
                names.append(entry.name)
1214
 
                parents.append(entry.parent_id)
1215
 
        return self._merge_names(file_id, parents, names,
1216
 
                                 resolver=self._three_way)
1217
 
 
1218
 
    def _merge_names(self, file_id, parents, names, resolver):
1219
 
        """Perform a merge on file_id names and parents"""
 
1178
    def _merge_names(self, trans_id, file_id, paths, parents, names, resolver):
 
1179
        """Perform a merge on file names and parents"""
1220
1180
        base_name, other_name, this_name = names
1221
1181
        base_parent, other_parent, this_parent = parents
 
1182
        unused_base_path, other_path, this_path = paths
1222
1183
 
1223
1184
        name_winner = resolver(*names)
1224
1185
 
1234
1195
            # Creating helpers (.OTHER or .THIS) here cause problems down the
1235
1196
            # road if a ContentConflict needs to be created so we should not do
1236
1197
            # that
1237
 
            trans_id = self.tt.trans_id_file_id(file_id)
1238
1198
            self._raw_conflicts.append(('path conflict', trans_id, file_id,
1239
1199
                                        this_parent, this_name,
1240
1200
                                        other_parent, other_name))
1241
 
        if not self.other_tree.has_id(file_id):
 
1201
        if other_path is None:
1242
1202
            # it doesn't matter whether the result was 'other' or
1243
1203
            # 'conflict'-- if it has no file id, we leave it alone.
1244
1204
            return
1257
1217
                parent_trans_id = transform.ROOT_PARENT
1258
1218
            else:
1259
1219
                parent_trans_id = self.tt.trans_id_file_id(parent_id)
1260
 
            self.tt.adjust_path(name, parent_trans_id,
1261
 
                                self.tt.trans_id_file_id(file_id))
 
1220
            self.tt.adjust_path(name, parent_trans_id, trans_id)
1262
1221
 
1263
 
    def _do_merge_contents(self, file_id):
 
1222
    def _do_merge_contents(self, paths, trans_id, file_id):
1264
1223
        """Performs a merge on file_id contents."""
1265
 
        def contents_pair(tree):
1266
 
            if not tree.has_id(file_id):
1267
 
                return (None, None)
1268
 
            kind = tree.kind(file_id)
 
1224
        def contents_pair(tree, path):
 
1225
            if path is None:
 
1226
                return (None, None)
 
1227
            try:
 
1228
                kind = tree.kind(path)
 
1229
            except errors.NoSuchFile:
 
1230
                return (None, None)
1269
1231
            if kind == "file":
1270
 
                contents = tree.get_file_sha1(file_id)
 
1232
                contents = tree.get_file_sha1(path)
1271
1233
            elif kind == "symlink":
1272
 
                contents = tree.get_symlink_target(file_id)
 
1234
                contents = tree.get_symlink_target(path)
1273
1235
            else:
1274
1236
                contents = None
1275
1237
            return kind, contents
1276
1238
 
 
1239
        base_path, other_path, this_path = paths
1277
1240
        # See SPOT run.  run, SPOT, run.
1278
1241
        # So we're not QUITE repeating ourselves; we do tricky things with
1279
1242
        # file kind...
1280
 
        base_pair = contents_pair(self.base_tree)
1281
 
        other_pair = contents_pair(self.other_tree)
 
1243
        other_pair = contents_pair(self.other_tree, other_path)
 
1244
        this_pair = contents_pair(self.this_tree, this_path)
1282
1245
        if self._lca_trees:
1283
 
            this_pair = contents_pair(self.this_tree)
1284
 
            lca_pairs = [contents_pair(tree) for tree in self._lca_trees]
 
1246
            (base_path, lca_paths) = base_path
 
1247
            base_pair = contents_pair(self.base_tree, base_path)
 
1248
            lca_pairs = [contents_pair(tree, path)
 
1249
                         for tree, path in zip(self._lca_trees, lca_paths)]
1285
1250
            winner = self._lca_multi_way((base_pair, lca_pairs), other_pair,
1286
1251
                                         this_pair, allow_overriding_lca=False)
1287
1252
        else:
 
1253
            base_pair = contents_pair(self.base_tree, base_path)
1288
1254
            if base_pair == other_pair:
1289
1255
                winner = 'this'
1290
1256
            else:
1291
1257
                # We delayed evaluating this_pair as long as we can to avoid
1292
1258
                # unnecessary sha1 calculation
1293
 
                this_pair = contents_pair(self.this_tree)
 
1259
                this_pair = contents_pair(self.this_tree, this_path)
1294
1260
                winner = self._three_way(base_pair, other_pair, this_pair)
1295
1261
        if winner == 'this':
1296
1262
            # No interesting changes introduced by OTHER
1297
1263
            return "unmodified"
1298
1264
        # We have a hypothetical conflict, but if we have files, then we
1299
1265
        # can try to merge the content
1300
 
        trans_id = self.tt.trans_id_file_id(file_id)
1301
 
        params = MergeFileHookParams(self, file_id, trans_id, this_pair[0],
 
1266
        params = MergeFileHookParams(
 
1267
            self, (base_path, other_path, this_path), trans_id, this_pair[0],
1302
1268
            other_pair[0], winner)
1303
1269
        hooks = self.active_hooks
1304
1270
        hook_status = 'not_applicable'
1318
1284
            result = None
1319
1285
            name = self.tt.final_name(trans_id)
1320
1286
            parent_id = self.tt.final_parent(trans_id)
1321
 
            duplicate = False
1322
1287
            inhibit_content_conflict = False
1323
 
            if params.this_kind is None: # file_id is not in THIS
 
1288
            if params.this_kind is None:  # file_id is not in THIS
1324
1289
                # Is the name used for a different file_id ?
1325
 
                dupe_path = self.other_tree.id2path(file_id)
1326
 
                this_id = self.this_tree.path2id(dupe_path)
1327
 
                if this_id is not None:
 
1290
                if self.this_tree.is_versioned(other_path):
1328
1291
                    # Two entries for the same path
1329
1292
                    keep_this = True
1330
1293
                    # versioning the merged file will trigger a duplicate
1331
1294
                    # conflict
1332
 
                    self.tt.version_file(file_id, trans_id)
 
1295
                    self.tt.version_file(trans_id, file_id=file_id)
1333
1296
                    transform.create_from_tree(
1334
 
                        self.tt, trans_id, self.other_tree, file_id,
1335
 
                        filter_tree_path=self._get_filter_tree_path(file_id))
 
1297
                        self.tt, trans_id, self.other_tree,
 
1298
                        other_path,
 
1299
                        filter_tree_path=self._get_filter_tree_path(other_path))
1336
1300
                    inhibit_content_conflict = True
1337
 
            elif params.other_kind is None: # file_id is not in OTHER
 
1301
            elif params.other_kind is None:  # file_id is not in OTHER
1338
1302
                # Is the name used for a different file_id ?
1339
 
                dupe_path = self.this_tree.id2path(file_id)
1340
 
                other_id = self.other_tree.path2id(dupe_path)
1341
 
                if other_id is not None:
 
1303
                if self.other_tree.is_versioned(this_path):
1342
1304
                    # Two entries for the same path again, but here, the other
1343
1305
                    # entry will also be merged.  We simply inhibit the
1344
1306
                    # 'content' conflict creation because we know OTHER will
1352
1314
                    self.tt.unversion_file(trans_id)
1353
1315
                # This is a contents conflict, because none of the available
1354
1316
                # functions could merge it.
1355
 
                file_group = self._dump_conflicts(name, parent_id, file_id,
1356
 
                                                  set_version=True)
 
1317
                file_group = self._dump_conflicts(
 
1318
                    name, (base_path, other_path, this_path), parent_id)
 
1319
                for tid in file_group:
 
1320
                    self.tt.version_file(tid, file_id=file_id)
 
1321
                    break
1357
1322
                self._raw_conflicts.append(('contents conflict', file_group))
1358
1323
        elif hook_status == 'success':
1359
1324
            self.tt.create_file(lines, trans_id)
1364
1329
            self._raw_conflicts.append(('text conflict', trans_id))
1365
1330
            name = self.tt.final_name(trans_id)
1366
1331
            parent_id = self.tt.final_parent(trans_id)
1367
 
            self._dump_conflicts(name, parent_id, file_id)
 
1332
            self._dump_conflicts(
 
1333
                name, (base_path, other_path, this_path), parent_id)
1368
1334
        elif hook_status == 'delete':
1369
1335
            self.tt.unversion_file(trans_id)
1370
1336
            result = "deleted"
1374
1340
            pass
1375
1341
        else:
1376
1342
            raise AssertionError('unknown hook_status: %r' % (hook_status,))
1377
 
        if not self.this_tree.has_id(file_id) and result == "modified":
1378
 
            self.tt.version_file(file_id, trans_id)
 
1343
        if not this_path and result == "modified":
 
1344
            self.tt.version_file(trans_id, file_id=file_id)
1379
1345
        if not keep_this:
1380
1346
            # The merge has been performed and produced a new content, so the
1381
1347
            # old contents should not be retained.
1384
1350
 
1385
1351
    def _default_other_winner_merge(self, merge_hook_params):
1386
1352
        """Replace this contents with other."""
1387
 
        file_id = merge_hook_params.file_id
1388
1353
        trans_id = merge_hook_params.trans_id
1389
 
        if self.other_tree.has_id(file_id):
 
1354
        if merge_hook_params.other_path is not None:
1390
1355
            # OTHER changed the file
1391
1356
            transform.create_from_tree(
1392
 
                self.tt, trans_id, self.other_tree, file_id,
1393
 
                filter_tree_path=self._get_filter_tree_path(file_id))
 
1357
                self.tt, trans_id, self.other_tree,
 
1358
                merge_hook_params.other_path,
 
1359
                filter_tree_path=self._get_filter_tree_path(merge_hook_params.other_path))
1394
1360
            return 'done', None
1395
 
        elif self.this_tree.has_id(file_id):
 
1361
        elif merge_hook_params.this_path is not None:
1396
1362
            # OTHER deleted the file
1397
1363
            return 'delete', None
1398
1364
        else:
1399
1365
            raise AssertionError(
1400
 
                'winner is OTHER, but file_id %r not in THIS or OTHER tree'
1401
 
                % (file_id,))
 
1366
                'winner is OTHER, but file %r not in THIS or OTHER tree'
 
1367
                % (merge_hook_params.base_path,))
1402
1368
 
1403
1369
    def merge_contents(self, merge_hook_params):
1404
1370
        """Fallback merge logic after user installed hooks."""
1405
1371
        # This function is used in merge hooks as the fallback instance.
1406
 
        # Perhaps making this function and the functions it calls be a 
 
1372
        # Perhaps making this function and the functions it calls be a
1407
1373
        # a separate class would be better.
1408
1374
        if merge_hook_params.winner == 'other':
1409
1375
            # OTHER is a straight winner, so replace this contents with other
1413
1379
            # BASE is a file, or both converted to files, so at least we
1414
1380
            # have agreement that output should be a file.
1415
1381
            try:
1416
 
                self.text_merge(merge_hook_params.file_id,
1417
 
                    merge_hook_params.trans_id)
 
1382
                self.text_merge(merge_hook_params.trans_id,
 
1383
                                merge_hook_params.paths)
1418
1384
            except errors.BinaryFile:
1419
1385
                return 'not_applicable', None
1420
1386
            return 'done', None
1421
1387
        else:
1422
1388
            return 'not_applicable', None
1423
1389
 
1424
 
    def get_lines(self, tree, file_id):
 
1390
    def get_lines(self, tree, path):
1425
1391
        """Return the lines in a file, or an empty list."""
1426
 
        if tree.has_id(file_id):
1427
 
            return tree.get_file_lines(file_id)
 
1392
        if path is None:
 
1393
            return []
 
1394
        try:
 
1395
            kind = tree.kind(path)
 
1396
        except errors.NoSuchFile:
 
1397
            return []
1428
1398
        else:
1429
 
            return []
 
1399
            if kind != 'file':
 
1400
                return []
 
1401
            return tree.get_file_lines(path)
1430
1402
 
1431
 
    def text_merge(self, file_id, trans_id):
1432
 
        """Perform a three-way text merge on a file_id"""
 
1403
    def text_merge(self, trans_id, paths):
 
1404
        """Perform a three-way text merge on a file"""
1433
1405
        # it's possible that we got here with base as a different type.
1434
1406
        # if so, we just want two-way text conflicts.
1435
 
        if self.base_tree.has_id(file_id) and \
1436
 
            self.base_tree.kind(file_id) == "file":
1437
 
            base_lines = self.get_lines(self.base_tree, file_id)
1438
 
        else:
1439
 
            base_lines = []
1440
 
        other_lines = self.get_lines(self.other_tree, file_id)
1441
 
        this_lines = self.get_lines(self.this_tree, file_id)
 
1407
        base_path, other_path, this_path = paths
 
1408
        base_lines = self.get_lines(self.base_tree, base_path)
 
1409
        other_lines = self.get_lines(self.other_tree, other_path)
 
1410
        this_lines = self.get_lines(self.this_tree, this_path)
1442
1411
        m3 = merge3.Merge3(base_lines, this_lines, other_lines,
1443
1412
                           is_cherrypick=self.cherrypick)
1444
 
        start_marker = "!START OF MERGE CONFLICT!" + "I HOPE THIS IS UNIQUE"
 
1413
        start_marker = b"!START OF MERGE CONFLICT!" + b"I HOPE THIS IS UNIQUE"
1445
1414
        if self.show_base is True:
1446
 
            base_marker = '|' * 7
 
1415
            base_marker = b'|' * 7
1447
1416
        else:
1448
1417
            base_marker = None
1449
1418
 
1450
1419
        def iter_merge3(retval):
1451
1420
            retval["text_conflicts"] = False
1452
 
            for line in m3.merge_lines(name_a = "TREE",
1453
 
                                       name_b = "MERGE-SOURCE",
1454
 
                                       name_base = "BASE-REVISION",
 
1421
            for line in m3.merge_lines(name_a=b"TREE",
 
1422
                                       name_b=b"MERGE-SOURCE",
 
1423
                                       name_base=b"BASE-REVISION",
1455
1424
                                       start_marker=start_marker,
1456
1425
                                       base_marker=base_marker,
1457
1426
                                       reprocess=self.reprocess):
1458
1427
                if line.startswith(start_marker):
1459
1428
                    retval["text_conflicts"] = True
1460
 
                    yield line.replace(start_marker, '<' * 7)
 
1429
                    yield line.replace(start_marker, b'<' * 7)
1461
1430
                else:
1462
1431
                    yield line
1463
1432
        retval = {}
1467
1436
            self._raw_conflicts.append(('text conflict', trans_id))
1468
1437
            name = self.tt.final_name(trans_id)
1469
1438
            parent_id = self.tt.final_parent(trans_id)
1470
 
            file_group = self._dump_conflicts(name, parent_id, file_id,
1471
 
                                              this_lines, base_lines,
1472
 
                                              other_lines)
 
1439
            file_group = self._dump_conflicts(
 
1440
                name, paths, parent_id,
 
1441
                lines=(base_lines, other_lines, this_lines))
1473
1442
            file_group.append(trans_id)
1474
1443
 
1475
 
 
1476
 
    def _get_filter_tree_path(self, file_id):
 
1444
    def _get_filter_tree_path(self, path):
1477
1445
        if self.this_tree.supports_content_filtering():
1478
1446
            # We get the path from the working tree if it exists.
1479
1447
            # That fails though when OTHER is adding a file, so
1480
1448
            # we fall back to the other tree to find the path if
1481
1449
            # it doesn't exist locally.
1482
 
            try:
1483
 
                return self.this_tree.id2path(file_id)
1484
 
            except errors.NoSuchId:
1485
 
                return self.other_tree.id2path(file_id)
1486
 
        # Skip the id2path lookup for older formats
 
1450
            filter_path = _mod_tree.find_previous_path(
 
1451
                self.other_tree, self.working_tree, path)
 
1452
            if filter_path is None:
 
1453
                filter_path = path
 
1454
            return filter_path
 
1455
        # Skip the lookup for older formats
1487
1456
        return None
1488
1457
 
1489
 
    def _dump_conflicts(self, name, parent_id, file_id, this_lines=None,
1490
 
                        base_lines=None, other_lines=None, set_version=False,
 
1458
    def _dump_conflicts(self, name, paths, parent_id, lines=None,
1491
1459
                        no_base=False):
1492
1460
        """Emit conflict files.
1493
1461
        If this_lines, base_lines, or other_lines are omitted, they will be
1494
1462
        determined automatically.  If set_version is true, the .OTHER, .THIS
1495
1463
        or .BASE (in that order) will be created as versioned files.
1496
1464
        """
1497
 
        data = [('OTHER', self.other_tree, other_lines),
1498
 
                ('THIS', self.this_tree, this_lines)]
 
1465
        base_path, other_path, this_path = paths
 
1466
        if lines:
 
1467
            base_lines, other_lines, this_lines = lines
 
1468
        else:
 
1469
            base_lines = other_lines = this_lines = None
 
1470
        data = [('OTHER', self.other_tree, other_path, other_lines),
 
1471
                ('THIS', self.this_tree, this_path, this_lines)]
1499
1472
        if not no_base:
1500
 
            data.append(('BASE', self.base_tree, base_lines))
 
1473
            data.append(('BASE', self.base_tree, base_path, base_lines))
1501
1474
 
1502
1475
        # We need to use the actual path in the working tree of the file here,
1503
 
        # ignoring the conflict suffixes
1504
 
        wt = self.this_tree
1505
 
        if wt.supports_content_filtering():
1506
 
            try:
1507
 
                filter_tree_path = wt.id2path(file_id)
1508
 
            except errors.NoSuchId:
1509
 
                # file has been deleted
1510
 
                filter_tree_path = None
 
1476
        if self.this_tree.supports_content_filtering():
 
1477
            filter_tree_path = this_path
1511
1478
        else:
1512
 
            # Skip the id2path lookup for older formats
1513
1479
            filter_tree_path = None
1514
1480
 
1515
 
        versioned = False
1516
1481
        file_group = []
1517
 
        for suffix, tree, lines in data:
1518
 
            if tree.has_id(file_id):
1519
 
                trans_id = self._conflict_file(name, parent_id, tree, file_id,
1520
 
                                               suffix, lines, filter_tree_path)
 
1482
        for suffix, tree, path, lines in data:
 
1483
            if path is not None:
 
1484
                trans_id = self._conflict_file(
 
1485
                    name, parent_id, path, tree, suffix, lines,
 
1486
                    filter_tree_path)
1521
1487
                file_group.append(trans_id)
1522
 
                if set_version and not versioned:
1523
 
                    self.tt.version_file(file_id, trans_id)
1524
 
                    versioned = True
1525
1488
        return file_group
1526
1489
 
1527
 
    def _conflict_file(self, name, parent_id, tree, file_id, suffix,
 
1490
    def _conflict_file(self, name, parent_id, path, tree, suffix,
1528
1491
                       lines=None, filter_tree_path=None):
1529
1492
        """Emit a single conflict file."""
1530
1493
        name = name + '.' + suffix
1531
1494
        trans_id = self.tt.create_path(name, parent_id)
1532
 
        transform.create_from_tree(self.tt, trans_id, tree, file_id, lines,
1533
 
            filter_tree_path)
 
1495
        transform.create_from_tree(
 
1496
            self.tt, trans_id, tree, path,
 
1497
            chunks=lines,
 
1498
            filter_tree_path=filter_tree_path)
1534
1499
        return trans_id
1535
1500
 
1536
 
    def merge_executable(self, file_id, file_status):
1537
 
        """Perform a merge on the execute bit."""
1538
 
        executable = [self.executable(t, file_id) for t in (self.base_tree,
1539
 
                      self.other_tree, self.this_tree)]
1540
 
        self._merge_executable(file_id, executable, file_status,
1541
 
                               resolver=self._three_way)
1542
 
 
1543
 
    def _merge_executable(self, file_id, executable, file_status,
 
1501
    def _merge_executable(self, paths, trans_id, executable, file_status,
1544
1502
                          resolver):
1545
1503
        """Perform a merge on the execute bit."""
1546
1504
        base_executable, other_executable, this_executable = executable
 
1505
        base_path, other_path, this_path = paths
1547
1506
        if file_status == "deleted":
1548
1507
            return
1549
1508
        winner = resolver(*executable)
1550
1509
        if winner == "conflict":
1551
 
        # There must be a None in here, if we have a conflict, but we
1552
 
        # need executability since file status was not deleted.
1553
 
            if self.executable(self.other_tree, file_id) is None:
 
1510
            # There must be a None in here, if we have a conflict, but we
 
1511
            # need executability since file status was not deleted.
 
1512
            if other_path is None:
1554
1513
                winner = "this"
1555
1514
            else:
1556
1515
                winner = "other"
1557
1516
        if winner == 'this' and file_status != "modified":
1558
1517
            return
1559
 
        trans_id = self.tt.trans_id_file_id(file_id)
1560
1518
        if self.tt.final_kind(trans_id) != "file":
1561
1519
            return
1562
1520
        if winner == "this":
1563
1521
            executability = this_executable
1564
1522
        else:
1565
 
            if self.other_tree.has_id(file_id):
 
1523
            if other_path is not None:
1566
1524
                executability = other_executable
1567
 
            elif self.this_tree.has_id(file_id):
 
1525
            elif this_path is not None:
1568
1526
                executability = this_executable
1569
 
            elif self.base_tree_has_id(file_id):
 
1527
            elif base_path is not None:
1570
1528
                executability = base_executable
1571
1529
        if executability is not None:
1572
 
            trans_id = self.tt.trans_id_file_id(file_id)
1573
1530
            self.tt.set_executability(executability, trans_id)
1574
1531
 
1575
1532
    def cook_conflicts(self, fs_conflicts):
1576
1533
        """Convert all conflicts into a form that doesn't depend on trans_id"""
1577
 
        content_conflict_file_ids = set()
1578
 
        cooked_conflicts = transform.cook_conflicts(fs_conflicts, self.tt)
1579
 
        fp = transform.FinalPaths(self.tt)
1580
 
        for conflict in self._raw_conflicts:
1581
 
            conflict_type = conflict[0]
1582
 
            if conflict_type == 'path conflict':
1583
 
                (trans_id, file_id,
1584
 
                this_parent, this_name,
1585
 
                other_parent, other_name) = conflict[1:]
1586
 
                if this_parent is None or this_name is None:
1587
 
                    this_path = '<deleted>'
1588
 
                else:
1589
 
                    parent_path =  fp.get_path(
1590
 
                        self.tt.trans_id_file_id(this_parent))
1591
 
                    this_path = osutils.pathjoin(parent_path, this_name)
1592
 
                if other_parent is None or other_name is None:
1593
 
                    other_path = '<deleted>'
1594
 
                else:
1595
 
                    if other_parent == self.other_tree.get_root_id():
1596
 
                        # The tree transform doesn't know about the other root,
1597
 
                        # so we special case here to avoid a NoFinalPath
1598
 
                        # exception
1599
 
                        parent_path = ''
1600
 
                    else:
1601
 
                        parent_path =  fp.get_path(
1602
 
                            self.tt.trans_id_file_id(other_parent))
1603
 
                    other_path = osutils.pathjoin(parent_path, other_name)
1604
 
                c = _mod_conflicts.Conflict.factory(
1605
 
                    'path conflict', path=this_path,
1606
 
                    conflict_path=other_path,
1607
 
                    file_id=file_id)
1608
 
            elif conflict_type == 'contents conflict':
1609
 
                for trans_id in conflict[1]:
1610
 
                    file_id = self.tt.final_file_id(trans_id)
1611
 
                    if file_id is not None:
1612
 
                        # Ok we found the relevant file-id
1613
 
                        break
1614
 
                path = fp.get_path(trans_id)
1615
 
                for suffix in ('.BASE', '.THIS', '.OTHER'):
1616
 
                    if path.endswith(suffix):
1617
 
                        # Here is the raw path
1618
 
                        path = path[:-len(suffix)]
1619
 
                        break
1620
 
                c = _mod_conflicts.Conflict.factory(conflict_type,
1621
 
                                                    path=path, file_id=file_id)
1622
 
                content_conflict_file_ids.add(file_id)
1623
 
            elif conflict_type == 'text conflict':
1624
 
                trans_id = conflict[1]
1625
 
                path = fp.get_path(trans_id)
1626
 
                file_id = self.tt.final_file_id(trans_id)
1627
 
                c = _mod_conflicts.Conflict.factory(conflict_type,
1628
 
                                                    path=path, file_id=file_id)
1629
 
            else:
1630
 
                raise AssertionError('bad conflict type: %r' % (conflict,))
1631
 
            cooked_conflicts.append(c)
1632
 
 
1633
 
        self.cooked_conflicts = []
1634
 
        # We want to get rid of path conflicts when a corresponding contents
1635
 
        # conflict exists. This can occur when one branch deletes a file while
1636
 
        # the other renames *and* modifies it. In this case, the content
1637
 
        # conflict is enough.
1638
 
        for c in cooked_conflicts:
1639
 
            if (c.typestring == 'path conflict'
1640
 
                and c.file_id in content_conflict_file_ids):
1641
 
                continue
1642
 
            self.cooked_conflicts.append(c)
1643
 
        self.cooked_conflicts.sort(key=_mod_conflicts.Conflict.sort_key)
 
1534
        self.cooked_conflicts = list(self.tt.cook_conflicts(
 
1535
            list(fs_conflicts) + self._raw_conflicts))
1644
1536
 
1645
1537
 
1646
1538
class WeaveMerger(Merge3Merger):
1649
1541
    supports_show_base = False
1650
1542
    supports_reverse_cherrypick = False
1651
1543
    history_based = True
 
1544
    requires_file_merge_plan = True
1652
1545
 
1653
 
    def _generate_merge_plan(self, file_id, base):
1654
 
        return self.this_tree.plan_file_merge(file_id, self.other_tree,
 
1546
    def _generate_merge_plan(self, this_path, base):
 
1547
        return self.this_tree.plan_file_merge(this_path, self.other_tree,
1655
1548
                                              base=base)
1656
1549
 
1657
 
    def _merged_lines(self, file_id):
 
1550
    def _merged_lines(self, this_path):
1658
1551
        """Generate the merged lines.
1659
1552
        There is no distinction between lines that are meant to contain <<<<<<<
1660
1553
        and conflicts.
1663
1556
            base = self.base_tree
1664
1557
        else:
1665
1558
            base = None
1666
 
        plan = self._generate_merge_plan(file_id, base)
 
1559
        plan = self._generate_merge_plan(this_path, base)
1667
1560
        if 'merge' in debug.debug_flags:
1668
1561
            plan = list(plan)
1669
1562
            trans_id = self.tt.trans_id_file_id(file_id)
1670
1563
            name = self.tt.final_name(trans_id) + '.plan'
1671
 
            contents = ('%11s|%s' % l for l in plan)
 
1564
            contents = (b'%11s|%s' % l for l in plan)
1672
1565
            self.tt.new_file(name, self.tt.final_parent(trans_id), contents)
1673
 
        textmerge = versionedfile.PlanWeaveMerge(plan, '<<<<<<< TREE\n',
1674
 
                                                 '>>>>>>> MERGE-SOURCE\n')
 
1566
        textmerge = versionedfile.PlanWeaveMerge(plan, b'<<<<<<< TREE\n',
 
1567
                                                 b'>>>>>>> MERGE-SOURCE\n')
1675
1568
        lines, conflicts = textmerge.merge_lines(self.reprocess)
1676
1569
        if conflicts:
1677
1570
            base_lines = textmerge.base_from_plan()
1679
1572
            base_lines = None
1680
1573
        return lines, base_lines
1681
1574
 
1682
 
    def text_merge(self, file_id, trans_id):
 
1575
    def text_merge(self, trans_id, paths):
1683
1576
        """Perform a (weave) text merge for a given file and file-id.
1684
1577
        If conflicts are encountered, .THIS and .OTHER files will be emitted,
1685
1578
        and a conflict will be noted.
1686
1579
        """
1687
 
        lines, base_lines = self._merged_lines(file_id)
 
1580
        base_path, other_path, this_path = paths
 
1581
        lines, base_lines = self._merged_lines(this_path)
1688
1582
        lines = list(lines)
1689
1583
        # Note we're checking whether the OUTPUT is binary in this case,
1690
1584
        # because we don't want to get into weave merge guts.
1695
1589
            self._raw_conflicts.append(('text conflict', trans_id))
1696
1590
            name = self.tt.final_name(trans_id)
1697
1591
            parent_id = self.tt.final_parent(trans_id)
1698
 
            file_group = self._dump_conflicts(name, parent_id, file_id,
1699
 
                                              no_base=False,
1700
 
                                              base_lines=base_lines)
 
1592
            file_group = self._dump_conflicts(name, paths, parent_id,
 
1593
                                              (base_lines, None, None),
 
1594
                                              no_base=False)
1701
1595
            file_group.append(trans_id)
1702
1596
 
1703
1597
 
1704
1598
class LCAMerger(WeaveMerger):
1705
1599
 
1706
 
    def _generate_merge_plan(self, file_id, base):
1707
 
        return self.this_tree.plan_file_lca_merge(file_id, self.other_tree,
 
1600
    requires_file_merge_plan = True
 
1601
 
 
1602
    def _generate_merge_plan(self, this_path, base):
 
1603
        return self.this_tree.plan_file_lca_merge(this_path, self.other_tree,
1708
1604
                                                  base=base)
1709
1605
 
 
1606
 
1710
1607
class Diff3Merger(Merge3Merger):
1711
1608
    """Three-way merger using external diff3 for text merging"""
1712
1609
 
1713
 
    def dump_file(self, temp_dir, name, tree, file_id):
 
1610
    requires_file_merge_plan = False
 
1611
 
 
1612
    def dump_file(self, temp_dir, name, tree, path):
1714
1613
        out_path = osutils.pathjoin(temp_dir, name)
1715
 
        out_file = open(out_path, "wb")
1716
 
        try:
1717
 
            in_file = tree.get_file(file_id)
 
1614
        with open(out_path, "wb") as out_file:
 
1615
            in_file = tree.get_file(path)
1718
1616
            for line in in_file:
1719
1617
                out_file.write(line)
1720
 
        finally:
1721
 
            out_file.close()
1722
1618
        return out_path
1723
1619
 
1724
 
    def text_merge(self, file_id, trans_id):
 
1620
    def text_merge(self, trans_id, paths):
1725
1621
        """Perform a diff3 merge using a specified file-id and trans-id.
1726
1622
        If conflicts are encountered, .BASE, .THIS. and .OTHER conflict files
1727
1623
        will be dumped, and a will be conflict noted.
1728
1624
        """
1729
 
        import bzrlib.patch
 
1625
        import breezy.patch
 
1626
        base_path, other_path, this_path = paths
1730
1627
        temp_dir = osutils.mkdtemp(prefix="bzr-")
1731
1628
        try:
1732
1629
            new_file = osutils.pathjoin(temp_dir, "new")
1733
 
            this = self.dump_file(temp_dir, "this", self.this_tree, file_id)
1734
 
            base = self.dump_file(temp_dir, "base", self.base_tree, file_id)
1735
 
            other = self.dump_file(temp_dir, "other", self.other_tree, file_id)
1736
 
            status = bzrlib.patch.diff3(new_file, this, base, other)
 
1630
            this = self.dump_file(
 
1631
                temp_dir, "this", self.this_tree, this_path)
 
1632
            base = self.dump_file(
 
1633
                temp_dir, "base", self.base_tree, base_path)
 
1634
            other = self.dump_file(
 
1635
                temp_dir, "other", self.other_tree, other_path)
 
1636
            status = breezy.patch.diff3(new_file, this, base, other)
1737
1637
            if status not in (0, 1):
1738
1638
                raise errors.BzrError("Unhandled diff3 exit code")
1739
 
            f = open(new_file, 'rb')
1740
 
            try:
 
1639
            with open(new_file, 'rb') as f:
1741
1640
                self.tt.create_file(f, trans_id)
1742
 
            finally:
1743
 
                f.close()
1744
1641
            if status == 1:
1745
1642
                name = self.tt.final_name(trans_id)
1746
1643
                parent_id = self.tt.final_parent(trans_id)
1747
 
                self._dump_conflicts(name, parent_id, file_id)
 
1644
                self._dump_conflicts(name, paths, parent_id)
1748
1645
                self._raw_conflicts.append(('text conflict', trans_id))
1749
1646
        finally:
1750
1647
            osutils.rmtree(temp_dir)
1766
1663
    """
1767
1664
 
1768
1665
    def __init__(self, this_tree, other_branch, other_tree, target_subdir,
1769
 
            source_subpath, other_rev_id=None):
 
1666
                 source_subpath, other_rev_id=None):
1770
1667
        """Create a new MergeIntoMerger object.
1771
1668
 
1772
1669
        source_subpath in other_tree will be effectively copied to
1783
1680
        # It is assumed that we are merging a tree that is not in our current
1784
1681
        # ancestry, which means we are using the "EmptyTree" as our basis.
1785
1682
        null_ancestor_tree = this_tree.branch.repository.revision_tree(
1786
 
                                _mod_revision.NULL_REVISION)
 
1683
            _mod_revision.NULL_REVISION)
1787
1684
        super(MergeIntoMerger, self).__init__(
1788
1685
            this_branch=this_tree.branch,
1789
1686
            this_tree=this_tree,
1801
1698
        self.merge_type = Merge3Merger
1802
1699
        self.show_base = False
1803
1700
        self.reprocess = False
1804
 
        self.interesting_ids = None
 
1701
        self.interesting_files = None
1805
1702
        self.merge_type = _MergeTypeParameterizer(MergeIntoMergeType,
1806
 
              target_subdir=self._target_subdir,
1807
 
              source_subpath=self._source_subpath)
 
1703
                                                  target_subdir=self._target_subdir,
 
1704
                                                  source_subpath=self._source_subpath)
1808
1705
        if self._source_subpath != '':
1809
1706
            # If this isn't a partial merge make sure the revisions will be
1810
1707
            # present.
1811
1708
            self._maybe_fetch(self.other_branch, self.this_branch,
1812
 
                self.other_basis)
 
1709
                              self.other_basis)
1813
1710
 
1814
1711
    def set_pending(self):
1815
1712
        if self._source_subpath != '':
1819
1716
 
1820
1717
class _MergeTypeParameterizer(object):
1821
1718
    """Wrap a merge-type class to provide extra parameters.
1822
 
    
 
1719
 
1823
1720
    This is hack used by MergeIntoMerger to pass some extra parameters to its
1824
1721
    merge_type.  Merger.do_merge() sets up its own set of parameters to pass to
1825
1722
    the 'merge_type' member.  It is difficult override do_merge without
1860
1757
        super(MergeIntoMergeType, self).__init__(*args, **kwargs)
1861
1758
 
1862
1759
    def _compute_transform(self):
1863
 
        child_pb = ui.ui_factory.nested_progress_bar()
1864
 
        try:
 
1760
        with ui.ui_factory.nested_progress_bar() as child_pb:
1865
1761
            entries = self._entries_to_incorporate()
1866
1762
            entries = list(entries)
1867
 
            for num, (entry, parent_id) in enumerate(entries):
1868
 
                child_pb.update(gettext('Preparing file merge'), num, len(entries))
 
1763
            for num, (entry, parent_id, relpath) in enumerate(entries):
 
1764
                child_pb.update(gettext('Preparing file merge'),
 
1765
                                num, len(entries))
1869
1766
                parent_trans_id = self.tt.trans_id_file_id(parent_id)
1870
 
                trans_id = transform.new_by_entry(self.tt, entry,
1871
 
                    parent_trans_id, self.other_tree)
1872
 
        finally:
1873
 
            child_pb.finished()
 
1767
                path = osutils.pathjoin(self._source_subpath, relpath)
 
1768
                trans_id = transform.new_by_entry(path, self.tt, entry,
 
1769
                                                  parent_trans_id, self.other_tree)
1874
1770
        self._finish_computing_transform()
1875
1771
 
1876
1772
    def _entries_to_incorporate(self):
1877
1773
        """Yields pairs of (inventory_entry, new_parent)."""
1878
 
        other_inv = self.other_tree.root_inventory
1879
 
        subdir_id = other_inv.path2id(self._source_subpath)
 
1774
        subdir_id = self.other_tree.path2id(self._source_subpath)
1880
1775
        if subdir_id is None:
1881
1776
            # XXX: The error would be clearer if it gave the URL of the source
1882
1777
            # branch, but we don't have a reference to that here.
1883
1778
            raise PathNotInTree(self._source_subpath, "Source tree")
1884
 
        subdir = other_inv[subdir_id]
 
1779
        subdir = next(self.other_tree.iter_entries_by_dir(
 
1780
            specific_files=[self._source_subpath]))[1]
1885
1781
        parent_in_target = osutils.dirname(self._target_subdir)
1886
1782
        target_id = self.this_tree.path2id(parent_in_target)
1887
1783
        if target_id is None:
1889
1785
        name_in_target = osutils.basename(self._target_subdir)
1890
1786
        merge_into_root = subdir.copy()
1891
1787
        merge_into_root.name = name_in_target
1892
 
        if self.this_tree.has_id(merge_into_root.file_id):
 
1788
        try:
 
1789
            self.this_tree.id2path(merge_into_root.file_id)
 
1790
        except errors.NoSuchId:
 
1791
            pass
 
1792
        else:
1893
1793
            # Give the root a new file-id.
1894
1794
            # This can happen fairly easily if the directory we are
1895
1795
            # incorporating is the root, and both trees have 'TREE_ROOT' as
1899
1799
            # an edge case, so we don't do anything special for those.  We let
1900
1800
            # them cause conflicts.
1901
1801
            merge_into_root.file_id = generate_ids.gen_file_id(name_in_target)
1902
 
        yield (merge_into_root, target_id)
 
1802
        yield (merge_into_root, target_id, '')
1903
1803
        if subdir.kind != 'directory':
1904
1804
            # No children, so we are done.
1905
1805
            return
1906
 
        for ignored_path, entry in other_inv.iter_entries_by_dir(subdir_id):
 
1806
        for path, entry in self.other_tree.root_inventory.iter_entries_by_dir(subdir_id):
1907
1807
            parent_id = entry.parent_id
1908
1808
            if parent_id == subdir.file_id:
1909
1809
                # The root's parent ID has changed, so make sure children of
1910
1810
                # the root refer to the new ID.
1911
1811
                parent_id = merge_into_root.file_id
1912
 
            yield (entry, parent_id)
 
1812
            yield (entry, parent_id, path)
1913
1813
 
1914
1814
 
1915
1815
def merge_inner(this_branch, other_tree, base_tree, ignore_zero=False,
1916
1816
                backup_files=False,
1917
1817
                merge_type=Merge3Merger,
1918
 
                interesting_ids=None,
1919
1818
                show_base=False,
1920
1819
                reprocess=False,
1921
1820
                other_rev_id=None,
1922
1821
                interesting_files=None,
1923
1822
                this_tree=None,
1924
 
                pb=None,
1925
1823
                change_reporter=None):
1926
1824
    """Primary interface for merging.
1927
1825
 
1931
1829
                    branch.get_revision_tree(base_revision))
1932
1830
    """
1933
1831
    if this_tree is None:
1934
 
        raise errors.BzrError("bzrlib.merge.merge_inner requires a this_tree "
 
1832
        raise errors.BzrError("breezy.merge.merge_inner requires a this_tree "
1935
1833
                              "parameter")
1936
1834
    merger = Merger(this_branch, other_tree, base_tree, this_tree=this_tree,
1937
 
                    pb=pb, change_reporter=change_reporter)
 
1835
                    change_reporter=change_reporter)
1938
1836
    merger.backup_files = backup_files
1939
1837
    merger.merge_type = merge_type
1940
 
    merger.interesting_ids = interesting_ids
1941
1838
    merger.ignore_zero = ignore_zero
1942
 
    if interesting_files:
1943
 
        if interesting_ids:
1944
 
            raise ValueError('Only supply interesting_ids'
1945
 
                             ' or interesting_files')
1946
 
        merger.interesting_files = interesting_files
 
1839
    merger.interesting_files = interesting_files
1947
1840
    merger.show_base = show_base
1948
1841
    merger.reprocess = reprocess
1949
1842
    merger.other_rev_id = other_rev_id
1968
1861
 
1969
1862
 
1970
1863
def get_merge_type_registry():
1971
 
    """Merge type registry was previously in bzrlib.option
 
1864
    """Merge type registry was previously in breezy.option
1972
1865
 
1973
1866
    This method provides a backwards compatible way to retrieve it.
1974
1867
    """
2044
1937
        for record in self.vf.get_record_stream(keys, 'unordered', True):
2045
1938
            if record.storage_kind == 'absent':
2046
1939
                raise errors.RevisionNotPresent(record.key, self.vf)
2047
 
            result[record.key[-1]] = osutils.chunks_to_lines(
2048
 
                record.get_bytes_as('chunked'))
 
1940
            result[record.key[-1]] = record.get_bytes_as('lines')
2049
1941
        return result
2050
1942
 
2051
1943
    def plan_merge(self):
2084
1976
                else:
2085
1977
                    yield 'killed-a', self.lines_b[b_index]
2086
1978
            # handle common lines
2087
 
            for a_index in range(i, i+n):
 
1979
            for a_index in range(i, i + n):
2088
1980
                yield 'unchanged', self.lines_a[a_index]
2089
 
            last_i = i+n
2090
 
            last_j = j+n
 
1981
            last_i = i + n
 
1982
            last_j = j + n
2091
1983
 
2092
1984
    def _get_matching_blocks(self, left_revision, right_revision):
2093
1985
        """Return a description of which sections of two revisions match.
2149
2041
        for i, j, n in matcher.get_matching_blocks():
2150
2042
            for jj in range(last_j, j):
2151
2043
                yield new_plan[jj]
2152
 
            for jj in range(j, j+n):
 
2044
            for jj in range(j, j + n):
2153
2045
                plan_line = new_plan[jj]
2154
2046
                if plan_line[0] == 'new-b':
2155
2047
                    pass
2199
2091
        while True:
2200
2092
            next_lcas = self.graph.find_lca(*cur_ancestors)
2201
2093
            # Map a plain NULL_REVISION to a simple no-ancestors
2202
 
            if next_lcas == set([_mod_revision.NULL_REVISION]):
 
2094
            if next_lcas == {_mod_revision.NULL_REVISION}:
2203
2095
                next_lcas = ()
2204
2096
            # Order the lca's based on when they were merged into the tip
2205
2097
            # While the actual merge portion of weave merge uses a set() of
2293
2185
        filtered_parent_map = {}
2294
2186
        child_map = {}
2295
2187
        tails = []
2296
 
        for key, parent_keys in parent_map.iteritems():
 
2188
        for key, parent_keys in parent_map.items():
2297
2189
            culled_parent_keys = [p for p in parent_keys if p in parent_map]
2298
2190
            if not culled_parent_keys:
2299
2191
                tails.append(key)
2349
2241
        return all_texts
2350
2242
 
2351
2243
    def _build_weave(self):
2352
 
        from bzrlib import weave
 
2244
        from .bzr import weave
2353
2245
        self._weave = weave.Weave(weave_name='in_memory_weave',
2354
2246
                                  allow_reserved=True)
2355
2247
        parent_map = self._find_recursive_lcas()
2386
2278
        are combined, they are written out in the format described in
2387
2279
        VersionedFile.plan_merge
2388
2280
        """
2389
 
        if self._head_key is not None: # There was a single head
 
2281
        if self._head_key is not None:  # There was a single head
2390
2282
            if self._head_key == self.a_key:
2391
2283
                plan = 'new-a'
2392
2284
            else: