~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/merge.py

  • Committer: Michael Ellerman
  • Date: 2006-02-28 14:45:51 UTC
  • mto: (1558.1.18 Aaron's integration)
  • mto: This revision was merged to the branch mainline in revision 1586.
  • Revision ID: michael@ellerman.id.au-20060228144551-3d9941ecde4a0b0a
Update contrib/pwk for -p1 diffs from bzr

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006 Canonical Ltd
2
 
#
 
1
# Copyright (C) 2005 Canonical Ltd
 
2
 
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
5
5
# the Free Software Foundation; either version 2 of the License, or
6
6
# (at your option) any later version.
7
 
#
 
7
 
8
8
# This program is distributed in the hope that it will be useful,
9
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
11
# GNU General Public License for more details.
12
 
#
 
12
 
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
17
17
 
18
18
import os
19
19
import errno
 
20
from shutil import rmtree
20
21
from tempfile import mkdtemp
21
 
import warnings
22
22
 
 
23
import bzrlib
23
24
from bzrlib.branch import Branch
24
 
from bzrlib.conflicts import ConflictList, Conflict
25
25
from bzrlib.delta import compare_trees
26
26
from bzrlib.errors import (BzrCommandError,
27
27
                           BzrError,
32
32
                           NotBranchError,
33
33
                           NotVersionedError,
34
34
                           UnrelatedBranches,
35
 
                           UnsupportedOperation,
36
35
                           WorkingTreeNotRevision,
37
 
                           BinaryFile,
38
36
                           )
39
37
from bzrlib.merge3 import Merge3
40
38
import bzrlib.osutils
41
 
from bzrlib.osutils import rename, pathjoin, rmtree
42
 
from progress import DummyProgress, ProgressPhase
 
39
from bzrlib.osutils import rename, pathjoin
 
40
from progress import DummyProgress
43
41
from bzrlib.revision import common_ancestor, is_ancestor, NULL_REVISION
44
 
from bzrlib.textfile import check_text_lines
 
42
from bzrlib.symbol_versioning import *
45
43
from bzrlib.trace import mutter, warning, note
46
44
from bzrlib.transform import (TreeTransform, resolve_conflicts, cook_conflicts,
47
 
                              FinalPaths, create_by_entry, unique_add)
48
 
from bzrlib.versionedfile import WeaveMerge
49
 
from bzrlib import ui
 
45
                              conflicts_strings, FinalPaths, create_by_entry,
 
46
                              unique_add)
50
47
 
51
48
# TODO: Report back as changes are merged in
52
49
 
101
98
        self.show_base = False
102
99
        self.reprocess = False
103
100
        self._pb = pb 
104
 
        self.pp = None
105
 
 
106
101
 
107
102
    def revision_tree(self, revision_id):
108
103
        return self.this_branch.repository.revision_tree(revision_id)
119
114
            changes = compare_trees(self.other_tree, other_basis_tree)
120
115
            if changes.has_changed():
121
116
                raise WorkingTreeNotRevision(self.this_tree)
122
 
            other_rev_id = self.other_basis
 
117
            other_rev_id = other_basis
123
118
            self.other_tree = other_basis_tree
124
119
 
125
120
    def file_revisions(self, file_id):
136
131
        trees = (self.this_basis_tree, self.other_tree)
137
132
        return [get_id(tree, file_id) for tree in trees]
138
133
 
139
 
    def check_basis(self, check_clean, require_commits=True):
140
 
        if self.this_basis is None and require_commits is True:
 
134
    def check_basis(self, check_clean):
 
135
        if self.this_basis is None:
141
136
            raise BzrCommandError("This branch has no commits")
142
137
        if check_clean:
143
138
            self.compare_basis()
204
199
        if other_branch.base != self.this_branch.base:
205
200
            self.this_branch.fetch(other_branch, last_revision=self.other_basis)
206
201
 
207
 
    def find_base(self):
208
 
        self.set_base([None, None])
209
 
 
210
202
    def set_base(self, base_revision):
211
203
        mutter("doing merge() with no base_revision specified")
212
204
        if base_revision == [None, None]:
213
205
            try:
214
 
                pb = bzrlib.ui.ui_factory.nested_progress_bar()
215
 
                try:
216
 
                    this_repo = self.this_branch.repository
