/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 bzrlib/merge.py

  • Committer: Robert Collins
  • Date: 2007-03-05 03:43:56 UTC
  • mfrom: (2312 +trunk)
  • mto: (2255.11.6 dirstate)
  • mto: This revision was merged to the branch mainline in revision 2322.
  • Revision ID: robertc@robertcollins.net-20070305034356-og43j35eg62m952f
Merge bzr.dev.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005, 2006 Canonical Ltd
 
2
#
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 of the License, or
 
6
# (at your option) any later version.
 
7
#
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
#
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
16
 
 
17
 
 
18
import os
 
19
import errno
 
20
import warnings
 
21
 
 
22
from bzrlib import (
 
23
    osutils,
 
24
    registry,
 
25
    )
 
26
from bzrlib.branch import Branch
 
27
from bzrlib.conflicts import ConflictList, Conflict
 
28
from bzrlib.errors import (BzrCommandError,
 
29
                           BzrError,
 
30
                           NoCommonAncestor,
 
31
                           NoCommits,
 
32
                           NoSuchRevision,
 
33
                           NoSuchFile,
 
34
                           NotBranchError,
 
35
                           NotVersionedError,
 
36
                           UnrelatedBranches,
 
37
                           UnsupportedOperation,
 
38
                           WorkingTreeNotRevision,
 
39
                           BinaryFile,
 
40
                           )
 
41
from bzrlib.merge3 import Merge3
 
42
from bzrlib.osutils import rename, pathjoin
 
43
from progress import DummyProgress, ProgressPhase
 
44
from bzrlib.revision import common_ancestor, is_ancestor, NULL_REVISION
 
45
from bzrlib.textfile import check_text_lines
 
46
from bzrlib.trace import mutter, warning, note
 
47
from bzrlib.transform import (TreeTransform, resolve_conflicts, cook_conflicts,
 
48
                              FinalPaths, create_by_entry, unique_add,
 
49
                              ROOT_PARENT)
 
50
from bzrlib.versionedfile import WeaveMerge
 
51
from bzrlib import ui
 
52
 
 
53
# TODO: Report back as changes are merged in
 
54
 
 
55
def _get_tree(treespec, local_branch=None):
 
56
    from bzrlib import workingtree
 
57
    location, revno = treespec
 
58
    if revno is None:
 
59
        tree = workingtree.WorkingTree.open_containing(location)[0]
 
60
        return tree.branch, tree
 
61
    branch = Branch.open_containing(location)[0]
 
62
    if revno == -1:
 
63
        revision = branch.last_revision()
 
64
    else:
 
65
        revision = branch.get_rev_id(revno)
 
66
        if revision is None:
 
67
            revision = NULL_REVISION
 
68
    return branch, _get_revid_tree(branch, revision, local_branch)
 
69
 
 
70
 
 
71
def _get_revid_tree(branch, revision, local_branch):
 
72
    if revision is None:
 
73
        base_tree = branch.bzrdir.open_workingtree()
 
74
    else:
 
75
        if local_branch is not None:
 
76
            if local_branch.base != branch.base:
 
77
                local_branch.fetch(branch, revision)
 
78
            base_tree = local_branch.repository.revision_tree(revision)
 
79
        else:
 
80
            base_tree = branch.repository.revision_tree(revision)
 
81
    return base_tree
 
82
 
 
83
 
 
84
def transform_tree(from_tree, to_tree, interesting_ids=None):
 
85
    merge_inner(from_tree.branch, to_tree, from_tree, ignore_zero=True,
 
86
                interesting_ids=interesting_ids, this_tree=from_tree)
 
87
 
 
88
 
 
89
class Merger(object):
 
90
    def __init__(self, this_branch, other_tree=None, base_tree=None,
 
91
                 this_tree=None, pb=DummyProgress(), change_reporter=None):
 
92
        object.__init__(self)
 
93
        assert this_tree is not None, "this_tree is required"
 
94
        self.this_branch = this_branch
 
95
        self.this_basis = this_branch.last_revision()
 
96
        self.this_rev_id = None
 
97
        self.this_tree = this_tree
 
98
        self.this_revision_tree = None
 
99
        self.this_basis_tree = None
 
100
        self.other_tree = other_tree
 
101
        self.base_tree = base_tree
 
102
        self.ignore_zero = False
 
103
        self.backup_files = False
 
104
        self.interesting_ids = None
 
105
        self.show_base = False
 
106
        self.reprocess = False
 
107
        self._pb = pb
 
108
        self.pp = None
 
109
        self.change_reporter = change_reporter
 
110
 
 
111
    def revision_tree(self, revision_id):
 
112
        return self.this_branch.repository.revision_tree(revision_id)
 
113
 
 
114
    def ensure_revision_trees(self):
 
115
        if self.this_revision_tree is None:
 
116
            self.this_basis_tree = self.this_branch.repository.revision_tree(
 
117
                self.this_basis)
 
118
            if self.this_basis == self.this_rev_id:
 
119
                self.this_revision_tree = self.this_basis_tree
 
120
 
 
121
        if self.other_rev_id is None:
 
122
            other_basis_tree = self.revision_tree(self.other_basis)
 
123
            changes = other_basis_tree.changes_from(self.other_tree)
 
124
            if changes.has_changed():
 
125
                raise WorkingTreeNotRevision(self.this_tree)
 
126
            other_rev_id = self.other_basis
 
127
            self.other_tree = other_basis_tree
 
128
 
 
129
    def file_revisions(self, file_id):
 
130
        self.ensure_revision_trees()
 
131
        def get_id(tree, file_id):
 
132
            revision_id = tree.inventory[file_id].revision
 
133
            assert revision_id is not None
 
134
            return revision_id
 
135
        if self.this_rev_id is None:
 
