/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: 2018-06-14 17:59:16 UTC
  • mto: This revision was merged to the branch mainline in revision 7065.
  • Revision ID: jelmer@jelmer.uk-20180614175916-a2e2xh5k533guq1x
Move breezy.plugins.git to breezy.git.

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
 
    generate_ids,
40
43
    versionedfile,
41
44
    )
42
45
from breezy.i18n import gettext
46
49
    errors,
47
50
    hooks,
48
51
    registry,
49
 
    transform,
 
52
    )
 
53
from .sixish import (
 
54
    viewitems,
50
55
    )
51
56
# TODO: Report back as changes are merged in
52
57
 
53
58
 
54
59
def transform_tree(from_tree, to_tree, interesting_files=None):
55
 
    with from_tree.lock_tree_write():
56
 
        merge_inner(from_tree.branch, to_tree, from_tree,
57
 
                    ignore_zero=True, this_tree=from_tree,
58
 
                    interesting_files=interesting_files)
 
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, this_tree=from_tree,
 
65
        interesting_files=interesting_files)
59
66
 
60
67
 
61
68
class MergeHooks(hooks.Hooks):
63
70
    def __init__(self):
64
71
        hooks.Hooks.__init__(self, "breezy.merge", "Merger.hooks")
65
72
        self.add_hook('merge_file_content',
66
 
                      "Called with a breezy.merge.Merger object to create a per file "
67
 
                      "merge object when starting a merge. "
68
 
                      "Should return either None or a subclass of "
69
 
                      "``breezy.merge.AbstractPerFileMerger``. "
70
 
                      "Such objects will then be called per file "
71
 
                      "that needs to be merged (including when one "
72
 
                      "side has deleted the file and the other has changed it). "
73
 
                      "See the AbstractPerFileMerger API docs for details on how it is "
74
 
                      "used by merge.",
75
 
                      (2, 1))
 
73
            "Called with a breezy.merge.Merger object to create a per file "
 
74
            "merge object when starting a merge. "
 
75
            "Should return either None or a subclass of "
 
76
            "``breezy.merge.AbstractPerFileMerger``. "
 
77
            "Such objects will then be called per file "
 
78
            "that needs to be merged (including when one "
 
79
            "side has deleted the file and the other has changed it). "
 
80
            "See the AbstractPerFileMerger API docs for details on how it is "
 
81
            "used by merge.",
 
82
            (2, 1))
76
83
        self.add_hook('pre_merge',
77
 
                      'Called before a merge. '
78
 
                      'Receives a Merger object as the single argument.',
79
 
                      (2, 5))
 
84
            'Called before a merge. '
 
85
            'Receives a Merger object as the single argument.',
 
86
            (2, 5))
80
87
        self.add_hook('post_merge',
81
 
                      'Called after a merge. '
82
 
                      'Receives a Merger object as the single argument. '
83
 
                      'The return value is ignored.',
84
 
                      (2, 5))
 
88
            'Called after a merge. '
 
89
            'Receives a Merger object as the single argument. '
 
90
            'The return value is ignored.',
 
91
            (2, 5))
85
92
 
86
93
 
87
94
class AbstractPerFileMerger(object):
133
140
            # THIS and OTHER aren't both files.
134
141
            not params.is_file_merge() or
135
142
            # The filename doesn't match
136
 
                not self.file_matches(params)):
 
143
            not self.file_matches(params)):
137
144
            return 'not_applicable', None
138
145
        return self.merge_matching(params)
139
146
 
219
226
 
220
227
    There are some fields hooks can access:
221
228
 
 
229
    :ivar file_id: the file ID of the file being merged
222
230
    :ivar base_path: Path in base tree
223
231
    :ivar other_path: Path in other tree
224
232
    :ivar this_path: Path in this tree
225
233
    :ivar trans_id: the transform ID for the merge of this file
226
 
    :ivar this_kind: kind of file in 'this' tree
227
 
    :ivar other_kind: kind of file in 'other' tree
 
234
    :ivar this_kind: kind of file_id in 'this' tree
 
235
    :ivar other_kind: kind of file_id in 'other' tree
228
236
    :ivar winner: one of 'this', 'other', 'conflict'
229
237
    """
230
238
 
231
 
    def __init__(self, merger, paths, trans_id, this_kind, other_kind,
232
 
                 winner):
 
239
    def __init__(self, merger, file_id, paths, trans_id, this_kind, other_kind,
 
240
            winner):
233
241
        self._merger = merger
 
242
        self.file_id = file_id
234
243
        self.paths = paths
235
244
        self.base_path, self.other_path, self.this_path = paths
236
245
        self.trans_id = trans_id
245
254
    @decorators.cachedproperty
246
255
    def base_lines(self):
247
256
        """The lines of the 'base' version of the file."""
248
 
        return self._merger.get_lines(self._merger.base_tree, self.base_path)
 
257
        return self._merger.get_lines(self._merger.base_tree, self.base_path, self.file_id)
249
258
 
250
259
    @decorators.cachedproperty
251
260
    def this_lines(self):
252
261
        """The lines of the 'this' version of the file."""
253
 
        return self._merger.get_lines(self._merger.this_tree, self.this_path)
 
262
        return self._merger.get_lines(self._merger.this_tree, self.this_path, self.file_id)
254
263
 
255
264
    @decorators.cachedproperty
256
265
    def other_lines(self):
257
266
        """The lines of the 'other' version of the file."""
258
 
        return self._merger.get_lines(self._merger.other_tree, self.other_path)
 
267
        return self._merger.get_lines(self._merger.other_tree, self.other_path, self.file_id)
259
268
 
260
269
 
261
270
class Merger(object):
364
373
        if base_revision_id is not None:
365
374
            if (base_revision_id != _mod_revision.NULL_REVISION and
366
375
                revision_graph.is_ancestor(
367
 
                    base_revision_id, tree.branch.last_revision())):
 
376
                base_revision_id, tree.branch.last_revision())):
368
377
                base_revision_id = None
369
378
            else:
370
379
                trace.warning('Performing cherrypick')
371
380
        merger = klass.from_revision_ids(tree, other_revision_id,
372
 
                                         base_revision_id, revision_graph=revision_graph)
 
381
                                         base_revision_id, revision_graph=
 
382
                                         revision_graph)
373
383
        return merger, verified
374
384
 
375
385
    @staticmethod
436
446
 
437
447
    def set_pending(self):
438
448
        if (not self.base_is_ancestor or not self.base_is_other_ancestor
439
 
                or self.other_rev_id is None):
 
449
            or self.other_rev_id is None):
440
450
            return
441
451
        self._add_parent()
442
452
 
443
453
    def _add_parent(self):
444
454
        new_parents = self.this_tree.get_parent_ids() + [self.other_rev_id]
445
455
        new_parent_trees = []
446
 
        with contextlib.ExitStack() as stack:
447
 
            for revision_id in new_parents:
448
 
                try:
449
 
                    tree = self.revision_tree(revision_id)
450
 
                except errors.NoSuchRevision:
451
 
                    tree = None
452
 
                else:
453
 
                    stack.enter_context(tree.lock_read())
454
 
                new_parent_trees.append((revision_id, tree))
455
 
            self.this_tree.set_parent_trees(new_parent_trees, allow_leftmost_as_ghost=True)
 
456
        operation = cleanup.OperationWithCleanups(
 
457
            self.this_tree.set_parent_trees)
 
458
        for revision_id in new_parents:
 
459
            try:
 
460
                tree = self.revision_tree(revision_id)
 
461
            except errors.NoSuchRevision:
 
462
                tree = None
 
463
            else:
 
464
                tree.lock_read()
 
465
                operation.add_cleanup(tree.unlock)
 
466
            new_parent_trees.append((revision_id, tree))
 
467
        operation.run_simple(new_parent_trees, allow_leftmost_as_ghost=True)
456
468
 
457
469
    def set_other(self, other_revision, possible_transports=None):
458
470
        """Set the revision and tree to merge from.