217
 
                    self.base_rev_id = common_ancestor(self.this_basis, 
218
 
                                                       self.other_basis, 
219
 
                                                       this_repo, pb)
220
 
                finally:
221
 
                    pb.finished()
 
206
                self.base_rev_id = common_ancestor(self.this_basis, 
 
207
                                                   self.other_basis, 
 
208
                                                   self.this_branch.repository,
 
209
                                                   self._pb)
222
210
            except NoCommonAncestor:
223
211
                raise UnrelatedBranches()
224
212
            self.base_tree = _get_revid_tree(self.this_branch, self.base_rev_id,
232
220
                self.base_rev_id = None
233
221
            else:
234
222
                self.base_rev_id = base_branch.get_rev_id(base_revision[1])
235
 
            if self.this_branch.base != base_branch.base:
236
 
                self.this_branch.fetch(base_branch)
 
223
            self.this_branch.fetch(base_branch)
237
224
            self.base_is_ancestor = is_ancestor(self.this_basis, 
238
225
                                                self.base_rev_id,
239
226
                                                self.this_branch)
240
227
 
241
228
    def do_merge(self):
242
229
        kwargs = {'working_tree':self.this_tree, 'this_tree': self.this_tree, 
243
 
                  'other_tree': self.other_tree, 
244
 
                  'interesting_ids': self.interesting_ids,
245
 
                  'pp': self.pp}
 
230
                  'other_tree': self.other_tree}
246
231
        if self.merge_type.requires_base:
247
232
            kwargs['base_tree'] = self.base_tree
248
233
        if self.merge_type.supports_reprocess:
249
234
            kwargs['reprocess'] = self.reprocess
250
235
        elif self.reprocess:
251
 
            raise BzrError("Conflict reduction is not supported for merge"
252
 
                                  " type %s." % self.merge_type)
 
236
            raise BzrError("Reprocess is not supported for this merge"
 
237
                                  " type. %s" % merge_type)
253
238
        if self.merge_type.supports_show_base:
254
239
            kwargs['show_base'] = self.show_base
255
240
        elif self.show_base:
286
271
        for file_id in old_entries:
287
272
            entry = old_entries[file_id]
288
273
            path = id2path(file_id)
289
 
            if file_id in self.base_tree.inventory:
290
 
                executable = getattr(self.base_tree.inventory[file_id], 'executable', False)
291
 
            else:
292
 
                executable = getattr(entry, 'executable', False)
293
274
            new_inventory[file_id] = (path, file_id, entry.parent_id, 
294
 
                                      entry.kind, executable)
295
 
                                      
 
275
                                      entry.kind)
296
276
            by_path[path] = file_id
297
277
        
298
278
        deletions = 0
315
295
                parent = by_path[os.path.dirname(path)]
316
296
            abspath = pathjoin(self.this_tree.basedir, path)
317
297
            kind = bzrlib.osutils.file_kind(abspath)
318
 
            if file_id in self.base_tree.inventory:
319
 
                executable = getattr(self.base_tree.inventory[file_id], 'executable', False)
320
 
            else:
321
 
                executable = False
322
 
            new_inventory[file_id] = (path, file_id, parent, kind, executable)
 
298
            new_inventory[file_id] = (path, file_id, parent, kind)
323
299
            by_path[path] = file_id 
324
300
 
325
301
        # Get a list in insertion order
342
318
    history_based = False
343
319
 
344
320
    def __init__(self, working_tree, this_tree, base_tree, other_tree, 
345
 
                 interesting_ids=None, reprocess=False, show_base=False,
346
 
                 pb=DummyProgress(), pp=None):
 
321
                 reprocess=False, show_base=False, pb=DummyProgress()):
347
322
        """Initialize the merger object and perform the merge."""
348
323
        object.__init__(self)
349
324
        self.this_tree = working_tree
354
329
        self.reprocess = reprocess
355
330
        self.show_base = show_base
356
331
        self.pb = pb
357
 
        self.pp = pp
358
 
        if self.pp is None:
359
 
            self.pp = ProgressPhase("Merge phase", 3, self.pb)
360
332
 
361
 
        if interesting_ids is not None:
362
 
            all_ids = interesting_ids
363
 
        else:
364
 
            all_ids = set(base_tree)
365
 
            all_ids.update(other_tree)