136
            if self.this_basis_tree.get_file_sha1(file_id) != \
 
137
                self.this_tree.get_file_sha1(file_id):
 
138
                raise WorkingTreeNotRevision(self.this_tree)
 
139
 
 
140
        trees = (self.this_basis_tree, self.other_tree)
 
141
        return [get_id(tree, file_id) for tree in trees]
 
142
 
 
143
    def check_basis(self, check_clean, require_commits=True):
 
144
        if self.this_basis is None and require_commits is True:
 
145
            raise BzrCommandError("This branch has no commits."
 
146
                                  " (perhaps you would prefer 'bzr pull')")
 
147
        if check_clean:
 
148
            self.compare_basis()
 
149
            if self.this_basis != self.this_rev_id:
 
150
                raise BzrCommandError("Working tree has uncommitted changes.")
 
151
 
 
152
    def compare_basis(self):
 
153
        changes = self.this_tree.changes_from(self.this_tree.basis_tree())
 
154
        if not changes.has_changed():
 
155
            self.this_rev_id = self.this_basis
 
156
 
 
157
    def set_interesting_files(self, file_list):
 
158
        try:
 
159
            self._set_interesting_files(file_list)
 
160
        except NotVersionedError, e:
 
161
            raise BzrCommandError("%s is not a source file in any"
 
162
                                      " tree." % e.path)
 
163
 
 
164
    def _set_interesting_files(self, file_list):
 
165
        """Set the list of interesting ids from a list of files."""
 
166
        if file_list is None:
 
167
            self.interesting_ids = None
 
168
            return
 
169
 
 
170
        interesting_ids = set()
 
171
        for path in file_list:
 
172
            found_id = False
 
173
            # TODO: jam 20070226 The trees are not locked at this time,
 
174
            #       wouldn't it make merge faster if it locks everything in the
 
175
            #       beginning? It locks at do_merge time, but this happens
 
176
            #       before that.
 
177
            for tree in (self.this_tree, self.base_tree, self.other_tree):
 
178
                file_id = tree.path2id(path)
 
179
                if file_id is not None:
 
180
                    interesting_ids.add(file_id)
 
181
                    found_id = True
 
182
            if not found_id:
 
183
                raise NotVersionedError(path=path)
 
184
        self.interesting_ids = interesting_ids
 
185
 
 
186
    def set_pending(self):
 
187
        if not self.base_is_ancestor:
 
188
            return
 
189
        if self.other_rev_id is None:
 
190
            return
 
191
        ancestry = self.this_branch.repository.get_ancestry(self.this_basis)
 
192
        if self.other_rev_id in ancestry:
 
193
            return
 
194
        self.this_tree.add_parent_tree((self.other_rev_id, self.other_tree))
 
195
 
 
196
    def set_other(self, other_revision):
 
197
        """Set the revision and tree to merge from.
 
198
 
 
199
        This sets the other_tree, other_rev_id, other_basis attributes.
 
200
 
 
201
        :param other_revision: The [path, revision] list to merge from.
 
202
        """
 
203
        other_branch, self.other_tree = _get_tree(other_revision,
 
204
                                                  self.this_branch)
 
205
        if other_revision[1] == -1:
 
206
            self.other_rev_id = other_branch.last_revision()
 
207
            if self.other_rev_id is None:
 
208
                raise NoCommits(other_branch)
 
209
            self.other_basis = self.other_rev_id
 
210
        elif other_revision[1] is not None:
 
211
            self.other_rev_id = other_branch.get_rev_id(other_revision[1])
 
212
            self.other_basis = self.other_rev_id
 
213
        else:
 
214
            self.other_rev_id = None
 
215
            self.other_basis = other_branch.last_revision()
 
216
            if self.other_basis is None:
 
217
                raise NoCommits(other_branch)
 
218
        if other_branch.base != self.this_branch.base:
 
219
            self.this_branch.fetch(other_branch, last_revision=self.other_basis)
 
220
 
 
221
    def find_base(self):
 
222
        self.set_base([None, None])
 
223
 
 
224
    def set_base(self, base_revision):
 
225
        """Set the base revision to use for the merge.
 
226
 
 
227
        :param base_revision: A 2-list containing a path and revision number.
 
228
        """
 
229
        mutter("doing merge() with no base_revision specified")
 
230
        if base_revision == [None, None]:
 
231
            try:
 
232
                pb = ui.ui_factory.nested_progress_bar()
 
233
                try:
 
234
                    this_repo = self.this_branch.repository
 
235
                    self.base_rev_id = common_ancestor(self.this_basis, 
 
236
                                                       self.other_basis, 
 
237
                                                       this_repo, pb)
 
238
                finally:
 
239
                    pb.finished()
 
240
            except NoCommonAncestor:
 
241
                raise UnrelatedBranches()
 
242
            self.base_tree = _get_revid_tree(self.this_branch, self.base_rev_id,
 
243
                                            None)
 
244
            self.base_is_ancestor = True
 
245
        else:
 
246
            base_branch, self.base_tree = _get_tree(base_revision)
 
247
            if base_revision[1] == -1:
 
248
                self.base_rev_id = base_branch.last_revision()
 
249
            elif base_revision[1] is None:
 
250
                self.base_rev_id = None
 
251
            else:
 
252
                self.base_rev_id = base_branch.get_rev_id(base_revision[1])
 
253
            if self.this_branch.base != base_branch.base:
 
254
                self.this_branch.fetch(base_branch)
 
255
            self.base_is_ancestor = is_ancestor(self.this_basis, 
 
256
                                                self.base_rev_id,
 
257
                                                self.this_branch)
 
258
 
 
259
    def do_merge(self):
 