479
491
                raise errors.NoCommits(self.other_branch)
480
492
        if self.other_rev_id is not None:
481
493
            self._cached_trees[self.other_rev_id] = self.other_tree
482
 
        self._maybe_fetch(self.other_branch,
483
 
                          self.this_branch, self.other_basis)
 
494
        self._maybe_fetch(self.other_branch, self.this_branch, self.other_basis)
484
495
 
485
496
    def set_other_revision(self, revision_id, other_branch):
486
497
        """Set 'other' based on a branch and revision id
523
534
                self.base_rev_id = _mod_revision.NULL_REVISION
524
535
            elif len(lcas) == 1:
525
536
                self.base_rev_id = list(lcas)[0]
526
 
            else:  # len(lcas) > 1
 
537
            else: # len(lcas) > 1
527
538
                self._is_criss_cross = True
528
539
                if len(lcas) > 2:
529
540
                    # find_unique_lca can only handle 2 nodes, so we have to
531
542
                    # the graph again, but better than re-implementing
532
543
                    # find_unique_lca.
533
544
                    self.base_rev_id = self.revision_graph.find_unique_lca(
534
 
                        revisions[0], revisions[1])
 
545
                                            revisions[0], revisions[1])
535
546
                else:
536
547
                    self.base_rev_id = self.revision_graph.find_unique_lca(
537
 
                        *lcas)
 
548
                                            *lcas)
538
549
                sorted_lca_keys = self.revision_graph.find_merge_order(
539
550
                    revisions[0], lcas)
540
551
                if self.base_rev_id == _mod_revision.NULL_REVISION:
553
564
                interesting_revision_ids = set(lcas)
554
565
                interesting_revision_ids.add(self.base_rev_id)
555
566
                interesting_trees = dict((t.get_revision_id(), t)
556
 
                                         for t in self.this_branch.repository.revision_trees(
557
 
                    interesting_revision_ids))
 
567
                    for t in self.this_branch.repository.revision_trees(
 
568
                        interesting_revision_ids))
558
569
                self._cached_trees.update(interesting_trees)
559
570
                if self.base_rev_id in lcas:
560
571
                    self.base_tree = interesting_trees[self.base_rev_id]
608
619
            raise errors.BzrError("Showing base is not supported for this"
609
620
                                  " merge type. %s" % self.merge_type)
610
621
        if (not getattr(self.merge_type, 'supports_reverse_cherrypick', True)
611
 
                and not self.base_is_other_ancestor):
 
622
            and not self.base_is_other_ancestor):
612
623
            raise errors.CannotReverseCherrypick()
613
624
        if self.merge_type.supports_cherrypick:
614
625
            kwargs['cherrypick'] = (not self.base_is_ancestor or
629
640
        for hook in Merger.hooks['post_merge']:
630
641
            hook(merge)
631
642
        if self.recurse == 'down':
632
 
            for relpath in self.this_tree.iter_references():
633
 
                sub_tree = self.this_tree.get_nested_tree(relpath)
 
643
            for relpath, file_id in self.this_tree.iter_references():
 
644
                sub_tree = self.this_tree.get_nested_tree(relpath, file_id)
634
645
                other_revision = self.other_tree.get_reference_revision(
635
 
                    relpath)
636
 
                if other_revision == sub_tree.last_revision():
 
646
                    relpath, file_id)
 
647
                if  other_revision == sub_tree.last_revision():
637
648
                    continue
638
649
                sub_merge = Merger(sub_tree.branch, this_tree=sub_tree)
639
650
                sub_merge.merge_type = self.merge_type
640
 
                other_branch = self.other_tree.reference_parent(relpath)
 
651
                other_branch = self.other_branch.reference_parent(
 
652
                    relpath, file_id)
641
653
                sub_merge.set_other_revision(other_revision, other_branch)
642
654
                base_tree_path = _mod_tree.find_previous_path(
643
655
                    self.this_tree, self.base_tree, relpath)
644
656
                base_revision = self.base_tree.get_reference_revision(
645
 
                    base_tree_path)
 
657
                    base_tree_path, file_id)
646
658
                sub_merge.base_tree = \
647
659
                    sub_tree.branch.repository.revision_tree(base_revision)
648
660
                sub_merge.base_rev_id = base_revision
650
662
        return merge
651
663
 
652
664
    def do_merge(self):
653
 
        with contextlib.ExitStack() as stack:
654
 
            stack.enter_context(self.this_tree.lock_tree_write())
655
 
            if self.base_tree is not None:
656
 
                stack.enter_context(self.base_tree.lock_read())
657
 
            if self.other_tree is not None:
658
 
                stack.enter_context(self.other_tree.lock_read())
659
 
            merge = self._do_merge_to()
 
665
        operation = cleanup.OperationWithCleanups(self._do_merge_to)
 
666
        self.this_tree.lock_tree_write()
 
667
        operation.add_cleanup(self.this_tree.unlock)
 
668
        if self.base_tree is not None:
 
669
            self.base_tree.lock_read()
 
670
            operation.add_cleanup(self.base_tree.unlock)
 
671
        if self.other_tree is not None:
 
672
            self.other_tree.lock_read()
 
673
            operation.add_cleanup(self.other_tree.unlock)
 
674
        merge = operation.run_simple()
660
675
        if len(merge.cooked_conflicts) == 0:
661
676
            if not self.ignore_zero and not trace.is_quiet():
662
677
                trace.note(gettext("All changes applied successfully."))
681
696
    symlink_target = None
682
697
    text_sha1 = None
683
698
 
684
 
    def is_unmodified(self, other):
685
 
        return other is self
686
 
 
687
 
 
688
699
_none_entry = _InventoryNoneEntry()
689
700
 
690
701
 
753
764
            self.do_merge()
754
765
 
755
766
    def do_merge(self):
756
 
        with contextlib.ExitStack() as stack:
757
 
            stack.enter_context(self.working_tree.lock_tree_write())
758
 
            stack.enter_context(self.this_tree.lock_read())
759
 
            stack.enter_context(self.base_tree.lock_read())
760
 
            stack.enter_context(self.other_tree.lock_read())
761
 
            self.tt = self.working_tree.transform()
762
 
            stack.enter_context(self.tt)
763
 
            self._compute_transform()
764
 
            results = self.tt.apply(no_conflicts=True)
765
 
            self.write_modified(results)
766
 
            try:
767
 
                self.working_tree.add_conflicts(self.cooked_conflicts)
768
 
            except errors.UnsupportedOperation:
769
 
                pass
 
767
        operation = cleanup.OperationWithCleanups(self._do_merge)
 
768
        self.working_tree.lock_tree_write()
 
769
        operation.add_cleanup(self.working_tree.unlock)
 
770
        self.this_tree.lock_read()
 
771
        operation.add_cleanup(self.this_tree.unlock)
 
772
        self.base_tree.lock_read()
 
773
        operation.add_cleanup(self.base_tree.unlock)
 
774
        self.other_tree.lock_read()
 
775
        operation.add_cleanup(self.other_tree.unlock)
 
776
        operation.run()
 
777
 
 
778
    def _do_merge(self, operation):
 
779
        self.tt = transform.TreeTransform(self.working_tree, None)
 
780
        operation.add_cleanup(self.tt.finalize)
 
781
        self._compute_transform()
 
782
        results = self.tt.apply(no_conflicts=True)
 
783
        self.write_modified(results)
 
784
        try:
 
785
            self.working_tree.add_conflicts(self.cooked_conflicts)
 
786
        except errors.UnsupportedOperation:
 
787
            pass
770
788
 
771
789
    def make_preview_transform(self):
772
 
        with self.base_tree.lock_read(), self.other_tree.lock_read():
773
 
            self.tt = self.working_tree.preview_transform()
774
 
            self._compute_transform()
775
 
            return self.tt
 
790
        operation = cleanup.OperationWithCleanups(self._make_preview_transform)
 
791
        self.base_tree.lock_read()
 
792
        operation.add_cleanup(self.base_tree.unlock)
 
793
        self.other_tree.lock_read()
 
794
        operation.add_cleanup(self.other_tree.unlock)
 
795
        return operation.run_simple()
 
796
 
 
797
    def _make_preview_transform(self):
 
798
        self.tt = transform.TransformPreview(self.working_tree)
 
799
        self._compute_transform()
 
800
        return self.tt
776
801
 
777
802
    def _compute_transform(self):
778
803
        if self._lca_trees is None:
779
 
            entries = list(self._entries3())
 
804
            entries = self._entries3()
780
805
            resolver = self._three_way
781
806
        else:
782
 
            entries = list(self._entries_lca())
 
807
            entries = self._entries_lca()
783
808
            resolver = self._lca_multi_way
784
809
        # Prepare merge hooks
785
810
        factories = Merger.hooks['merge_file_content']
789
814
        with ui.ui_factory.nested_progress_bar() as child_pb:
790
815
            for num, (file_id, changed, paths3, parents3, names3,
791
816
                      executable3) in enumerate(entries):
792
 
                trans_id = self.tt.trans_id_file_id(file_id)
793
817
                # Try merging each entry
794
818
                child_pb.update(gettext('Preparing file merge'),
795
819
                                num, len(entries))
796
 
                self._merge_names(trans_id, file_id, paths3, parents3,
797
 
                                  names3, resolver=resolver)
 
820
                self._merge_names(file_id, paths3, parents3, names3, resolver=resolver)
798
821
                if changed:
799
 
                    file_status = self._do_merge_contents(paths3, trans_id, file_id)
 
822
                    file_status = self._do_merge_contents(paths3, file_id)
800
823
                else:
801
824
                    file_status = 'unmodified'
802
 
                self._merge_executable(paths3, trans_id, executable3,
803
 
                                       file_status, resolver=resolver)
 
825
                self._merge_executable(paths3, file_id, executable3,
 
826
                        file_status, resolver=resolver)
804
827
        self.tt.fixup_new_roots()
805
828
        self._finish_computing_transform()
806
829
 
810
833
        This is the second half of _compute_transform.
811
834
        """