366
 
        working_tree.lock_write()
 
333
        all_ids = set(base_tree)
 
334
        all_ids.update(other_tree)
367
335
        self.tt = TreeTransform(working_tree, self.pb)
368
336
        try:
369
 
            self.pp.next_phase()
370
 
            child_pb = ui.ui_factory.nested_progress_bar()
371
 
            try:
372
 
                for num, file_id in enumerate(all_ids):
373
 
                    child_pb.update('Preparing file merge', num, len(all_ids))
374
 
                    self.merge_names(file_id)
375
 
                    file_status = self.merge_contents(file_id)
376
 
                    self.merge_executable(file_id, file_status)
377
 
            finally:
378
 
                child_pb.finished()
 
337
            for num, file_id in enumerate(all_ids):
 
338
                self.pb.update('Preparing file merge', num+1, len(all_ids))
 
339
                self.merge_names(file_id)
 
340
                file_status = self.merge_contents(file_id)
 
341
                self.merge_executable(file_id, file_status)
 
342
            self.pb.clear()
379
343
                
380
 
            self.pp.next_phase()
381
 
            child_pb = ui.ui_factory.nested_progress_bar()
382
 
            try:
383
 
                fs_conflicts = resolve_conflicts(self.tt, child_pb)
384
 
            finally:
385
 
                child_pb.finished()
 
344
            fs_conflicts = resolve_conflicts(self.tt, self.pb)
386
345
            self.cook_conflicts(fs_conflicts)
387
 
            for conflict in self.cooked_conflicts:
388
 
                warning(conflict)
389
 
            self.pp.next_phase()
390
 
            results = self.tt.apply()
391
 
            self.write_modified(results)
 
346
            for line in conflicts_strings(self.cooked_conflicts):
 
347
                warning(line)
 
348
            self.tt.apply()
 
349
        finally:
392
350
            try:
393
 
                working_tree.add_conflicts(self.cooked_conflicts)
394
 
            except UnsupportedOperation:
 
351
                self.tt.finalize()
 
352
            except:
395
353
                pass
396
 
        finally:
397
 
            self.tt.finalize()
398
 
            working_tree.unlock()
399
 
            self.pb.clear()
400
 
 
401
 
    def write_modified(self, results):
402
 
        modified_hashes = {}
403
 
        for path in results.modified_paths:
404
 
            file_id = self.this_tree.path2id(self.this_tree.relpath(path))
405
 
            if file_id is None:
406
 
                continue
407
 
            hash = self.this_tree.get_file_sha1(file_id)
408
 
            if hash is None:
409
 
                continue
410
 
            modified_hashes[file_id] = hash
411
 
        self.this_tree.set_merge_modified(modified_hashes)
412
 
 
 
354
       
413
355
    @staticmethod
414
356
    def parent(entry, file_id):
415
357
        """Determine the parent for a file_id (used as a key method)"""
527
469
            else:
528
470
                contents = None
529
471
            return kind, contents
530
 
 
531
 
        def contents_conflict():
532
 
            trans_id = self.tt.trans_id_file_id(file_id)
533
 
            name = self.tt.final_name(trans_id)
534
 
            parent_id = self.tt.final_parent(trans_id)
535
 
            if file_id in self.this_tree.inventory:
536
 
                self.tt.unversion_file(trans_id)
537
 
                self.tt.delete_contents(trans_id)
538
 
            file_group = self._dump_conflicts(name, parent_id, file_id, 
539
 
                                              set_version=True)
540
 
            self._raw_conflicts.append(('contents conflict', file_group))
541
 
 
542
472
        # See SPOT run.  run, SPOT, run.
543
473
        # So we're not QUITE repeating ourselves; we do tricky things with
544
474
        # file kind...
575
505
                # THIS and OTHER are both files, so text merge.  Either
576
506
                # BASE is a file, or both converted to files, so at least we
577
507
                # have agreement that output should be a file.
578
 
                try:
579
 
                    self.text_merge(file_id, trans_id)
580
 
                except BinaryFile:
581
 
                    return contents_conflict()
582
508
                if file_id not in self.this_tree.inventory:
583
509
                    self.tt.version_file(file_id, trans_id)
 
510
                self.text_merge(file_id, trans_id)
584
511
                try:
585
512
                    self.tt.tree_kind(trans_id)
586
513
                    self.tt.delete_contents(trans_id)
589
516
                return "modified"
590
517
            else:
591
518
                # Scalar conflict, can't text merge.  Dump conflicts
592
 
                return contents_conflict()
 
519
                trans_id = self.tt.trans_id_file_id(file_id)
 
520
                name = self.tt.final_name(trans_id)
 
521
                parent_id = self.tt.final_parent(trans_id)
 
522
                if file_id in self.this_tree.inventory:
 
523
                    self.tt.unversion_file(trans_id)
 
524
                    self.tt.delete_contents(trans_id)
 
525
                file_group = self._dump_conflicts(name, parent_id, file_id, 
 
526
                                                  set_version=True)
 
527
                self._raw_conflicts.append(('contents conflict', file_group))
593
528
 
594
529
    def get_lines(self, tree, file_id):
595
530
        """Return the lines in a file, or an empty list."""
690
625
        if winner == "conflict":
691
626
        # There must be a None in here, if we have a conflict, but we
692
627
        # need executability since file status was not deleted.
693
 
            if self.executable(self.other_tree, file_id) is None:
 
628
            if self.other_tree.is_executable(file_id) is None:
694
629
                winner = "this"
695
630
            else:
696
631
                winner = "other"
714
649
 
715
650
    def cook_conflicts(self, fs_conflicts):
716
651
        """Convert all conflicts into a form that doesn't depend on trans_id"""
717
 
        from conflicts import Conflict
718
652
        name_conflicts = {}
719
653
        self.cooked_conflicts.extend(cook_conflicts(fs_conflicts, self.tt))
720
654
        fp = FinalPaths(self.tt)
737
671
                    if path.endswith(suffix):
738
672
                        path = path[:-len(suffix)]
739
673
                        break
740
 
                c = Conflict.factory(conflict_type, path=path, file_id=file_id)
741
 
                self.cooked_conflicts.append(c)
 
674
                self.cooked_conflicts.append((conflict_type, file_id, path))
742
675
            if conflict_type == 'text conflict':
743
676
                trans_id = conflict[1]
744
677
                path = fp.get_path(trans_id)
745
678
                file_id = self.tt.final_file_id(trans_id)
746
 
                c = Conflict.factory(conflict_type, path=path, file_id=file_id)
747
 
                self.cooked_conflicts.append(c)
 
679
                self.cooked_conflicts.append((conflict_type, file_id, path))
748
680
 
749
681
        for trans_id, conflicts in name_conflicts.iteritems():
750
682
            try:
766
698
            else:
767
699
                this_path = "<deleted>"
768
700
            file_id = self.tt.final_file_id(trans_id)
769
 
            c = Conflict.factory('path conflict', path=this_path,
770
 
                                 conflict_path=other_path, file_id=file_id)
771
 
            self.cooked_conflicts.append(c)
772
 
        self.cooked_conflicts.sort(key=Conflict.sort_key)
 
701
            self.cooked_conflicts.append(('path conflict', file_id, this_path, 
 
702
                                         other_path))
773
703
 
774
704
 
775
705
class WeaveMerger(Merge3Merger):
776
706
    """Three-way tree merger, text weave merger."""
777
 
    supports_reprocess = True
 
707
    supports_reprocess = False
778
708
    supports_show_base = False
779
709
 
780
710
    def __init__(self, working_tree, this_tree, base_tree, other_tree, 
781
 
                 interesting_ids=None, pb=DummyProgress(), pp=None,
782
 
                 reprocess=False):
 
711
                 pb=DummyProgress()):
783
712
        self.this_revision_tree = self._get_revision_tree(this_tree)
784
713
        self.other_revision_tree = self._get_revision_tree(other_tree)
785
714
        super(WeaveMerger, self).__init__(working_tree, this_tree, 
786
 
                                          base_tree, other_tree, 
787
 
                                          interesting_ids=interesting_ids, 
788
 
                                          pb=pb, pp=pp, reprocess=reprocess)
 
715
                                          base_tree, other_tree, pb=pb)
789
716
 
790
717
    def _get_revision_tree(self, tree):
791
 
        """Return a revision tree related to this tree.
 
718
        """Return a revision tree releated to this tree.
792
719
        If the tree is a WorkingTree, the basis will be returned.