260
        kwargs = {'working_tree':self.this_tree, 'this_tree': self.this_tree,
 
261
                  'other_tree': self.other_tree,
 
262
                  'interesting_ids': self.interesting_ids,
 
263
                  'pp': self.pp}
 
264
        if self.merge_type.requires_base:
 
265
            kwargs['base_tree'] = self.base_tree
 
266
        if self.merge_type.supports_reprocess:
 
267
            kwargs['reprocess'] = self.reprocess
 
268
        elif self.reprocess:
 
269
            raise BzrError("Conflict reduction is not supported for merge"
 
270
                                  " type %s." % self.merge_type)
 
271
        if self.merge_type.supports_show_base:
 
272
            kwargs['show_base'] = self.show_base
 
273
        elif self.show_base:
 
274
            raise BzrError("Showing base is not supported for this"
 
275
                                  " merge type. %s" % self.merge_type)
 
276
        self.this_tree.lock_tree_write()
 
277
        if self.base_tree is not None:
 
278
            self.base_tree.lock_read()
 
279
        if self.other_tree is not None:
 
280
            self.other_tree.lock_read()
 
281
        try:
 
282
            merge = self.merge_type(pb=self._pb,
 
283
                                    change_reporter=self.change_reporter,
 
284
                                    **kwargs)
 
285
        finally:
 
286
            if self.other_tree is not None:
 
287
                self.other_tree.unlock()
 
288
            if self.base_tree is not None:
 
289
                self.base_tree.unlock()
 
290
            self.this_tree.unlock()
 
291
        if len(merge.cooked_conflicts) == 0:
 
292
            if not self.ignore_zero:
 
293
                note("All changes applied successfully.")
 
294
        else:
 
295
            note("%d conflicts encountered." % len(merge.cooked_conflicts))
 
296
 
 
297
        return len(merge.cooked_conflicts)
 
298
 
 
299
    def regen_inventory(self, new_entries):
 
300
        old_entries = self.this_tree.read_working_inventory()
 
301
        new_inventory = {}
 
302
        by_path = {}
 
303
        new_entries_map = {} 
 
304
        for path, file_id in new_entries:
 
305
            if path is None:
 
306
                continue
 
307
            new_entries_map[file_id] = path
 
308
 
 
309
        def id2path(file_id):
 
310
            path = new_entries_map.get(file_id)
 
311
            if path is not None:
 
312
                return path
 
313
            entry = old_entries[file_id]
 
314
            if entry.parent_id is None:
 
315
                return entry.name
 
316
            return pathjoin(id2path(entry.parent_id), entry.name)
 
317
            
 
318
        for file_id in old_entries:
 
319
            entry = old_entries[file_id]
 
320
            path = id2path(file_id)
 
321
            if file_id in self.base_tree.inventory:
 
322
                executable = getattr(self.base_tree.inventory[file_id], 'executable', False)
 
323
            else:
 
324
                executable = getattr(entry, 'executable', False)
 
325
            new_inventory[file_id] = (path, file_id, entry.parent_id, 
 
326
                                      entry.kind, executable)
 
327
                                      
 
328
            by_path[path] = file_id
 
329
        
 
330
        deletions = 0
 
331
        insertions = 0
 
332
        new_path_list = []
 
333
        for path, file_id in new_entries:
 
334
            if path is None:
 
335
                del new_inventory[file_id]
 
336
                deletions += 1
 
337
            else:
 
338
                new_path_list.append((path, file_id))
 
339
                if file_id not in old_entries:
 
340
                    insertions += 1
 
341
        # Ensure no file is added before its parent
 
342
        new_path_list.sort()
 
343
        for path, file_id in new_path_list:
 
344
            if path == '':
 
345
                parent = None
 
346
            else:
 
347
                parent = by_path[os.path.dirname(path)]
 
348
            abspath = pathjoin(self.this_tree.basedir, path)
 
349
            kind = osutils.file_kind(abspath)
 
350
            if file_id in self.base_tree.inventory:
 
351
                executable = getattr(self.base_tree.inventory[file_id], 'executable', False)
 
352
            else:
 
353
                executable = False
 
354
            new_inventory[file_id] = (path, file_id, parent, kind, executable)
 
355
            by_path[path] = file_id 
 
356
 
 
357
        # Get a list in insertion order
 
358
        new_inventory_list = new_inventory.values()
 
359
        mutter ("""Inventory regeneration:
 
360
    old length: %i insertions: %i deletions: %i new_length: %i"""\
 
361
            % (len(old_entries), insertions, deletions, 
 
362
               len(new_inventory_list)))
 
363
        assert len(new_inventory_list) == len(old_entries) + insertions\
 
364
            - deletions
 
365
        new_inventory_list.sort()
 
366
        return new_inventory_list
 
367
 
 
368
 
 
369
class Merge3Merger(object):
 
370
    """Three-way merger that uses the merge3 text merger"""
 
371
    requires_base = True
 
372
    supports_reprocess = True
 
373
    supports_show_base = True
 
374
    history_based = False
 
375
 
 
376
    def __init__(self, working_tree, this_tree, base_tree, other_tree, 
 
377
                 interesting_ids=None, reprocess=False, show_base=False,
 
378
                 pb=DummyProgress(), pp=None, change_reporter=None):
 
379
        """Initialize the merger object and perform the merge."""
 
380
        object.__init__(self)
 
381
        self.this_tree = working_tree
 
382
        self.this_tree.lock_tree_write()
 
383
        self.base_tree = base_tree
 
384
        self.base_tree.lock_read()
 
385
        self.other_tree = other_tree
 
386
        self.other_tree.lock_read()
 
387
        self._raw_conflicts = []
 
388
        self.cooked_conflicts = []
 
389
        self.reprocess = reprocess
 
390
        self.show_base = show_base
 
391
        self.pb = pb
 
392
        self.pp = pp
 