812
835
        with ui.ui_factory.nested_progress_bar() as child_pb:
813
 
            fs_conflicts = transform.resolve_conflicts(
814
 
                self.tt, child_pb,
 
836
            fs_conflicts = transform.resolve_conflicts(self.tt, child_pb,
815
837
                lambda t, c: transform.conflict_pass(t, c, self.other_tree))
816
838
        if self.change_reporter is not None:
817
839
            from breezy import delta
819
841
                self.tt.iter_changes(), self.change_reporter)
820
842
        self.cook_conflicts(fs_conflicts)
821
843
        for conflict in self.cooked_conflicts:
822
 
            trace.warning('%s', conflict.describe())
 
844
            trace.warning(unicode(conflict))
823
845
 
824
846
    def _entries3(self):
825
847
        """Gather data about files modified between three trees.
830
852
        other and this.  names3 is a tuple of names for base, other and this.
831
853
        executable3 is a tuple of execute-bit values for base, other and this.
832
854
        """
 
855
        result = []
833
856
        iterator = self.other_tree.iter_changes(self.base_tree,
834
 
                                                specific_files=self.interesting_files,
835
 
                                                extra_trees=[self.this_tree])
 
857
                specific_files=self.interesting_files,
 
858
                extra_trees=[self.this_tree])
836
859
        this_interesting_files = self.this_tree.find_related_paths_across_trees(
837
 
            self.interesting_files, trees=[self.other_tree])
 
860
                self.interesting_files, trees=[self.other_tree])
838
861
        this_entries = dict(self.this_tree.iter_entries_by_dir(
839
862
                            specific_files=this_interesting_files))
840
 
        for change in iterator:
841
 
            if change.path[0] is not None:
 
863
        for (file_id, paths, changed, versioned, parents, names, kind,
 
864
             executable) in iterator:
 
865
            if paths[0] is not None:
842
866
                this_path = _mod_tree.find_previous_path(
843
 
                    self.base_tree, self.this_tree, change.path[0])
 
867
                        self.base_tree, self.this_tree, paths[0])
844
868
            else:
845
869
                this_path = _mod_tree.find_previous_path(
846
 
                    self.other_tree, self.this_tree, change.path[1])
 
870
                        self.other_tree, self.this_tree, paths[1])
847
871
            this_entry = this_entries.get(this_path)
848
872
            if this_entry is not None:
849
873
                this_name = this_entry.name
853
877
                this_name = None
854
878
                this_parent = None
855
879
                this_executable = None
856
 
            parents3 = change.parent_id + (this_parent,)
857
 
            names3 = change.name + (this_name,)
858
 
            paths3 = change.path + (this_path, )
859
 
            executable3 = change.executable + (this_executable,)
860
 
            yield (
861
 
                (change.file_id, change.changed_content, paths3,
862
 
                 parents3, names3, executable3))
 
880
            parents3 = parents + (this_parent,)
 
881
            names3 = names + (this_name,)
 
882
            paths3 = paths + (this_path, )
 
883
            executable3 = executable + (this_executable,)
 
884
            result.append((file_id, changed, paths3, parents3, names3, executable3))
 
885
        return result
863
886
 
864
887
    def _entries_lca(self):
