/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: Jelmer Vernooij
  • Date: 2017-07-23 22:06:41 UTC
  • mfrom: (6738 trunk)
  • mto: This revision was merged to the branch mainline in revision 6739.
  • Revision ID: jelmer@jelmer.uk-20170723220641-69eczax9bmv8d6kk
Merge trunk, address review comments.

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