393
        self.change_reporter = change_reporter
 
394
        if self.pp is None:
 
395
            self.pp = ProgressPhase("Merge phase", 3, self.pb)
 
396
 
 
397
        if interesting_ids is not None:
 
398
            all_ids = interesting_ids
 
399
        else:
 
400
            all_ids = set(base_tree)
 
401
            all_ids.update(other_tree)
 
402
        self.tt = TreeTransform(working_tree, self.pb)
 
403
        try:
 
404
            self.pp.next_phase()
 
405
            child_pb = ui.ui_factory.nested_progress_bar()
 
406
            try:
 
407
                for num, file_id in enumerate(all_ids):
 
408
                    child_pb.update('Preparing file merge', num, len(all_ids))
 
409
                    self.merge_names(file_id)
 
410
                    file_status = self.merge_contents(file_id)
 
411
                    self.merge_executable(file_id, file_status)
 
412
            finally:
 
413
                child_pb.finished()
 
414
            self.fix_root()
 
415
            self.pp.next_phase()
 
416
            child_pb = ui.ui_factory.nested_progress_bar()
 
417
            try:
 
418
                fs_conflicts = resolve_conflicts(self.tt, child_pb)
 
419
            finally:
 
420
                child_pb.finished()
 
421
            if change_reporter is not None:
 
422
                from bzrlib import delta
 
423
                delta.report_changes(self.tt._iter_changes(), change_reporter)
 
424
            self.cook_conflicts(fs_conflicts)
 
425
            for conflict in self.cooked_conflicts:
 
426
                warning(conflict)
 
427
            self.pp.next_phase()
 
428
            results = self.tt.apply()
 
429
            self.write_modified(results)
 
430
            try:
 
431
                working_tree.add_conflicts(self.cooked_conflicts)
 
432
            except UnsupportedOperation:
 
433
                pass
 
434
        finally:
 
435
            self.tt.finalize()
 
436
            self.other_tree.unlock()
 
437
            self.base_tree.unlock()
 
438
            self.this_tree.unlock()
 
439
            self.pb.clear()
 
440
 
 
441
    def fix_root(self):
 
442
        try:
 
443
            self.tt.final_kind(self.tt.root)
 
444
        except NoSuchFile:
 
445
            self.tt.cancel_deletion(self.tt.root)
 
446
        if self.tt.final_file_id(self.tt.root) is None:
 
447
            self.tt.version_file(self.tt.tree_file_id(self.tt.root), 
 
448
                                 self.tt.root)
 
449
        if self.other_tree.inventory.root is None:
 
450
            return
 
451
        other_root_file_id = self.other_tree.inventory.root.file_id
 
452
        other_root = self.tt.trans_id_file_id(other_root_file_id)
 
453
        if other_root == self.tt.root:
 
454
            return
 
455
        try:
 
456
            self.tt.final_kind(other_root)
 
457
        except NoSuchFile:
 
458
            return
 
459
        self.reparent_children(self.other_tree.inventory.root, self.tt.root)
 
460
        self.tt.cancel_creation(other_root)
 
461
        self.tt.cancel_versioning(other_root)
 
462
 
 
463
    def reparent_children(self, ie, target):
 
464
        for thing, child in ie.children.iteritems():
 
465
            trans_id = self.tt.trans_id_file_id(child.file_id)
 
466
            self.tt.adjust_path(self.tt.final_name(trans_id), target, trans_id)
 
467
 
 
468
    def write_modified(self, results):
 
469
        modified_hashes = {}
 
470
        for path in results.modified_paths:
 
471
            file_id = self.this_tree.path2id(self.this_tree.relpath(path))
 
472
            if file_id is None:
 
473
                continue
 
474
            hash = self.this_tree.get_file_sha1(file_id)
 
475
            if hash is None:
 
476
                continue
 
477
            modified_hashes[file_id] = hash
 
478
        self.this_tree.set_merge_modified(modified_hashes)
 
479
 
 
480
    @staticmethod
 
481
    def parent(entry, file_id):
 
482
        """Determine the parent for a file_id (used as a key method)"""
 
483
        if entry is None:
 
484
            return None
 
485
        return entry.parent_id
 
486
 
 
487
    @staticmethod
 
488
    def name(entry, file_id):
 
489
        """Determine the name for a file_id (used as a key method)"""
 
490
        if entry is None:
 
491
            return None
 
492
        return entry.name
 
493
    
 
494
    @staticmethod
 
495
    def contents_sha1(tree, file_id):
 
496
        """Determine the sha1 of the file contents (used as a key method)."""
 
497
        if file_id not in tree:
 
498
            return None
 
499
        return tree.get_file_sha1(file_id)
 
500
 
 
501
    @staticmethod
 
502
    def executable(tree, file_id):
 
503
        """Determine the executability of a file-id (used as a key method)."""
 
504
        if file_id not in tree:
 
505
            return None
 
506
        if tree.kind(file_id) != "file":
 
507
            return False
 
508
        return tree.is_executable(file_id)
 
509
 
 
510
    @staticmethod
 
511
    def kind(tree, file_id):
 
512
        """Determine the kind of a file-id (used as a key method)."""
 
513
        if file_id not in tree:
 
514
            return None
 
515
        return tree.kind(file_id)
 
516
 
 
517
    @staticmethod
 
518
    def scalar_three_way(this_tree, base_tree, other_tree, file_id, key):
 
519
        """Do a three-way test on a scalar.
 
520
        Return "this", "other" or "conflict", depending whether a value wins.
 
521
        """
 
522
        key_base = key(base_tree, file_id)
 
523
        key_other = key(other_tree, file_id)
 
524
        #if base == other, either they all agree, or only THIS has changed.
 
525
        if key_base == key_other:
 
526
            return "this"
 