865
888
        """Gather data about files modified between multiple trees.
885
908
            lookup_trees.extend(self._lca_trees)
886
909
            # I think we should include the lca trees as well
887
910
            interesting_files = self.other_tree.find_related_paths_across_trees(
888
 
                self.interesting_files, lookup_trees)
 
911
                    self.interesting_files, lookup_trees)
889
912
        else:
890
913
            interesting_files = None
891
 
        from .multiwalker import MultiWalker
892
 
        walker = MultiWalker(self.other_tree, self._lca_trees)
 
914
        result = []
 
915
        walker = _mod_tree.MultiWalker(self.other_tree, self._lca_trees)
893
916
 
894
 
        for other_path, file_id, other_ie, lca_values in walker.iter_all():
 
917
        base_inventory = self.base_tree.root_inventory
 
918
        this_inventory = self.this_tree.root_inventory
 
919
        for path, file_id, other_ie, lca_values in walker.iter_all():
895
920
            # Is this modified at all from any of the other trees?
896
921
            if other_ie is None:
897
922
                other_ie = _none_entry
898
923
                other_path = None
 
924
            else:
 
925
                other_path = self.other_tree.id2path(file_id)
899
926
            if interesting_files is not None and other_path not in interesting_files:
900
927
                continue
901
928
 
905
932
            # we know that the ancestry is linear, and that OTHER did not
906
933
            # modify anything
907
934
            # See doc/developers/lca_merge_resolution.txt for details
908
 
            # We can't use this shortcut when other_revision is None,
909
 
            # because it may be None because things are WorkingTrees, and
910
 
            # not because it is *actually* None.
911
 
            is_unmodified = False
912
 
            for lca_path, ie in lca_values:
913
 
                if ie is not None and other_ie.is_unmodified(ie):
914
 
                    is_unmodified = True
915
 
                    break
916
 
            if is_unmodified:
917
 
                continue
 
935
            other_revision = other_ie.revision
 
936
            if other_revision is not None:
 
937
                # We can't use this shortcut when other_revision is None,
 
938
                # because it may be None because things are WorkingTrees, and
 
939
                # not because it is *actually* None.
 
940
                is_unmodified = False
 
941
                for lca_path, ie in lca_values:
 
942
                    if ie is not None and ie.revision == other_revision:
 
943
                        is_unmodified = True
 
944
                        break
 
945
                if is_unmodified:
 
946
                    continue
918
947
 
919
948
            lca_entries = []
920
949
            lca_paths = []
927
956
                    lca_paths.append(lca_path)
928
957
 
929
958
            try:
 
959
                base_ie = base_inventory.get_entry(file_id)
 
960
            except errors.NoSuchId:
 
961
                base_ie = _none_entry
 
962
                base_path = None
 
963
            else:
930
964
                base_path = self.base_tree.id2path(file_id)
931
 
            except errors.NoSuchId:
932
 
                base_path = None
933
 
                base_ie = _none_entry
934
 
            else:
935
 
                base_ie = next(self.base_tree.iter_entries_by_dir(specific_files=[base_path]))[1]
936
965
 
937
966
            try:
938
 
                this_path = self.this_tree.id2path(file_id)
 
967
                this_ie = this_inventory.get_entry(file_id)
939
968
            except errors.NoSuchId:
940
969
                this_ie = _none_entry
941
970
                this_path = None
942
971
            else:
943
 
                this_ie = next(self.this_tree.iter_entries_by_dir(specific_files=[this_path]))[1]
 
972
                this_path = self.this_tree.id2path(file_id)
944
973
 
945
974
            lca_kinds = []
946
975
            lca_parent_ids = []
975
1004
                        if path is None:
976
1005
                            return None
977
1006
                        try:
978
 
                            return tree.get_file_sha1(path)
 
1007
                            return tree.get_file_sha1(path, file_id)
979
1008
                        except errors.NoSuchFile:
980
1009
                            return None
981
1010
                    base_sha1 = get_sha1(self.base_tree, base_path)
991
1020
                        (base_ie.executable, lca_executable),
992
1021
                        other_ie.executable, this_ie.executable)
993
1022
                    if (parent_id_winner == 'this' and name_winner == 'this'
994
 
                            and sha1_winner == 'this' and exec_winner == 'this'):
 
1023
                        and sha1_winner == 'this' and exec_winner == 'this'):
995
1024
                        # No kind, parent, name, exec, or content change for
996
1025
                        # OTHER, so this node is not considered interesting
997
1026
                        continue
1001
1030
                    def get_target(ie, tree, path):
1002
1031
                        if ie.kind != 'symlink':
1003
1032
                            return None
1004
 
                        return tree.get_symlink_target(path)
 
1033
                        return tree.get_symlink_target(path, file_id)
1005
1034
                    base_target = get_target(base_ie, self.base_tree, base_path)
1006
1035
                    lca_targets = [get_target(ie, tree, lca_path) for ie, tree, lca_path
1007
1036
                                   in zip(lca_entries, self._lca_trees, lca_paths)]
1008
 
                    this_target = get_target(
1009
 
                        this_ie, self.this_tree, this_path)
1010
 
                    other_target = get_target(
1011
 
                        other_ie, self.other_tree, other_path)
 
1037
                    this_target = get_target(this_ie, self.this_tree, this_path)
 
1038
                    other_target = get_target(other_ie, self.other_tree, other_path)
1012
1039
                    target_winner = self._lca_multi_way(
1013
1040
                        (base_target, lca_targets),
1014
1041
                        other_target, this_target)
1015
1042
                    if (parent_id_winner == 'this' and name_winner == 'this'
1016
 
                            and target_winner == 'this'):
 
1043
                        and target_winner == 'this'):
1017
1044
                        # No kind, parent, name, or symlink target change
1018
1045
                        # not interesting
1019
1046
                        continue
1031
1058
                    raise AssertionError('unhandled kind: %s' % other_ie.kind)
1032
1059
 
1033
1060
            # If we have gotten this far, that means something has changed
1034
 
            yield (file_id, content_changed,
 
1061
            result.append((file_id, content_changed,
1035
1062
                           ((base_path, lca_paths),
1036
1063
                            other_path, this_path),
1037
1064
                           ((base_ie.parent_id, lca_parent_ids),
1040
1067
                            other_ie.name, this_ie.name),
1041
1068
                           ((base_ie.executable, lca_executable),
1042
1069
                            other_ie.executable, this_ie.executable)
1043
 
                           )
 
1070
                          ))
 
1071
        return result
1044
1072
 
1045
1073
    def write_modified(self, results):
1046
1074
        if not self.working_tree.supports_merge_modified():
1048
1076
        modified_hashes = {}
1049
1077
        for path in results.modified_paths:
1050
1078
            wt_relpath = self.working_tree.relpath(path)
1051
 
            if not self.working_tree.is_versioned(wt_relpath):
 
1079
            file_id = self.working_tree.path2id(wt_relpath)
 
1080
            if file_id is None:
1052
1081
                continue
1053
 
            hash = self.working_tree.get_file_sha1(wt_relpath)
 
1082
            hash = self.working_tree.get_file_sha1(wt_relpath, file_id)
1054
1083
            if hash is None:
1055
1084
                continue
1056
 
            modified_hashes[wt_relpath] = hash
 
1085
            modified_hashes[file_id] = hash
1057
1086
        self.working_tree.set_merge_modified(modified_hashes)
1058
1087
 
1059
1088
    @staticmethod
1060
 
    def parent(entry):
 
1089
    def parent(entry, file_id):
1061
1090
        """Determine the parent for a file_id (used as a key method)"""
1062
1091
        if entry is None:
1063
1092
            return None
1064
1093
        return entry.parent_id
1065
1094
 
1066
1095
    @staticmethod
1067
 
    def name(entry):
 
1096
    def name(entry, file_id):
1068
1097
        """Determine the name for a file_id (used as a key method)"""
1069
1098
        if entry is None:
1070
1099
            return None
1071
1100
        return entry.name
1072
1101
 
1073
1102
    @staticmethod
1074
 
    def contents_sha1(tree, path):
 
1103
    def contents_sha1(tree, path, file_id=None):
1075
1104
        """Determine the sha1 of the file contents (used as a key method)."""
1076
1105
        try:
1077
 
            return tree.get_file_sha1(path)
 
1106
            return tree.get_file_sha1(path, file_id)
1078
1107
        except errors.NoSuchFile:
1079
1108
            return None
1080
1109
 
1081
1110
    @staticmethod
1082
 
    def executable(tree, path):
 
1111
    def executable(tree, path, file_id=None):
1083
1112
        """Determine the executability of a file-id (used as a key method)."""
1084
1113
        try:
1085
 
            if tree.kind(path) != "file":
 
1114
            if tree.kind(path, file_id) != "file":
1086
1115
                return False
1087
1116
        except errors.NoSuchFile:
1088
1117
            return None
1089
 
        return tree.is_executable(path)
 
1118
        return tree.is_executable(path, file_id)
1090
1119
 
1091
1120
    @staticmethod
1092
 
    def kind(tree, path):
 
1121
    def kind(tree, path, file_id=None):
1093
1122
        """Determine the kind of a file-id (used as a key method)."""
1094
1123
        try:
1095
 
            return tree.kind(path)
 
1124
            return tree.kind(path, file_id)
1096
1125
        except errors.NoSuchFile:
1097
1126
            return None
1098
1127
 
1137
1166
        base_val, lca_vals = bases
1138
1167
        # Remove 'base_val' from the lca_vals, because it is not interesting
1139
1168
        filtered_lca_vals = [lca_val for lca_val in lca_vals
1140
 
                             if lca_val != base_val]
 
1169
                                      if lca_val != base_val]
1141
1170
        if len(filtered_lca_vals) == 0:
1142
1171
            return Merge3Merger._three_way(base_val, other, this)
1143
1172
 
1162
1191
        # At this point, the lcas disagree, and the tip disagree
1163
1192
        return 'conflict'
1164
1193
 
1165
 
    def _merge_names(self, trans_id, file_id, paths, parents, names, resolver):
1166
 
        """Perform a merge on file names and parents"""
 
1194
    def merge_names(self, paths):
 
1195
        def get_entry(tree, path):
 
1196
            try:
 
1197
                return next(tree.iter_entries_by_dir(specific_files=[path]))[1]
 
1198
            except StopIteration:
 
1199
                return None
 
1200
        used_base_path, other_path, this_path = paths
 
1201
        this_entry = get_entry(self.this_tree, this_path)
 
1202
        other_entry = get_entry(self.other_tree, other_path)
 
1203
        base_entry = get_entry(self.base_tree, base_path)
 
1204
        entries = (base_entry, other_entry, this_entry)
 
1205
        names = []
 
1206
        parents = []
 
1207
        for entry in entries:
 
1208
            if entry is None:
 
1209
                names.append(None)
 
1210
                parents.append(None)
 
1211
            else:
 
1212
                names.append(entry.name)
 
1213
                parents.append(entry.parent_id)
 
1214
        return self._merge_names(file_id, paths, parents, names,
 
1215
                                 resolver=self._three_way)
 
1216
 
 
1217
    def _merge_names(self, file_id, paths, parents, names, resolver):
 
1218
        """Perform a merge on file_id names and parents"""
1167
1219
        base_name, other_name, this_name = names
1168
1220
        base_parent, other_parent, this_parent = parents
1169
1221
        unused_base_path, other_path, this_path = paths
1182
1234
            # Creating helpers (.OTHER or .THIS) here cause problems down the
1183
1235
            # road if a ContentConflict needs to be created so we should not do
1184
1236
            # that
 
1237
            trans_id = self.tt.trans_id_file_id(file_id)
1185
1238
            self._raw_conflicts.append(('path conflict', trans_id, file_id,
1186
1239
                                        this_parent, this_name,
1187
1240
                                        other_parent, other_name))
1204
1257
                parent_trans_id = transform.ROOT_PARENT
1205
1258
            else:
1206
1259
                parent_trans_id = self.tt.trans_id_file_id(parent_id)
1207
 
            self.tt.adjust_path(name, parent_trans_id, trans_id)
 
1260
            self.tt.adjust_path(name, parent_trans_id,
 
1261
                                self.tt.trans_id_file_id(file_id))
1208
1262
 
1209
 
    def _do_merge_contents(self, paths, trans_id, file_id):
 
1263
    def _do_merge_contents(self, paths, file_id):
1210
1264
        """Performs a merge on file_id contents."""
1211
1265
        def contents_pair(tree, path):
1212
1266
            if path is None:
1213
1267
                return (None, None)
1214
1268
            try:
1215
 
                kind = tree.kind(path)
 
1269
                kind = tree.kind(path, file_id)
1216
1270
            except errors.NoSuchFile:
1217
1271
                return (None, None)
1218
1272
            if kind == "file":
1219
 
                contents = tree.get_file_sha1(path)
 
1273
                contents = tree.get_file_sha1(path, file_id)
1220
1274
            elif kind == "symlink":
1221
 
                contents = tree.get_symlink_target(path)
 
1275
                contents = tree.get_symlink_target(path, file_id)
1222
1276
            else:
1223
1277
                contents = None
1224
1278
            return kind, contents
1250
1304
            return "unmodified"
1251
1305
        # We have a hypothetical conflict, but if we have files, then we
1252
1306
        # can try to merge the content
 
1307
        trans_id = self.tt.trans_id_file_id(file_id)
1253
1308
        params = MergeFileHookParams(
1254
 
            self, (base_path, other_path, this_path), trans_id, this_pair[0],
 
1309
            self, file_id, (base_path, other_path,
 
1310
            this_path), trans_id, this_pair[0],
1255
1311
            other_pair[0], winner)
1256
1312
        hooks = self.active_hooks
1257
1313
        hook_status = 'not_applicable'
1271
1327
            result = None
1272
1328
            name = self.tt.final_name(trans_id)
1273
1329
            parent_id = self.tt.final_parent(trans_id)
 
1330
            duplicate = False
1274
1331
            inhibit_content_conflict = False
1275
 
            if params.this_kind is None:  # file_id is not in THIS
 
1332
            if params.this_kind is None: # file_id is not in THIS
1276
1333
                # Is the name used for a different file_id ?
1277
1334
                if self.this_tree.is_versioned(other_path):
1278
1335
                    # Two entries for the same path
1279
1336
                    keep_this = True
1280
1337
                    # versioning the merged file will trigger a duplicate
1281
1338
                    # conflict
1282
 
                    self.tt.version_file(trans_id, file_id=file_id)
 
1339
                    self.tt.version_file(file_id, trans_id)
1283
1340
                    transform.create_from_tree(
1284
1341
                        self.tt, trans_id, self.other_tree,
1285
 
                        other_path,
1286
 
                        filter_tree_path=self._get_filter_tree_path(other_path))
 
1342
                        other_path, file_id=file_id,
 
1343
                        filter_tree_path=self._get_filter_tree_path(file_id))
1287
1344
                    inhibit_content_conflict = True
1288
 
            elif params.other_kind is None:  # file_id is not in OTHER
 
1345
            elif params.other_kind is None: # file_id is not in OTHER
1289
1346
                # Is the name used for a different file_id ?
1290
1347
                if self.other_tree.is_versioned(this_path):
1291
1348
                    # Two entries for the same path again, but here, the other
1302
1359
                # This is a contents conflict, because none of the available
1303
1360
                # functions could merge it.
1304
1361
                file_group = self._dump_conflicts(
1305
 
                    name, (base_path, other_path, this_path), parent_id,
1306
 
                    file_id, set_version=True)
 
1362
                        name, (base_path, other_path, this_path), parent_id,
 
1363
                        file_id, set_version=True)
1307
1364
                self._raw_conflicts.append(('contents conflict', file_group))
1308
1365
        elif hook_status == 'success':
1309
1366
            self.tt.create_file(lines, trans_id)
1326
1383
        else:
1327
1384
            raise AssertionError('unknown hook_status: %r' % (hook_status,))
1328
1385
        if not this_path and result == "modified":
1329
 
            self.tt.version_file(trans_id, file_id=file_id)
 
1386
            self.tt.version_file(file_id, trans_id)
1330
1387
        if not keep_this:
1331
1388
            # The merge has been performed and produced a new content, so the
1332
1389
            # old contents should not be retained.
1335
1392
 
1336
1393
    def _default_other_winner_merge(self, merge_hook_params):
1337
1394
        """Replace this contents with other."""
 
1395
        file_id = merge_hook_params.file_id
1338
1396
        trans_id = merge_hook_params.trans_id
1339
1397
        if merge_hook_params.other_path is not None:
1340
1398
            # OTHER changed the file
1341
1399
            transform.create_from_tree(
1342
1400
                self.tt, trans_id, self.other_tree,
1343
 
                merge_hook_params.other_path,
1344
 
                filter_tree_path=self._get_filter_tree_path(merge_hook_params.other_path))
 
1401
                merge_hook_params.other_path, file_id=file_id,
 
1402
                filter_tree_path=self._get_filter_tree_path(file_id))
1345
1403
            return 'done', None
1346
1404
        elif merge_hook_params.this_path is not None:
1347
1405
            # OTHER deleted the file
1348
1406
            return 'delete', None
1349
1407
        else:
1350
1408
            raise AssertionError(
1351
 
                'winner is OTHER, but file %r not in THIS or OTHER tree'
1352
 
                % (merge_hook_params.base_path,))
 
1409
                'winner is OTHER, but file_id %r not in THIS or OTHER tree'
 
1410
                % (file_id,))
1353
1411
 
1354
1412
    def merge_contents(self, merge_hook_params):
1355
1413
        """Fallback merge logic after user installed hooks."""
1365
1423
            # have agreement that output should be a file.
1366
1424
            try:
1367
1425
                self.text_merge(merge_hook_params.trans_id,
1368
 
                                merge_hook_params.paths)
 
1426
                                merge_hook_params.paths, merge_hook_params.file_id)
1369
1427
            except errors.BinaryFile:
1370
1428
                return 'not_applicable', None
1371
1429
            return 'done', None
1372
1430
        else:
1373
1431
            return 'not_applicable', None
1374
1432
 
1375
 
    def get_lines(self, tree, path):
 
1433
    def get_lines(self, tree, path, file_id=None):
1376
1434
        """Return the lines in a file, or an empty list."""
1377
1435
        if path is None:
1378
1436
            return []
1379
1437
        try:
1380
 
            kind = tree.kind(path)
 
1438
            kind = tree.kind(path, file_id)
1381
1439
        except errors.NoSuchFile:
1382
1440
            return []
1383
1441
        else:
1384
1442
            if kind != 'file':
1385
1443
                return []
1386
 
            return tree.get_file_lines(path)
 
1444
            return tree.get_file_lines(path, file_id)
1387
1445
 
1388
 
    def text_merge(self, trans_id, paths):
1389
 
        """Perform a three-way text merge on a file"""
 
1446
    def text_merge(self, trans_id, paths, file_id):
 
1447
        """Perform a three-way text merge on a file_id"""
1390
1448
        # it's possible that we got here with base as a different type.
1391
1449
        # if so, we just want two-way text conflicts.
1392
1450
        base_path, other_path, this_path = paths
1393
 
        base_lines = self.get_lines(self.base_tree, base_path)
1394
 
        other_lines = self.get_lines(self.other_tree, other_path)
1395
 
        this_lines = self.get_lines(self.this_tree, this_path)
 
1451
        base_lines = self.get_lines(self.base_tree, base_path, file_id)
 
1452
        other_lines = self.get_lines(self.other_tree, other_path, file_id)
 
1453
        this_lines = self.get_lines(self.this_tree, this_path, file_id)
1396
1454
        m3 = merge3.Merge3(base_lines, this_lines, other_lines,
1397
1455
                           is_cherrypick=self.cherrypick)
1398
 
        start_marker = b"!START OF MERGE CONFLICT!" + b"I HOPE THIS IS UNIQUE"
 
1456
        start_marker = "!START OF MERGE CONFLICT!" + "I HOPE THIS IS UNIQUE"
1399
1457
        if self.show_base is True:
1400
 
            base_marker = b'|' * 7
 
1458
            base_marker = '|' * 7
1401
1459
        else:
1402
1460
            base_marker = None
1403
1461
 
1404
1462
        def iter_merge3(retval):
1405
1463
            retval["text_conflicts"] = False
1406
 
            for line in m3.merge_lines(name_a=b"TREE",
1407
 
                                       name_b=b"MERGE-SOURCE",
1408
 
                                       name_base=b"BASE-REVISION",
 
1464
            for line in m3.merge_lines(name_a = "TREE",
 
1465
                                       name_b = "MERGE-SOURCE",
 
1466
                                       name_base = "BASE-REVISION",
1409
1467
                                       start_marker=start_marker,
1410
1468
                                       base_marker=base_marker,
1411
1469
                                       reprocess=self.reprocess):
1412
1470
                if line.startswith(start_marker):
1413
1471
                    retval["text_conflicts"] = True
1414
 
                    yield line.replace(start_marker, b'<' * 7)
 
1472
                    yield line.replace(start_marker, '<' * 7)
1415
1473
                else:
1416
1474
                    yield line
1417
1475
        retval = {}
1421
1479
            self._raw_conflicts.append(('text conflict', trans_id))
1422
1480
            name = self.tt.final_name(trans_id)
1423
1481
            parent_id = self.tt.final_parent(trans_id)
1424
 
            file_id = self.tt.final_file_id(trans_id)
1425
1482
            file_group = self._dump_conflicts(name, paths, parent_id, file_id,
1426
1483
                                              this_lines, base_lines,
1427
1484
                                              other_lines)
1428
1485
            file_group.append(trans_id)
1429
1486
 
1430
 
    def _get_filter_tree_path(self, path):
 
1487
    def _get_filter_tree_path(self, file_id):
1431
1488
        if self.this_tree.supports_content_filtering():
1432
1489
            # We get the path from the working tree if it exists.
1433
1490
            # That fails though when OTHER is adding a file, so
1434
1491
            # we fall back to the other tree to find the path if
1435
1492
            # it doesn't exist locally.
1436
 
            filter_path = _mod_tree.find_previous_path(
1437
 
                self.other_tree, self.working_tree, path)
1438
 
            if filter_path is None:
1439
 
                filter_path = path
1440
 
            return filter_path
1441
 
        # Skip the lookup for older formats
 
1493
            try:
 
1494
                return self.this_tree.id2path(file_id)
 
1495
            except errors.NoSuchId:
 
1496
                return self.other_tree.id2path(file_id)
 
1497
        # Skip the id2path lookup for older formats
1442
1498
        return None
1443
1499
 
1444
1500
    def _dump_conflicts(self, name, paths, parent_id, file_id, this_lines=None,
1456
1512
            data.append(('BASE', self.base_tree, base_path, base_lines))
1457
1513
 
1458
1514
        # We need to use the actual path in the working tree of the file here,
1459
 
        if self.this_tree.supports_content_filtering():
1460
 
            filter_tree_path = this_path
 
1515
        # ignoring the conflict suffixes
 
1516
        wt = self.this_tree
 
1517
        if wt.supports_content_filtering():
 
1518
            try:
 
1519
                filter_tree_path = wt.id2path(file_id)
 
1520
            except errors.NoSuchId:
 
1521
                # file has been deleted
 
1522
                filter_tree_path = None
1461
1523
        else:
1462
1524
            # Skip the id2path lookup for older formats
1463
1525
            filter_tree_path = None
1467
1529
        for suffix, tree, path, lines in data:
1468
1530
            if path is not None:
1469
1531
                trans_id = self._conflict_file(
1470
 
                    name, parent_id, path, tree, suffix, lines,
1471
 
                    filter_tree_path)
 
1532
                        name, parent_id, path, tree, file_id, suffix, lines,
 
1533
                        filter_tree_path)
1472
1534
                file_group.append(trans_id)
1473
1535
                if set_version and not versioned:
1474
 
                    self.tt.version_file(trans_id, file_id=file_id)
 
1536
                    self.tt.version_file(file_id, trans_id)
1475
1537
                    versioned = True
1476
1538
        return file_group
1477
1539
 
1478
 
    def _conflict_file(self, name, parent_id, path, tree, suffix,
 
1540
    def _conflict_file(self, name, parent_id, path, tree, file_id, suffix,
1479
1541
                       lines=None, filter_tree_path=None):
1480
1542
        """Emit a single conflict file."""
1481
1543
        name = name + '.' + suffix
1482
1544
        trans_id = self.tt.create_path(name, parent_id)
1483
1545
        transform.create_from_tree(
1484
 
            self.tt, trans_id, tree, path,
1485
 
            chunks=lines,
1486
 
            filter_tree_path=filter_tree_path)
 
1546
                self.tt, trans_id, tree, path,
 
1547
                file_id=file_id, chunks=lines,
 
1548
                filter_tree_path=filter_tree_path)
1487
1549
        return trans_id
1488
1550
 
1489
 
    def _merge_executable(self, paths, trans_id, executable, file_status,
 
1551
    def merge_executable(self, paths, file_id, file_status):
 
1552
        """Perform a merge on the execute bit."""
 
1553
        executable = [self.executable(t, p, file_id) for t, p in zip([self.base_tree,
 
1554
                      self.other_tree, self.this_tree], paths)]
 
1555
        self._merge_executable(paths, file_id, executable, file_status,
 
1556
                               resolver=self._three_way)
 
1557
 
 
1558
    def _merge_executable(self, paths, file_id, executable, file_status,
1490
1559
                          resolver):
1491
1560
        """Perform a merge on the execute bit."""
1492
1561
        base_executable, other_executable, this_executable = executable
1495
1564
            return
1496
1565
        winner = resolver(*executable)
1497
1566
        if winner == "conflict":
1498
 
            # There must be a None in here, if we have a conflict, but we
1499
 
            # need executability since file status was not deleted.
 
1567
        # There must be a None in here, if we have a conflict, but we
 
1568
        # need executability since file status was not deleted.
1500
1569
            if other_path is None:
1501
1570
                winner = "this"
1502
1571
            else:
1503
1572
                winner = "other"
1504
1573
        if winner == 'this' and file_status != "modified":
1505
1574
            return
 
1575
        trans_id = self.tt.trans_id_file_id(file_id)
1506
1576
        if self.tt.final_kind(trans_id) != "file":
1507
1577
            return
1508
1578
        if winner == "this":
1515
1585
            elif base_path is not None:
1516
1586
                executability = base_executable
1517
1587
        if executability is not None:
 
1588
            trans_id = self.tt.trans_id_file_id(file_id)
1518
1589
            self.tt.set_executability(executability, trans_id)
1519
1590
 
1520
1591
    def cook_conflicts(self, fs_conflicts):
1526
1597
            conflict_type = conflict[0]
1527
1598
            if conflict_type == 'path conflict':
1528
1599
                (trans_id, file_id,
1529
 
                 this_parent, this_name,
1530
 
                 other_parent, other_name) = conflict[1:]
 
1600
                this_parent, this_name,
 
1601
                other_parent, other_name) = conflict[1:]
1531
1602
                if this_parent is None or this_name is None:
1532
1603
                    this_path = '<deleted>'
1533
1604
                else:
1534
 
                    parent_path = fp.get_path(
 
1605
                    parent_path =  fp.get_path(
1535
1606
                        self.tt.trans_id_file_id(this_parent))
1536
1607
                    this_path = osutils.pathjoin(parent_path, this_name)
1537
1608
                if other_parent is None or other_name is None:
1538
1609
                    other_path = '<deleted>'
1539
1610
                else:
1540
 
                    if other_parent == self.other_tree.path2id(''):
 
1611
                    if other_parent == self.other_tree.get_root_id():
1541
1612
                        # The tree transform doesn't know about the other root,
1542
1613
                        # so we special case here to avoid a NoFinalPath
1543
1614
                        # exception
1544
1615
                        parent_path = ''
1545
1616
                    else:
1546
 
                        parent_path = fp.get_path(
 
1617
                        parent_path =  fp.get_path(
1547
1618
                            self.tt.trans_id_file_id(other_parent))
1548
1619
                    other_path = osutils.pathjoin(parent_path, other_name)
1549
1620
                c = _mod_conflicts.Conflict.factory(
1582
1653
        # conflict is enough.
1583
1654
        for c in cooked_conflicts:
1584
1655
            if (c.typestring == 'path conflict'
1585
 
                    and c.file_id in content_conflict_file_ids):
 
1656
                and c.file_id in content_conflict_file_ids):
1586
1657
                continue
1587
1658
            self.cooked_conflicts.append(c)
1588
1659
        self.cooked_conflicts.sort(key=_mod_conflicts.Conflict.sort_key)
1596
1667
    history_based = True
1597
1668
    requires_file_merge_plan = True
1598
1669
 
1599
 
    def _generate_merge_plan(self, this_path, base):
1600
 
        return self.this_tree.plan_file_merge(this_path, self.other_tree,
 
1670
    def _generate_merge_plan(self, file_id, base):
 
1671
        return self.this_tree.plan_file_merge(file_id, self.other_tree,
1601
1672
                                              base=base)
1602
1673
 
1603
 
    def _merged_lines(self, this_path):
 
1674
    def _merged_lines(self, file_id):
1604
1675
        """Generate the merged lines.