793
720
        """
794
721
        if getattr(tree, 'get_weave', False) is False:
815
742
        this_revision_id = self.this_revision_tree.inventory[file_id].revision
816
743
        other_revision_id = \
817
744
            self.other_revision_tree.inventory[file_id].revision
818
 
        wm = WeaveMerge(weave, this_revision_id, other_revision_id, 
819
 
                        '<<<<<<< TREE\n', '>>>>>>> MERGE-SOURCE\n')
820
 
        return wm.merge_lines(self.reprocess)
 
745
        this_i = weave.lookup(this_revision_id)
 
746
        other_i = weave.lookup(other_revision_id)
 
747
        plan =  weave.plan_merge(this_i, other_i)
 
748
        return weave.weave_merge(plan, '<<<<<<< TREE\n', 
 
749
                                       '>>>>>>> MERGE-SOURCE\n')
821
750
 
822
751
    def text_merge(self, file_id, trans_id):
823
752
        """Perform a (weave) text merge for a given file and file-id.
825
754
        and a conflict will be noted.
826
755
        """
827
756
        self._check_file(file_id)
828
 
        lines, conflicts = self._merged_lines(file_id)
829
 
        lines = list(lines)
830
 
        # Note we're checking whether the OUTPUT is binary in this case, 
831
 
        # because we don't want to get into weave merge guts.
832
 
        check_text_lines(lines)
 
757
        lines = self._merged_lines(file_id)
 
758
        conflicts = '<<<<<<< TREE\n' in lines
833
759
        self.tt.create_file(lines, trans_id)
834
760
        if conflicts:
835
761
            self._raw_conflicts.append(('text conflict', trans_id))
842
768
 
843
769
class Diff3Merger(Merge3Merger):
844
770
    """Three-way merger using external diff3 for text merging"""
845
 
 
846
771
    def dump_file(self, temp_dir, name, tree, file_id):
847
772
        out_path = pathjoin(temp_dir, name)
848
 
        out_file = open(out_path, "wb")
849
 
        try:
850
 
            in_file = tree.get_file(file_id)
851
 
            for line in in_file:
852
 
                out_file.write(line)
853
 
        finally:
854
 
            out_file.close()
 
773
        out_file = file(out_path, "wb")
 
774
        in_file = tree.get_file(file_id)
 
775
        for line in in_file:
 
776
            out_file.write(line)
855
777
        return out_path
856
778
 
857
779
    def text_merge(self, file_id, trans_id):
869
791
            status = bzrlib.patch.diff3(new_file, this, base, other)
870
792
            if status not in (0, 1):
871
793
                raise BzrError("Unhandled diff3 exit code")
872
 
            f = open(new_file, 'rb')
873
 
            try:
874
 
                self.tt.create_file(f, trans_id)
875
 
            finally:
876
 
                f.close()
 
794
            self.tt.create_file(file(new_file, "rb"), trans_id)
877
795
            if status == 1:
878
796
                name = self.tt.final_name(trans_id)
879
797
                parent_id = self.tt.final_parent(trans_id)
900
818
                     branch.get_revision_tree(base_revision))'
901
819
        """
902
820
    if this_tree is None:
903
 
        warnings.warn("bzrlib.merge.merge_inner requires a this_tree parameter as of "
 
821
        warn("bzrlib.merge.merge_inner requires a this_tree parameter as of "
904
822
             "bzrlib version 0.8.",
905
823
             DeprecationWarning,
906
824
             stacklevel=2)
910
828
    merger.backup_files = backup_files
911
829
    merger.merge_type = merge_type
912
830
    merger.interesting_ids = interesting_ids
913
 
    merger.ignore_zero = ignore_zero
914
831
    if interesting_files:
915
832
        assert not interesting_ids, ('Only supply interesting_ids'
916
833
                                     ' or interesting_files')
926
843
                     "diff3": (Diff3Merger,  "Merge using external diff3"),
927
844
                     'weave': (WeaveMerger, "Weave-based merge")
928
845
              }
929
 
 
930
 
 
931
 
def merge_type_help():
932
 
    templ = '%s%%7s: %%s' % (' '*12)
933
 
    lines = [templ % (f[0], f[1][1]) for f in merge_types.iteritems()]
934
 
    return '\n'.join(lines)