527
        key_this = key(this_tree, file_id)
 
528
        if key_this not in (key_base, key_other):
 
529
            return "conflict"
 
530
        # "Ambiguous clean merge"
 
531
        elif key_this == key_other:
 
532
            return "this"
 
533
        else:
 
534
            assert key_this == key_base
 
535
            return "other"
 
536
 
 
537
    def merge_names(self, file_id):
 
538
        """Perform a merge on file_id names and parents"""
 
539
        def get_entry(tree):
 
540
            if file_id in tree.inventory:
 
541
                return tree.inventory[file_id]
 
542
            else:
 
543
                return None
 
544
        this_entry = get_entry(self.this_tree)
 
545
        other_entry = get_entry(self.other_tree)
 
546
        base_entry = get_entry(self.base_tree)
 
547
        name_winner = self.scalar_three_way(this_entry, base_entry, 
 
548
                                            other_entry, file_id, self.name)
 
549
        parent_id_winner = self.scalar_three_way(this_entry, base_entry, 
 
550
                                                 other_entry, file_id, 
 
551
                                                 self.parent)
 
552
        if this_entry is None:
 
553
            if name_winner == "this":
 
554
                name_winner = "other"
 
555
            if parent_id_winner == "this":
 
556
                parent_id_winner = "other"
 
557
        if name_winner == "this" and parent_id_winner == "this":
 
558
            return
 
559
        if name_winner == "conflict":
 
560
            trans_id = self.tt.trans_id_file_id(file_id)
 
561
            self._raw_conflicts.append(('name conflict', trans_id, 
 
562
                                        self.name(this_entry, file_id), 
 
563
                                        self.name(other_entry, file_id)))
 
564
        if parent_id_winner == "conflict":
 
565
            trans_id = self.tt.trans_id_file_id(file_id)
 
566
            self._raw_conflicts.append(('parent conflict', trans_id, 
 
567
                                        self.parent(this_entry, file_id), 
 
568
                                        self.parent(other_entry, file_id)))
 
569
        if other_entry is None:
 
570
            # it doesn't matter whether the result was 'other' or 
 
571
            # 'conflict'-- if there's no 'other', we leave it alone.
 
572
            return
 
573
        # if we get here, name_winner and parent_winner are set to safe values.
 
574
        winner_entry = {"this": this_entry, "other": other_entry, 
 
575
                        "conflict": other_entry}
 
576
        trans_id = self.tt.trans_id_file_id(file_id)
 
577
        parent_id = winner_entry[parent_id_winner].parent_id
 
578
        if parent_id is not None:
 
579
            parent_trans_id = self.tt.trans_id_file_id(parent_id)
 
580
            self.tt.adjust_path(winner_entry[name_winner].name, 
 
581
                                parent_trans_id, trans_id)
 
582
 
 
583
    def merge_contents(self, file_id):
 
584
        """Performa a merge on file_id contents."""
 
585
        def contents_pair(tree):
 
586
            if file_id not in tree:
 
587
                return (None, None)
 
588
            kind = tree.kind(file_id)
 
589
            if kind == "file":
 
590
                contents = tree.get_file_sha1(file_id)
 
591
            elif kind == "symlink":
 
592
                contents = tree.get_symlink_target(file_id)
 
593
            else:
 
594
                contents = None
 
595
            return kind, contents
 
596
 
 
597
        def contents_conflict():
 
598
            trans_id = self.tt.trans_id_file_id(file_id)
 
599
            name = self.tt.final_name(trans_id)
 
600
            parent_id = self.tt.final_parent(trans_id)
 
601
            if file_id in self.this_tree.inventory:
 
602
                self.tt.unversion_file(trans_id)
 
603
                if file_id in self.this_tree:
 
604
                    self.tt.delete_contents(trans_id)
 
605
            file_group = self._dump_conflicts(name, parent_id, file_id, 
 
606
                                              set_version=True)
 
607
            self._raw_conflicts.append(('contents conflict', file_group))
 
608
 
 
609
        # See SPOT run.  run, SPOT, run.
 
610
        # So we're not QUITE repeating ourselves; we do tricky things with
 
611
        # file kind...
 
612
        base_pair = contents_pair(self.base_tree)
 
613
        other_pair = contents_pair(self.other_tree)
 
614
        if base_pair == other_pair:
 
615
            # OTHER introduced no changes
 
616
            return "unmodified"
 
617
        this_pair = contents_pair(self.this_tree)
 
618
        if this_pair == other_pair:
 
619
            # THIS and OTHER introduced the same changes
 
620
            return "unmodified"
 
621
        else:
 
622
            trans_id = self.tt.trans_id_file_id(file_id)
 
623
            if this_pair == base_pair:
 
624
                # only OTHER introduced changes
 
625
                if file_id in self.this_tree:
 
626
                    # Remove any existing contents
 
627
                    self.tt.delete_contents(trans_id)
 
628
                if file_id in self.other_tree:
 
629
                    # OTHER changed the file
 
630
                    create_by_entry(self.tt, 
 
631
                                    self.other_tree.inventory[file_id], 
 
632
                                    self.other_tree, trans_id)
 
633
                    if file_id not in self.this_tree.inventory:
 
634
                        self.tt.version_file(file_id, trans_id)
 
635
                    return "modified"
 
636
                elif file_id in self.this_tree.inventory:
 
637
                    # OTHER deleted the file
 
638
                    self.tt.unversion_file(trans_id)
 
639
                    return "deleted"
 
640
            #BOTH THIS and OTHER introduced changes; scalar conflict
 
641
            elif this_pair[0] == "file" and other_pair[0] == "file":
 
642
                # THIS and OTHER are both files, so text merge.  Either
 
643
                # BASE is a file, or both converted to files, so at least we
 
644
                # have agreement that output should be a file.
 