1605
1676
        There is no distinction between lines that are meant to contain <<<<<<<
1606
1677
        and conflicts.
1609
1680
            base = self.base_tree
1610
1681
        else:
1611
1682
            base = None
1612
 
        plan = self._generate_merge_plan(this_path, base)
 
1683
        plan = self._generate_merge_plan(file_id, base)
1613
1684
        if 'merge' in debug.debug_flags:
1614
1685
            plan = list(plan)
1615
1686
            trans_id = self.tt.trans_id_file_id(file_id)
1616
1687
            name = self.tt.final_name(trans_id) + '.plan'
1617
 
            contents = (b'%11s|%s' % l for l in plan)
 
1688
            contents = ('%11s|%s' % l for l in plan)
1618
1689
            self.tt.new_file(name, self.tt.final_parent(trans_id), contents)
1619
 
        textmerge = versionedfile.PlanWeaveMerge(plan, b'<<<<<<< TREE\n',
1620
 
                                                 b'>>>>>>> MERGE-SOURCE\n')
 
1690
        textmerge = versionedfile.PlanWeaveMerge(plan, '<<<<<<< TREE\n',
 
1691
                                                 '>>>>>>> MERGE-SOURCE\n')
1621
1692
        lines, conflicts = textmerge.merge_lines(self.reprocess)
1622
1693
        if conflicts:
1623
1694
            base_lines = textmerge.base_from_plan()
1625
1696
            base_lines = None
1626
1697
        return lines, base_lines
1627
1698
 
1628
 
    def text_merge(self, trans_id, paths):
 
1699
    def text_merge(self, trans_id, paths, file_id):
1629
1700
        """Perform a (weave) text merge for a given file and file-id.
1630
1701
        If conflicts are encountered, .THIS and .OTHER files will be emitted,
1631
1702
        and a conflict will be noted.
1632
1703
        """
1633
1704
        base_path, other_path, this_path = paths
1634
 
        lines, base_lines = self._merged_lines(this_path)
 
1705
        lines, base_lines = self._merged_lines(file_id)
1635
1706
        lines = list(lines)
1636
1707
        # Note we're checking whether the OUTPUT is binary in this case,
1637
1708
        # because we don't want to get into weave merge guts.
1642
1713
            self._raw_conflicts.append(('text conflict', trans_id))
1643
1714
            name = self.tt.final_name(trans_id)
1644
1715
            parent_id = self.tt.final_parent(trans_id)
1645
 
            file_id = self.tt.final_file_id(trans_id)
1646
1716
            file_group = self._dump_conflicts(name, paths, parent_id, file_id,
1647
1717
                                              no_base=False,
1648
1718
                                              base_lines=base_lines)