645
                try:
 
646
                    self.text_merge(file_id, trans_id)
 
647
                except BinaryFile:
 
648
                    return contents_conflict()
 
649
                if file_id not in self.this_tree.inventory:
 
650
                    self.tt.version_file(file_id, trans_id)
 
651
                try:
 
652
                    self.tt.tree_kind(trans_id)
 
653
                    self.tt.delete_contents(trans_id)
 
654
                except NoSuchFile:
 
655
                    pass
 
656
                return "modified"
 
657
            else:
 
658
                # Scalar conflict, can't text merge.  Dump conflicts
 
659
                return contents_conflict()
 
660
 
 
661
    def get_lines(self, tree, file_id):
 
662
        """Return the lines in a file, or an empty list."""
 
663
        if file_id in tree:
 
664
            return tree.get_file(file_id).readlines()
 
665
        else:
 
666
            return []
 
667
 
 
668
    def text_merge(self, file_id, trans_id):
 
669
        """Perform a three-way text merge on a file_id"""
 
670
        # it's possible that we got here with base as a different type.
 
671
        # if so, we just want two-way text conflicts.
 
672
        if file_id in self.base_tree and \
 
673
            self.base_tree.kind(file_id) == "file":
 
674
            base_lines = self.get_lines(self.base_tree, file_id)
 
675
        else:
 
676
            base_lines = []
 
677
        other_lines = self.get_lines(self.other_tree, file_id)
 
678
        this_lines = self.get_lines(self.this_tree, file_id)
 
679
        m3 = Merge3(base_lines, this_lines, other_lines)
 
680
        start_marker = "!START OF MERGE CONFLICT!" + "I HOPE THIS IS UNIQUE"
 
681
        if self.show_base is True:
 
682
            base_marker = '|' * 7
 
683
        else:
 
684
            base_marker = None
 
685
 
 
686
        def iter_merge3(retval):
 
687
            retval["text_conflicts"] = False
 
688
            for line in m3.merge_lines(name_a = "TREE", 
 
689
                                       name_b = "MERGE-SOURCE", 
 
690
                                       name_base = "BASE-REVISION",
 
691
                                       start_marker=start_marker, 
 
692
                                       base_marker=base_marker,
 
693
                                       reprocess=self.reprocess):
 
694
                if line.startswith(start_marker):
 
695
                    retval["text_conflicts"] = True
 
696
                    yield line.replace(start_marker, '<' * 7)
 
697
                else:
 
698
                    yield line
 
699
        retval = {}
 
700
        merge3_iterator = iter_merge3(retval)
 
701
        self.tt.create_file(merge3_iterator, trans_id)
 
702
        if retval["text_conflicts"] is True:
 
703
            self._raw_conflicts.append(('text conflict', trans_id))
 
704
            name = self.tt.final_name(trans_id)
 
705
            parent_id = self.tt.final_parent(trans_id)
 
706
            file_group = self._dump_conflicts(name, parent_id, file_id, 
 
707
                                              this_lines, base_lines,
 
708
                                              other_lines)
 
709
            file_group.append(trans_id)
 
710
 
 
711
    def _dump_conflicts(self, name, parent_id, file_id, this_lines=None, 
 
712
                        base_lines=None, other_lines=None, set_version=False,
 
713
                        no_base=False):
 
714
        """Emit conflict files.
 
715
        If this_lines, base_lines, or other_lines are omitted, they will be
 
716
        determined automatically.  If set_version is true, the .OTHER, .THIS
 
717
        or .BASE (in that order) will be created as versioned files.
 
718
        """
 
719
        data = [('OTHER', self.other_tree, other_lines), 
 
720
                ('THIS', self.this_tree, this_lines)]
 
721
        if not no_base:
 
722
            data.append(('BASE', self.base_tree, base_lines))
 
723
        versioned = False
 
724
        file_group = []
 
725
        for suffix, tree, lines in data:
 
726
            if file_id in tree:
 
727
                trans_id = self._conflict_file(name, parent_id, tree, file_id,
 
728
                                               suffix, lines)
 
729
                file_group.append(trans_id)
 
730
                if set_version and not versioned:
 
731
                    self.tt.version_file(file_id, trans_id)
 
732
                    versioned = True
 
733
        return file_group
 
734
           
 
735
    def _conflict_file(self, name, parent_id, tree, file_id, suffix, 
 
736
                       lines=None):
 
737
        """Emit a single conflict file."""
 
738
        name = name + '.' + suffix
 
739
        trans_id = self.tt.create_path(name, parent_id)
 
740
        entry = tree.inventory[file_id]
 
741
        create_by_entry(self.tt, entry, tree, trans_id, lines)
 
742
        return trans_id
 
743
 
 
744
    def merge_executable(self, file_id, file_status):
 
745
        """Perform a merge on the execute bit."""
 
746
        if file_status == "deleted":
 
747
            return
 
748
        trans_id = self.tt.trans_id_file_id(file_id)
 
749
        try:
 
750
            if self.tt.final_kind(trans_id) != "file":
 
751
                return
 
752
        except NoSuchFile:
 
753
            return
 
754
        winner = self.scalar_three_way(self.this_tree, self.base_tree, 
 
755
                                       self.other_tree, file_id, 
 
756
                                       self.executable)
 
757
        if winner == "conflict":
 
758
        # There must be a None in here, if we have a conflict, but we
 
759
        # need executability since file status was not deleted.
 
760
            if self.executable(self.other_tree, file_id) is None:
 
761
                winner = "this"
 
762
            else:
 
763
                winner = "other"
 
764
        if winner == "this":
 
765
            if file_status == "modified":
 
766
                executability = self.this_tree.is_executable(file_id)
 
767
                if executability is not None:
 
768
                    trans_id = self.tt.trans_id_file_id(file_id)
 
769
                    self.tt.set_executability(executability, trans_id)
 
770
        else:
 
771
            assert winner == "other"
 
772
            if file_id in self.other_tree:
 
773
                executability = self.other_tree.is_executable(file_id)
 
774
            elif file_id in self.this_tree:
 
775
                executability = self.this_tree.is_executable(file_id)
 
776
            elif file_id in self.base_tree:
 
777
                executability = self.base_tree.is_executable(file_id)
 
778
            if executability is not None:
 
779
                trans_id = self.tt.trans_id_file_id(file_id)
 
780
                self.tt.set_executability(executability, trans_id)
 
781
 
 
782
    def cook_conflicts(self, fs_conflicts):
 
783
        """Convert all conflicts into a form that doesn't depend on trans_id"""
 
784
        from conflicts import Conflict
 
785
        name_conflicts = {}
 
786
        self.cooked_conflicts.extend(cook_conflicts(fs_conflicts, self.tt))
 
787
        fp = FinalPaths(self.tt)
 
788
        for conflict in self._raw_conflicts:
 
789
            conflict_type = conflict[0]
 
790
            if conflict_type in ('name conflict', 'parent conflict'):
 
791
                trans_id = conflict[1]
 
792
                conflict_args = conflict[2:]
 
793
                if trans_id not in name_conflicts:
 
794
                    name_conflicts[trans_id] = {}
 
795
                unique_add(name_conflicts[trans_id], conflict_type, 
 
796
                           conflict_args)
 
797
            if conflict_type == 'contents conflict':
 
798
                for trans_id in conflict[1]:
 
799
                    file_id = self.tt.final_file_id(trans_id)
 
800
                    if file_id is not None:
 
801
                        break
 
802
                path = fp.get_path(trans_id)
 
803
                for suffix in ('.BASE', '.THIS', '.OTHER'):
 
804
                    if path.endswith(suffix):
 
805
                        path = path[:-len(suffix)]
 
806
                        break
 
807
                c = Conflict.factory(conflict_type, path=path, file_id=file_id)
 
808
                self.cooked_conflicts.append(c)
 
809
            if conflict_type == 'text conflict':
 
810
                trans_id = conflict[1]
 
811
                path = fp.get_path(trans_id)
 
812
                file_id = self.tt.final_file_id(trans_id)
 
813
                c = Conflict.factory(conflict_type, path=path, file_id=file_id)
 
814
                self.cooked_conflicts.append(c)
 
815
 
 
816
        for trans_id, conflicts in name_conflicts.iteritems():
 
817
            try:
 
818
                this_parent, other_parent = conflicts['parent conflict']
 
819
                assert this_parent != other_parent
 
820
            except KeyError:
 
821
                this_parent = other_parent = \
 
822
                    self.tt.final_file_id(self.tt.final_parent(trans_id))
 
823
            try:
 
824
                this_name, other_name = conflicts['name conflict']
 
825
                assert this_name != other_name
 
826
            except KeyError:
 
827
                this_name = other_name = self.tt.final_name(trans_id)
 
828
            other_path = fp.get_path(trans_id)
 
829
            if this_parent is not None:
 
830
                this_parent_path = \
 
831
                    fp.get_path(self.tt.trans_id_file_id(this_parent))
 
832
                this_path = pathjoin(this_parent_path, this_name)
 
833
            else:
 
834
                this_path = "<deleted>"
 
835
            file_id = self.tt.final_file_id(trans_id)
 
836
            c = Conflict.factory('path conflict', path=this_path,
 
837
                                 conflict_path=other_path, file_id=file_id)
 
838
            self.cooked_conflicts.append(c)
 
839
        self.cooked_conflicts.sort(key=Conflict.sort_key)
 
840
 
 
841
 
 
842
class WeaveMerger(Merge3Merger):
 
843
    """Three-way tree merger, text weave merger."""
 
844
    supports_reprocess = True
 
845
    supports_show_base = False
 
846
 
 
847
    def __init__(self, working_tree, this_tree, base_tree, other_tree, 
 
848
                 interesting_ids=None, pb=DummyProgress(), pp=None,
 
849
                 reprocess=False, change_reporter=None):
 
850
        self.this_revision_tree = self._get_revision_tree(this_tree)
 
851
        self.other_revision_tree = self._get_revision_tree(other_tree)
 
852
        super(WeaveMerger, self).__init__(working_tree, this_tree, 
 
853
                                          base_tree, other_tree, 
 
854
                                          interesting_ids=interesting_ids, 
 
855
                                          pb=pb, pp=pp, reprocess=reprocess,
 
856
                                          change_reporter=change_reporter)
 
857
 
 
858
    def _get_revision_tree(self, tree):
 
859
        """Return a revision tree related to this tree.
 
860
        If the tree is a WorkingTree, the basis will be returned.
 
861
        """
 
862
        if getattr(tree, 'get_weave', False) is False:
 
863
            # If we have a WorkingTree, try using the basis
 
864
            return tree.branch.basis_tree()
 
865
        else:
 
866
            return tree
 
867
 
 
868
    def _check_file(self, file_id):
 
869
        """Check that the revision tree's version of the file matches."""
 
870
        for tree, rt in ((self.this_tree, self.this_revision_tree), 
 
871
                         (self.other_tree, self.other_revision_tree)):
 
872
            if rt is tree:
 
873
                continue
 
874
            if tree.get_file_sha1(file_id) != rt.get_file_sha1(file_id):
 
875
                raise WorkingTreeNotRevision(self.this_tree)
 
876
 
 
877
    def _merged_lines(self, file_id):
 
878
        """Generate the merged lines.
 
879
        There is no distinction between lines that are meant to contain <<<<<<<
 
880
        and conflicts.
 
881
        """
 
882
        weave = self.this_revision_tree.get_weave(file_id)
 
883
        this_revision_id = self.this_revision_tree.inventory[file_id].revision
 
884
        other_revision_id = \
 
885
            self.other_revision_tree.inventory[file_id].revision
 
886
        wm = WeaveMerge(weave, this_revision_id, other_revision_id, 
 
887
                        '<<<<<<< TREE\n', '>>>>>>> MERGE-SOURCE\n')
 
888
        return wm.merge_lines(self.reprocess)
 
889
 
 
890
    def text_merge(self, file_id, trans_id):
 
891
        """Perform a (weave) text merge for a given file and file-id.
 
892
        If conflicts are encountered, .THIS and .OTHER files will be emitted,
 
893
        and a conflict will be noted.
 
894
        """
 
895
        self._check_file(file_id)
 
896
        lines, conflicts = self._merged_lines(file_id)
 
897
        lines = list(lines)
 
898
        # Note we're checking whether the OUTPUT is binary in this case, 
 
899
        # because we don't want to get into weave merge guts.
 
900
        check_text_lines(lines)
 
901
        self.tt.create_file(lines, trans_id)
 
902
        if conflicts:
 
903
            self._raw_conflicts.append(('text conflict', trans_id))
 
904
            name = self.tt.final_name(trans_id)
 
905
            parent_id = self.tt.final_parent(trans_id)
 
906
            file_group = self._dump_conflicts(name, parent_id, file_id, 
 
907
                                              no_base=True)
 
908
            file_group.append(trans_id)
 
909
 
 
910
 
 
911
class Diff3Merger(Merge3Merger):
 
912
    """Three-way merger using external diff3 for text merging"""
 
913
 
 
914
    def dump_file(self, temp_dir, name, tree, file_id):
 
915
        out_path = pathjoin(temp_dir, name)
 
916
        out_file = open(out_path, "wb")
 
917
        try:
 
918
            in_file = tree.get_file(file_id)
 
919
            for line in in_file:
 
920
                out_file.write(line)
 
921
        finally:
 
922
            out_file.close()
 
923
        return out_path
 
924
 
 
925
    def text_merge(self, file_id, trans_id):
 
926
        """Perform a diff3 merge using a specified file-id and trans-id.
 
927
        If conflicts are encountered, .BASE, .THIS. and .OTHER conflict files
 
928
        will be dumped, and a will be conflict noted.
 
929
        """
 
930
        import bzrlib.patch
 
931
        temp_dir = osutils.mkdtemp(prefix="bzr-")
 
932
        try:
 
933
            new_file = pathjoin(temp_dir, "new")
 
934
            this = self.dump_file(temp_dir, "this", self.this_tree, file_id)
 
935
            base = self.dump_file(temp_dir, "base", self.base_tree, file_id)
 
936
            other = self.dump_file(temp_dir, "other", self.other_tree, file_id)
 
937
            status = bzrlib.patch.diff3(new_file, this, base, other)
 
938
            if status not in (0, 1):
 
939
                raise BzrError("Unhandled diff3 exit code")
 
940
            f = open(new_file, 'rb')
 
941
            try:
 
942
                self.tt.create_file(f, trans_id)
 
943
            finally:
 
944
                f.close()
 
945
            if status == 1:
 
946
                name = self.tt.final_name(trans_id)
 
947
                parent_id = self.tt.final_parent(trans_id)
 
948
                self._dump_conflicts(name, parent_id, file_id)
 
949
                self._raw_conflicts.append(('text conflict', trans_id))
 
950
        finally:
 
951
            osutils.rmtree(temp_dir)
 
952
 
 
953
 
 
954
def merge_inner(this_branch, other_tree, base_tree, ignore_zero=False,
 
955
                backup_files=False,
 
956
                merge_type=Merge3Merger,
 
957
                interesting_ids=None,
 
958
                show_base=False,
 
959
                reprocess=False,
 
960
                other_rev_id=None,
 
961
                interesting_files=None,
 
962
                this_tree=None,
 
963
                pb=DummyProgress(),
 
964
                change_reporter=None):
 
965
    """Primary interface for merging. 
 
966
 
 
967
        typical use is probably 
 
968
        'merge_inner(branch, branch.get_revision_tree(other_revision),
 
969
                     branch.get_revision_tree(base_revision))'
 
970
        """
 
971
    if this_tree is None:
 
972
        warnings.warn("bzrlib.merge.merge_inner requires a this_tree parameter as of "
 
973
             "bzrlib version 0.8.",
 
974
             DeprecationWarning,
 
975
             stacklevel=2)
 
976
        this_tree = this_branch.bzrdir.open_workingtree()
 
977
    merger = Merger(this_branch, other_tree, base_tree, this_tree=this_tree,
 
978
                    pb=pb, change_reporter=change_reporter)
 
979
    merger.backup_files = backup_files
 
980
    merger.merge_type = merge_type
 
981
    merger.interesting_ids = interesting_ids
 
982
    merger.ignore_zero = ignore_zero
 
983
    if interesting_files:
 
984
        assert not interesting_ids, ('Only supply interesting_ids'
 
985
                                     ' or interesting_files')
 
986
        merger._set_interesting_files(interesting_files)
 
987
    merger.show_base = show_base
 
988
    merger.reprocess = reprocess
 
989
    merger.other_rev_id = other_rev_id
 
990
    merger.other_basis = other_rev_id
 
991
    return merger.do_merge()
 
992
 
 
993
def get_merge_type_registry():
 
994
    """Merge type registry is in bzrlib.option to avoid circular imports.
 
995
 
 
996
    This method provides a sanctioned way to retrieve it.
 
997
    """
 
998
    from bzrlib import option
 
999
    return option._merge_type_registry