1653
1723
 
1654
1724
    requires_file_merge_plan = True
1655
1725
 
1656
 
    def _generate_merge_plan(self, this_path, base):
1657
 
        return self.this_tree.plan_file_lca_merge(this_path, self.other_tree,
 
1726
    def _generate_merge_plan(self, file_id, base):
 
1727
        return self.this_tree.plan_file_lca_merge(file_id, self.other_tree,
1658
1728
                                                  base=base)
1659
1729
 
1660
 
 
1661
1730
class Diff3Merger(Merge3Merger):
1662
1731
    """Three-way merger using external diff3 for text merging"""
1663
1732
 
1664
1733
    requires_file_merge_plan = False
1665
1734
 
1666
 
    def dump_file(self, temp_dir, name, tree, path):
 
1735
    def dump_file(self, temp_dir, name, tree, path, file_id=None):
1667
1736
        out_path = osutils.pathjoin(temp_dir, name)
1668
1737
        with open(out_path, "wb") as out_file:
1669
 
            in_file = tree.get_file(path)
 
1738
            in_file = tree.get_file(path, file_id=None)
1670
1739
            for line in in_file:
1671
1740
                out_file.write(line)
1672
1741
        return out_path
1673
1742
 
1674
 
    def text_merge(self, trans_id, paths):
 
1743
    def text_merge(self, trans_id, paths, file_id):
1675
1744
        """Perform a diff3 merge using a specified file-id and trans-id.
1676
1745
        If conflicts are encountered, .BASE, .THIS. and .OTHER conflict files
1677
1746
        will be dumped, and a will be conflict noted.
1681
1750
        temp_dir = osutils.mkdtemp(prefix="bzr-")
1682
1751
        try:
1683
1752
            new_file = osutils.pathjoin(temp_dir, "new")
1684
 
            this = self.dump_file(
1685
 
                temp_dir, "this", self.this_tree, this_path)
1686
 
            base = self.dump_file(
1687
 
                temp_dir, "base", self.base_tree, base_path)
1688
 
            other = self.dump_file(
1689
 
                temp_dir, "other", self.other_tree, other_path)
 
1753
            this = self.dump_file(temp_dir, "this", self.this_tree, this_path, file_id)
 
1754
            base = self.dump_file(temp_dir, "base", self.base_tree, base_path, file_id)
 
1755
            other = self.dump_file(temp_dir, "other", self.other_tree, other_path, file_id)
1690
1756
            status = breezy.patch.diff3(new_file, this, base, other)
1691
1757
            if status not in (0, 1):
1692
1758
                raise errors.BzrError("Unhandled diff3 exit code")
1695
1761
            if status == 1:
1696
1762
                name = self.tt.final_name(trans_id)
1697
1763
                parent_id = self.tt.final_parent(trans_id)
1698
 
                file_id = self.tt.final_file_id(trans_id)
1699
1764
                self._dump_conflicts(name, paths, parent_id, file_id)
1700
1765
                self._raw_conflicts.append(('text conflict', trans_id))
1701
1766
        finally:
1718
1783
    """
1719
1784
 
1720
1785
    def __init__(self, this_tree, other_branch, other_tree, target_subdir,
1721
 
                 source_subpath, other_rev_id=None):
 
1786
            source_subpath, other_rev_id=None):
1722
1787
        """Create a new MergeIntoMerger object.
1723
1788
 
1724
1789
        source_subpath in other_tree will be effectively copied to
1735
1800
        # It is assumed that we are merging a tree that is not in our current
1736
1801
        # ancestry, which means we are using the "EmptyTree" as our basis.
1737
1802
        null_ancestor_tree = this_tree.branch.repository.revision_tree(
1738
 
            _mod_revision.NULL_REVISION)
 
1803
                                _mod_revision.NULL_REVISION)
1739
1804
        super(MergeIntoMerger, self).__init__(
1740
1805
            this_branch=this_tree.branch,
1741
1806
            this_tree=this_tree,
1755
1820
        self.reprocess = False
1756
1821
        self.interesting_files = None
1757
1822
        self.merge_type = _MergeTypeParameterizer(MergeIntoMergeType,
1758
 
                                                  target_subdir=self._target_subdir,
1759
 
                                                  source_subpath=self._source_subpath)
 
1823
              target_subdir=self._target_subdir,
 
1824
              source_subpath=self._source_subpath)
1760
1825
        if self._source_subpath != '':
1761
1826
            # If this isn't a partial merge make sure the revisions will be
1762
1827
            # present.
1763
1828
            self._maybe_fetch(self.other_branch, self.this_branch,
1764
 
                              self.other_basis)
 
1829
                self.other_basis)
1765
1830
 
1766
1831
    def set_pending(self):
1767
1832
        if self._source_subpath != '':
1816
1881
            entries = self._entries_to_incorporate()
1817
1882
            entries = list(entries)
1818
1883
            for num, (entry, parent_id, relpath) in enumerate(entries):
1819
 
                child_pb.update(gettext('Preparing file merge'),
1820
 
                                num, len(entries))
 
1884
                child_pb.update(gettext('Preparing file merge'), num, len(entries))
1821
1885
                parent_trans_id = self.tt.trans_id_file_id(parent_id)
1822
1886
                path = osutils.pathjoin(self._source_subpath, relpath)
1823
1887
                trans_id = transform.new_by_entry(path, self.tt, entry,
1824
 
                                                  parent_trans_id, self.other_tree)
 
1888
                    parent_trans_id, self.other_tree)
1825
1889
        self._finish_computing_transform()
1826
1890
 
1827
1891
    def _entries_to_incorporate(self):
1828
1892
        """Yields pairs of (inventory_entry, new_parent)."""
1829
 
        subdir_id = self.other_tree.path2id(self._source_subpath)
 
1893
        other_inv = self.other_tree.root_inventory
 
1894
        subdir_id = other_inv.path2id(self._source_subpath)
1830
1895
        if subdir_id is None:
1831
1896
            # XXX: The error would be clearer if it gave the URL of the source
1832
1897
            # branch, but we don't have a reference to that here.
1833
1898
            raise PathNotInTree(self._source_subpath, "Source tree")
1834
 
        subdir = next(self.other_tree.iter_entries_by_dir(
1835
 
            specific_files=[self._source_subpath]))[1]
 
1899
        subdir = other_inv.get_entry(subdir_id)
1836
1900
        parent_in_target = osutils.dirname(self._target_subdir)
1837
1901
        target_id = self.this_tree.path2id(parent_in_target)
1838
1902
        if target_id is None:
1840
1904
        name_in_target = osutils.basename(self._target_subdir)
1841
1905
        merge_into_root = subdir.copy()
1842
1906
        merge_into_root.name = name_in_target
1843
 
        try:
1844
 
            self.this_tree.id2path(merge_into_root.file_id)
1845
 
        except errors.NoSuchId:
1846
 
            pass
1847
 
        else:
 
1907
        if self.this_tree.has_id(merge_into_root.file_id):
1848
1908
            # Give the root a new file-id.
1849
1909
            # This can happen fairly easily if the directory we are
1850
1910
            # incorporating is the root, and both trees have 'TREE_ROOT' as
1858
1918
        if subdir.kind != 'directory':
1859
1919
            # No children, so we are done.
1860
1920
            return
1861
 
        for path, entry in self.other_tree.root_inventory.iter_entries_by_dir(subdir_id):
 
1921
        for path, entry in other_inv.iter_entries_by_dir(subdir_id):
1862
1922
            parent_id = entry.parent_id
1863
1923
            if parent_id == subdir.file_id:
1864
1924
                # The root's parent ID has changed, so make sure children of
1992
2052
        for record in self.vf.get_record_stream(keys, 'unordered', True):
1993
2053
            if record.storage_kind == 'absent':
1994
2054
                raise errors.RevisionNotPresent(record.key, self.vf)
1995
 
            result[record.key[-1]] = record.get_bytes_as('lines')
 
2055
            result[record.key[-1]] = osutils.chunks_to_lines(
 
2056
                record.get_bytes_as('chunked'))
1996
2057
        return result
1997
2058
 
1998
2059
    def plan_merge(self):
2031
2092
                else:
2032
2093
                    yield 'killed-a', self.lines_b[b_index]
2033
2094
            # handle common lines
2034
 
            for a_index in range(i, i + n):
 
2095
            for a_index in range(i, i+n):
2035
2096
                yield 'unchanged', self.lines_a[a_index]
2036
 
            last_i = i + n
2037
 
            last_j = j + n
 
2097
            last_i = i+n
 
2098
            last_j = j+n
2038
2099
 
2039
2100
    def _get_matching_blocks(self, left_revision, right_revision):
2040
2101
        """Return a description of which sections of two revisions match.
2096
2157
        for i, j, n in matcher.get_matching_blocks():
2097
2158
            for jj in range(last_j, j):
2098
2159
                yield new_plan[jj]
2099
 
            for jj in range(j, j + n):
 
2160
            for jj in range(j, j+n):
2100
2161
                plan_line = new_plan[jj]
2101
2162
                if plan_line[0] == 'new-b':
2102
2163
                    pass
2240
2301
        filtered_parent_map = {}
2241
2302
        child_map = {}
2242
2303
        tails = []
2243
 
        for key, parent_keys in parent_map.items():
 
2304
        for key, parent_keys in viewitems(parent_map):
2244
2305
            culled_parent_keys = [p for p in parent_keys if p in parent_map]
2245
2306
            if not culled_parent_keys:
2246
2307
                tails.append(key)
2333
2394
        are combined, they are written out in the format described in
2334
2395
        VersionedFile.plan_merge
2335
2396
        """
2336
 
        if self._head_key is not None:  # There was a single head
 
2397
        if self._head_key is not None: # There was a single head
2337
2398
            if self._head_key == self.a_key:
2338
2399
                plan = 'new-a'
2339
2400
            else: