/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

Switched to build_tree instead of revert

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005 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
from tempfile import mkdtemp
 
21
from shutil import rmtree
 
22
 
 
23
import bzrlib
 
24
from bzrlib.branch import Branch
 
25
from bzrlib.delta import compare_trees
 
26
from bzrlib.errors import (BzrCommandError,
 
27
                           BzrError,
 
28
                           NoCommonAncestor,
 
29
                           NoCommits,
 
30
                           NoSuchRevision,
 
31
                           NoSuchFile,
 
32
                           NotBranchError,
 
33
                           NotVersionedError,
 
34
                           UnrelatedBranches,
 
35
                           WorkingTreeNotRevision,
 
36
                           )
 
37
from bzrlib.fetch import greedy_fetch, fetch
 
38
import bzrlib.osutils
 
39
from bzrlib.merge3 import Merge3
 
40
from bzrlib.osutils import rename, pathjoin
 
41
from bzrlib.revision import common_ancestor, is_ancestor, NULL_REVISION
 
42
from bzrlib.transform import TreeTransform, resolve_conflicts, FinalPaths, create_by_entry, unique_add
 
43
from bzrlib.trace import mutter, warning, note
 
44
 
 
45
# TODO: Report back as changes are merged in
 
46
 
 
47
# comments from abentley on irc: merge happens in two stages, each
 
48
# of which generates a changeset object
 
49
 
 
50
# stage 1: generate OLD->OTHER,
 
51
# stage 2: use MINE and OLD->OTHER to generate MINE -> RESULT
 
52
 
 
53
def _get_tree(treespec, local_branch=None):
 
54
    location, revno = treespec
 
55
    branch = Branch.open_containing(location)[0]
 
56
    if revno is None:
 
57
        revision = None
 
58
    elif revno == -1:
 
59
        revision = branch.last_revision()
 
60
    else:
 
61
        revision = branch.get_rev_id(revno)
 
62
        if revision is None:
 
63
            revision = NULL_REVISION
 
64
    return branch, _get_revid_tree(branch, revision, local_branch)
 
65
 
 
66
 
 
67
def _get_revid_tree(branch, revision, local_branch):
 
68
    if revision is None:
 
69
        base_tree = branch.bzrdir.open_workingtree()
 
70
    else:
 
71
        if local_branch is not None:
 
72
            if local_branch.base != branch.base:
 
73
                greedy_fetch(local_branch, branch, revision)
 
74
            base_tree = local_branch.repository.revision_tree(revision)
 
75
        else:
 
76
            base_tree = branch.repository.revision_tree(revision)
 
77
    return base_tree
 
78
 
 
79
 
 
80
def build_working_dir(to_dir):
 
81
    """Build a working directory in an empty directory.
 
82
 
 
83
    to_dir is a directory containing branch metadata but no working files,
 
84
    typically constructed by cloning an existing branch. 
 
85
 
 
86
    This is split out as a special idiomatic case of merge.  It could
 
87
    eventually be done by just building the tree directly calling into 
 
88
    lower-level code (e.g. constructing a changeset).
 
89
    """
 
90
    # RBC 20051019 is this not just 'export' ?
 
91
    # AB Well, export doesn't take care of inventory...
 
92
    from transform import build_tree
 
93
    this_branch = Branch.open_containing(to_dir)[0]
 
94
    build_tree(this_branch, this_branch.basis_tree())
 
95
 
 
96
 
 
97
def transform_tree(from_tree, to_tree, interesting_ids=None):
 
98
    merge_inner(from_tree.branch, to_tree, from_tree, ignore_zero=True,
 
99
                interesting_ids=interesting_ids)
 
100
 
 
101
 
 
102
class Merger(object):
 
103
    def __init__(self, this_branch, other_tree=None, base_tree=None, this_tree=None):
 
104
        object.__init__(self)
 
105
        assert this_tree is not None, "this_tree is required"
 
106
        self.this_branch = this_branch
 
107
        self.this_basis = this_branch.last_revision()
 
108
        self.this_rev_id = None
 
109
        self.this_tree = this_tree
 
110
        self.this_revision_tree = None
 
111
        self.this_basis_tree = None
 
112
        self.other_tree = other_tree
 
113
        self.base_tree = base_tree
 
114
        self.ignore_zero = False
 
115
        self.backup_files = False
 
116
        self.interesting_ids = None
 
117
        self.show_base = False
 
118
        self.reprocess = False
 
119
 
 
120
    def revision_tree(self, revision_id):
 
121
        return self.this_branch.repository.revision_tree(revision_id)
 
122
 
 
123
    def ensure_revision_trees(self):
 
124
        if self.this_revision_tree is None:
 
125
            self.this_basis_tree = self.this_branch.repository.revision_tree(
 
126
                self.this_basis)
 
127
            if self.this_basis == self.this_rev_id:
 
128
                self.this_revision_tree = self.this_basis_tree
 
129
 
 
130
        if self.other_rev_id is None:
 
131
            other_basis_tree = self.revision_tree(self.other_basis)
 
132
            changes = compare_trees(self.other_tree, other_basis_tree)
 
133
            if changes.has_changed():
 
134
                raise WorkingTreeNotRevision(self.this_tree)
 
135
            other_rev_id = other_basis
 
136
            self.other_tree = other_basis_tree
 
137
 
 
138
    def file_revisions(self, file_id):
 
139
        self.ensure_revision_trees()
 
140
        def get_id(tree, file_id):
 
141
            revision_id = tree.inventory[file_id].revision
 
142
            assert revision_id is not None
 
143
            return revision_id
 
144
        if self.this_rev_id is None:
 
145
            if self.this_basis_tree.get_file_sha1(file_id) != \
 
146
                self.this_tree.get_file_sha1(file_id):
 
147
                raise WorkingTreeNotRevision(self.this_tree)
 
148
 
 
149
        trees = (self.this_basis_tree, self.other_tree)
 
150
        return [get_id(tree, file_id) for tree in trees]
 
151
 
 
152
    def check_basis(self, check_clean):
 
153
        if self.this_basis is None:
 
154
            raise BzrCommandError("This branch has no commits")
 
155
        if check_clean:
 
156
            self.compare_basis()
 
157
            if self.this_basis != self.this_rev_id:
 
158
                raise BzrCommandError("Working tree has uncommitted changes.")
 
159
 
 
160
    def compare_basis(self):
 
161
        changes = compare_trees(self.this_tree, 
 
162
                                self.this_tree.basis_tree(), False)
 
163
        if not changes.has_changed():
 
164
            self.this_rev_id = self.this_basis
 
165
 
 
166
    def set_interesting_files(self, file_list):
 
167
        try:
 
168
            self._set_interesting_files(file_list)
 
169
        except NotVersionedError, e:
 
170
            raise BzrCommandError("%s is not a source file in any"
 
171
                                      " tree." % e.path)
 
172
 
 
173
    def _set_interesting_files(self, file_list):
 
174
        """Set the list of interesting ids from a list of files."""
 
175
        if file_list is None:
 
176
            self.interesting_ids = None
 
177
            return
 
178
 
 
179
        interesting_ids = set()
 
180
        for path in file_list:
 
181
            found_id = False
 
182
            for tree in (self.this_tree, self.base_tree, self.other_tree):
 
183
                file_id = tree.inventory.path2id(path)
 
184
                if file_id is not None:
 
185
                    interesting_ids.add(file_id)
 
186
                    found_id = True
 
187
            if not found_id:
 
188
                raise NotVersionedError(path=path)
 
189
        self.interesting_ids = interesting_ids
 
190
 
 
191
    def set_pending(self):
 
192
        if not self.base_is_ancestor:
 
193
            return
 
194
        if self.other_rev_id is None:
 
195
            return
 
196
        ancestry = self.this_branch.repository.get_ancestry(self.this_basis)
 
197
        if self.other_rev_id in ancestry:
 
198
            return
 
199
        self.this_tree.add_pending_merge(self.other_rev_id)
 
200
 
 
201
    def set_other(self, other_revision):
 
202
        other_branch, self.other_tree = _get_tree(other_revision, 
 
203
                                                  self.this_branch)
 
204
        if other_revision[1] == -1:
 
205
            self.other_rev_id = other_branch.last_revision()
 
206
            if self.other_rev_id is None:
 
207
                raise NoCommits(other_branch)
 
208
            self.other_basis = self.other_rev_id
 
209
        elif other_revision[1] is not None:
 
210
            self.other_rev_id = other_branch.get_rev_id(other_revision[1])
 
211
            self.other_basis = self.other_rev_id
 
212
        else:
 
213
            self.other_rev_id = None
 
214
            self.other_basis = other_branch.last_revision()
 
215
            if self.other_basis is None:
 
216
                raise NoCommits(other_branch)
 
217
        if other_branch.base != self.this_branch.base:
 
218
            fetch(from_branch=other_branch, to_branch=self.this_branch, 
 
219
                  last_revision=self.other_basis)
 
220
 
 
221
    def set_base(self, base_revision):
 
222
        mutter("doing merge() with no base_revision specified")
 
223
        if base_revision == [None, None]:
 
224
            try:
 
225
                self.base_rev_id = common_ancestor(self.this_basis, 
 
226
                                                   self.other_basis, 
 
227
                                                   self.this_branch.repository)
 
228
            except NoCommonAncestor:
 
229
                raise UnrelatedBranches()
 
230
            self.base_tree = _get_revid_tree(self.this_branch, self.base_rev_id,
 
231
                                            None)
 
232
            self.base_is_ancestor = True
 
233
        else:
 
234
            base_branch, self.base_tree = _get_tree(base_revision)
 
235
            if base_revision[1] == -1:
 
236
                self.base_rev_id = base_branch.last_revision()
 
237
            elif base_revision[1] is None:
 
238
                self.base_rev_id = None
 
239
            else:
 
240
                self.base_rev_id = base_branch.get_rev_id(base_revision[1])
 
241
            fetch(from_branch=base_branch, to_branch=self.this_branch)
 
242
            self.base_is_ancestor = is_ancestor(self.this_basis, 
 
243
                                                self.base_rev_id,
 
244
                                                self.this_branch)
 
245
 
 
246
    def do_merge(self):
 
247
        kwargs = {'working_tree':self.this_tree, 'this_tree': self.this_tree, 
 
248
                  'other_tree': self.other_tree}
 
249
        if self.merge_type.requires_base:
 
250
            kwargs['base_tree'] = self.base_tree
 
251
        if self.merge_type.supports_reprocess:
 
252
            kwargs['reprocess'] = self.reprocess
 
253
        elif self.reprocess:
 
254
            raise BzrError("Reprocess is not supported for this merge"
 
255
                                  " type. %s" % merge_type)
 
256
        if self.merge_type.supports_show_base:
 
257
            kwargs['show_base'] = self.show_base
 
258
        elif self.show_base:
 
259
            raise BzrError("Showing base is not supported for this"
 
260
                                  " merge type. %s" % self.merge_type)
 
261
        merge = self.merge_type(**kwargs)
 
262
        if len(merge.cooked_conflicts) == 0:
 
263
            if not self.ignore_zero:
 
264
                note("All changes applied successfully.")
 
265
        else:
 
266
            note("%d conflicts encountered." % len(merge.cooked_conflicts))
 
267
 
 
268
        return len(merge.cooked_conflicts)
 
269
 
 
270
    def regen_inventory(self, new_entries):
 
271
        old_entries = self.this_tree.read_working_inventory()
 
272
        new_inventory = {}
 
273
        by_path = {}
 
274
        new_entries_map = {} 
 
275
        for path, file_id in new_entries:
 
276
            if path is None:
 
277
                continue
 
278
            new_entries_map[file_id] = path
 
279
 
 
280
        def id2path(file_id):
 
281
            path = new_entries_map.get(file_id)
 
282
            if path is not None:
 
283
                return path
 
284
            entry = old_entries[file_id]
 
285
            if entry.parent_id is None:
 
286
                return entry.name
 
287
            return pathjoin(id2path(entry.parent_id), entry.name)
 
288
            
 
289
        for file_id in old_entries:
 
290
            entry = old_entries[file_id]
 
291
            path = id2path(file_id)
 
292
            new_inventory[file_id] = (path, file_id, entry.parent_id, 
 
293
                                      entry.kind)
 
294
            by_path[path] = file_id
 
295
        
 
296
        deletions = 0
 
297
        insertions = 0
 
298
        new_path_list = []
 
299
        for path, file_id in new_entries:
 
300
            if path is None:
 
301
                del new_inventory[file_id]
 
302
                deletions += 1
 
303
            else:
 
304
                new_path_list.append((path, file_id))
 
305
                if file_id not in old_entries:
 
306
                    insertions += 1
 
307
        # Ensure no file is added before its parent
 
308
        new_path_list.sort()
 
309
        for path, file_id in new_path_list:
 
310
            if path == '':
 
311
                parent = None
 
312
            else:
 
313
                parent = by_path[os.path.dirname(path)]
 
314
            abspath = pathjoin(self.this_tree.basedir, path)
 
315
            kind = bzrlib.osutils.file_kind(abspath)
 
316
            new_inventory[file_id] = (path, file_id, parent, kind)
 
317
            by_path[path] = file_id 
 
318
 
 
319
        # Get a list in insertion order
 
320
        new_inventory_list = new_inventory.values()
 
321
        mutter ("""Inventory regeneration:
 
322
    old length: %i insertions: %i deletions: %i new_length: %i"""\
 
323
            % (len(old_entries), insertions, deletions, 
 
324
               len(new_inventory_list)))
 
325
        assert len(new_inventory_list) == len(old_entries) + insertions\
 
326
            - deletions
 
327
        new_inventory_list.sort()
 
328
        return new_inventory_list
 
329
 
 
330
 
 
331
class Merge3Merger(object):
 
332
    requires_base = True
 
333
    supports_reprocess = True
 
334
    supports_show_base = True
 
335
    history_based = False
 
336
    def __init__(self, working_tree, this_tree, base_tree, other_tree, 
 
337
                 reprocess=False, show_base=False):
 
338
        """Initialize the merger object and perform the merge."""
 
339
        object.__init__(self)
 
340
        self.this_tree = working_tree
 
341
        self.base_tree = base_tree
 
342
        self.other_tree = other_tree
 
343
        self._raw_conflicts = []
 
344
        self.cooked_conflicts = []
 
345
        self.reprocess = reprocess
 
346
        self.show_base = show_base
 
347
 
 
348
        all_ids = set(base_tree)
 
349
        all_ids.update(other_tree)
 
350
        self.tt = TreeTransform(working_tree)
 
351
        try:
 
352
            for file_id in all_ids:
 
353
                self.merge_names(file_id)
 
354
                file_status = self.merge_contents(file_id)
 
355
                self.merge_executable(file_id, file_status)
 
356
                
 
357
            resolve_conflicts(self.tt)
 
358
            self.cook_conflicts()
 
359
            for line in conflicts_strings(self.cooked_conflicts):
 
360
                warning(line)
 
361
            self.tt.apply()
 
362
        finally:
 
363
            try:
 
364
                self.tt.finalize()
 
365
            except:
 
366
                pass
 
367
       
 
368
    @staticmethod
 
369
    def parent(entry, file_id):
 
370
        """Determine the parent for a file_id (used as a key method)"""
 
371
        if entry is None:
 
372
            return None
 
373
        return entry.parent_id
 
374
 
 
375
    @staticmethod
 
376
    def name(entry, file_id):
 
377
        """Determine the name for a file_id (used as a key method)"""
 
378
        if entry is None:
 
379
            return None
 
380
        return entry.name
 
381
    
 
382
    @staticmethod
 
383
    def contents_sha1(tree, file_id):
 
384
        """Determine the sha1 of the file contents (used as a key method)."""
 
385
        if file_id not in tree:
 
386
            return None
 
387
        return tree.get_file_sha1(file_id)
 
388
 
 
389
    @staticmethod
 
390
    def executable(tree, file_id):
 
391
        """Determine the executability of a file-id (used as a key method)."""
 
392
        if file_id not in tree:
 
393
            return None
 
394
        if tree.kind(file_id) != "file":
 
395
            return False
 
396
        return tree.is_executable(file_id)
 
397
 
 
398
    @staticmethod
 
399
    def kind(tree, file_id):
 
400
        """Determine the kind of a file-id (used as a key method)."""
 
401
        if file_id not in tree:
 
402
            return None
 
403
        return tree.kind(file_id)
 
404
 
 
405
    @staticmethod
 
406
    def scalar_three_way(this_tree, base_tree, other_tree, file_id, key):
 
407
        """Do a three-way test on a scalar.
 
408
        Return "this", "other" or "conflict", depending whether a value wins.
 
409
        """
 
410
        key_base = key(base_tree, file_id)
 
411
        key_other = key(other_tree, file_id)
 
412
        #if base == other, either they all agree, or only THIS has changed.
 
413
        if key_base == key_other:
 
414
            return "this"
 
415
        key_this = key(this_tree, file_id)
 
416
        if key_this not in (key_base, key_other):
 
417
            return "conflict"
 
418
        # "Ambiguous clean merge"
 
419
        elif key_this == key_other:
 
420
            return "this"
 
421
        else:
 
422
            assert key_this == key_base
 
423
            return "other"
 
424
 
 
425
    def merge_names(self, file_id):
 
426
        """Perform a merge on file_id names and parents"""
 
427
        def get_entry(tree):
 
428
            if file_id in tree.inventory:
 
429
                return tree.inventory[file_id]
 
430
            else:
 
431
                return None
 
432
        this_entry = get_entry(self.this_tree)
 
433
        other_entry = get_entry(self.other_tree)
 
434
        base_entry = get_entry(self.base_tree)
 
435
        name_winner = self.scalar_three_way(this_entry, base_entry, 
 
436
                                            other_entry, file_id, self.name)
 
437
        parent_id_winner = self.scalar_three_way(this_entry, base_entry, 
 
438
                                                 other_entry, file_id, 
 
439
                                                 self.parent)
 
440
        if this_entry is None:
 
441
            if name_winner == "this":
 
442
                name_winner = "other"
 
443
            if parent_id_winner == "this":
 
444
                parent_id_winner = "other"
 
445
        if name_winner == "this" and parent_id_winner == "this":
 
446
            return
 
447
        if name_winner == "conflict":
 
448
            trans_id = self.tt.get_trans_id(file_id)
 
449
            self._raw_conflicts.append(('name conflict', trans_id, 
 
450
                                        self.name(this_entry, file_id), 
 
451
                                        self.name(other_entry, file_id)))
 
452
        if parent_id_winner == "conflict":
 
453
            trans_id = self.tt.get_trans_id(file_id)
 
454
            self._raw_conflicts.append(('parent conflict', trans_id, 
 
455
                                        self.parent(this_entry, file_id), 
 
456
                                        self.parent(other_entry, file_id)))
 
457
        if other_entry is None:
 
458
            # it doesn't matter whether the result was 'other' or 
 
459
            # 'conflict'-- if there's no 'other', we leave it alone.
 
460
            return
 
461
        # if we get here, name_winner and parent_winner are set to safe values.
 
462
        winner_entry = {"this": this_entry, "other": other_entry, 
 
463
                        "conflict": other_entry}
 
464
        trans_id = self.tt.get_trans_id(file_id)
 
465
        parent_id = winner_entry[parent_id_winner].parent_id
 
466
        parent_trans_id = self.tt.get_trans_id(parent_id)
 
467
        self.tt.adjust_path(winner_entry[name_winner].name, parent_trans_id,
 
468
                            trans_id)
 
469
 
 
470
 
 
471
    def merge_contents(self, file_id):
 
472
        """Performa a merge on file_id contents."""
 
473
        def contents_pair(tree):
 
474
            if file_id not in tree:
 
475
                return (None, None)
 
476
            kind = tree.kind(file_id)
 
477
            if kind == "root_directory":
 
478
                kind = "directory"
 
479
            if kind == "file":
 
480
                contents = tree.get_file_sha1(file_id)
 
481
            elif kind == "symlink":
 
482
                contents = tree.get_symlink_target(file_id)
 
483
            else:
 
484
                contents = None
 
485
            return kind, contents
 
486
        # See SPOT run.  run, SPOT, run.
 
487
        # So we're not QUITE repeating ourselves; we do tricky things with
 
488
        # file kind...
 
489
        base_pair = contents_pair(self.base_tree)
 
490
        other_pair = contents_pair(self.other_tree)
 
491
        if base_pair == other_pair:
 
492
            # OTHER introduced no changes
 
493
            return "unmodified"
 
494
        this_pair = contents_pair(self.this_tree)
 
495
        if this_pair == other_pair:
 
496
            # THIS and OTHER introduced the same changes
 
497
            return "unmodified"
 
498
        else:
 
499
            trans_id = self.tt.get_trans_id(file_id)
 
500
            if this_pair == base_pair:
 
501
                # only OTHER introduced changes
 
502
                if file_id in self.this_tree:
 
503
                    # Remove any existing contents
 
504
                    self.tt.delete_contents(trans_id)
 
505
                if file_id in self.other_tree:
 
506
                    # OTHER changed the file
 
507
                    create_by_entry(self.tt, 
 
508
                                    self.other_tree.inventory[file_id], 
 
509
                                    self.other_tree, trans_id)
 
510
                    if file_id not in self.this_tree.inventory:
 
511
                        self.tt.version_file(file_id, trans_id)
 
512
                    return "modified"
 
513
                elif file_id in self.this_tree.inventory:
 
514
                    # OTHER deleted the file
 
515
                    self.tt.unversion_file(trans_id)
 
516
                    return "deleted"
 
517
            #BOTH THIS and OTHER introduced changes; scalar conflict
 
518
            elif this_pair[0] == "file" and other_pair[0] == "file":
 
519
                # THIS and OTHER are both files, so text merge.  Either
 
520
                # BASE is a file, or both converted to files, so at least we
 
521
                # have agreement that output should be a file.
 
522
                if file_id not in self.this_tree.inventory:
 
523
                    self.tt.version_file(file_id, trans_id)
 
524
                self.text_merge(file_id, trans_id)
 
525
                try:
 
526
                    self.tt.tree_kind(trans_id)
 
527
                    self.tt.delete_contents(trans_id)
 
528
                except NoSuchFile:
 
529
                    pass
 
530
                return "modified"
 
531
            else:
 
532
                # Scalar conflict, can't text merge.  Dump conflicts
 
533
                trans_id = self.tt.get_trans_id(file_id)
 
534
                name = self.tt.final_name(trans_id)
 
535
                parent_id = self.tt.final_parent(trans_id)
 
536
                if file_id in self.this_tree.inventory:
 
537
                    self.tt.unversion_file(trans_id)
 
538
                    self.tt.delete_contents(trans_id)
 
539
                file_group = self._dump_conflicts(name, parent_id, file_id, 
 
540
                                                  set_version=True)
 
541
                self._raw_conflicts.append(('contents conflict', file_group))
 
542
 
 
543
    def get_lines(self, tree, file_id):
 
544
        """Return the lines in a file, or an empty list."""
 
545
        if file_id in tree:
 
546
            return tree.get_file(file_id).readlines()
 
547
        else:
 
548
            return []
 
549
 
 
550
    def text_merge(self, file_id, trans_id):
 
551
        """Perform a three-way text merge on a file_id"""
 
552
        # it's possible that we got here with base as a different type.
 
553
        # if so, we just want two-way text conflicts.
 
554
        if file_id in self.base_tree and \
 
555
            self.base_tree.kind(file_id) == "file":
 
556
            base_lines = self.get_lines(self.base_tree, file_id)
 
557
        else:
 
558
            base_lines = []
 
559
        other_lines = self.get_lines(self.other_tree, file_id)
 
560
        this_lines = self.get_lines(self.this_tree, file_id)
 
561
        m3 = Merge3(base_lines, this_lines, other_lines)
 
562
        start_marker = "!START OF MERGE CONFLICT!" + "I HOPE THIS IS UNIQUE"
 
563
        if self.show_base is True:
 
564
            base_marker = '|' * 7
 
565
        else:
 
566
            base_marker = None
 
567
 
 
568
        def iter_merge3(retval):
 
569
            retval["text_conflicts"] = False
 
570
            for line in m3.merge_lines(name_a = "TREE", 
 
571
                                       name_b = "MERGE-SOURCE", 
 
572
                                       name_base = "BASE-REVISION",
 
573
                                       start_marker=start_marker, 
 
574
                                       base_marker=base_marker,
 
575
                                       reprocess=self.reprocess):
 
576
                if line.startswith(start_marker):
 
577
                    retval["text_conflicts"] = True
 
578
                    yield line.replace(start_marker, '<' * 7)
 
579
                else:
 
580
                    yield line
 
581
        retval = {}
 
582
        merge3_iterator = iter_merge3(retval)
 
583
        self.tt.create_file(merge3_iterator, trans_id)
 
584
        if retval["text_conflicts"] is True:
 
585
            self._raw_conflicts.append(('text conflict', trans_id))
 
586
            name = self.tt.final_name(trans_id)
 
587
            parent_id = self.tt.final_parent(trans_id)
 
588
            file_group = self._dump_conflicts(name, parent_id, file_id, 
 
589
                                              this_lines, base_lines,
 
590
                                              other_lines)
 
591
            file_group.append(trans_id)
 
592
 
 
593
    def _dump_conflicts(self, name, parent_id, file_id, this_lines=None, 
 
594
                        base_lines=None, other_lines=None, set_version=False,
 
595
                        no_base=False):
 
596
        """Emit conflict files.
 
597
        If this_lines, base_lines, or other_lines are omitted, they will be
 
598
        determined automatically.  If set_version is true, the .OTHER, .THIS
 
599
        or .BASE (in that order) will be created as versioned files.
 
600
        """
 
601
        data = [('OTHER', self.other_tree, other_lines), 
 
602
                ('THIS', self.this_tree, this_lines)]
 
603
        if not no_base:
 
604
            data.append(('BASE', self.base_tree, base_lines))
 
605
        versioned = False
 
606
        file_group = []
 
607
        for suffix, tree, lines in data:
 
608
            if file_id in tree:
 
609
                trans_id = self._conflict_file(name, parent_id, tree, file_id,
 
610
                                               suffix, lines)
 
611
                file_group.append(trans_id)
 
612
                if set_version and not versioned:
 
613
                    self.tt.version_file(file_id, trans_id)
 
614
                    versioned = True
 
615
        return file_group
 
616
           
 
617
    def _conflict_file(self, name, parent_id, tree, file_id, suffix, 
 
618
                       lines=None):
 
619
        """Emit a single conflict file."""
 
620
        name = name + '.' + suffix
 
621
        trans_id = self.tt.create_path(name, parent_id)
 
622
        entry = tree.inventory[file_id]
 
623
        create_by_entry(self.tt, entry, tree, trans_id, lines)
 
624
        return trans_id
 
625
 
 
626
    def merge_executable(self, file_id, file_status):
 
627
        """Perform a merge on the execute bit."""
 
628
        if file_status == "deleted":
 
629
            return
 
630
        trans_id = self.tt.get_trans_id(file_id)
 
631
        try:
 
632
            if self.tt.final_kind(trans_id) != "file":
 
633
                return
 
634
        except NoSuchFile:
 
635
            return
 
636
        winner = self.scalar_three_way(self.this_tree, self.base_tree, 
 
637
                                       self.other_tree, file_id, 
 
638
                                       self.executable)
 
639
        if winner == "conflict":
 
640
        # There must be a None in here, if we have a conflict, but we
 
641
        # need executability since file status was not deleted.
 
642
            if self.other_tree.is_executable(file_id) is None:
 
643
                winner = "this"
 
644
            else:
 
645
                winner = "other"
 
646
        if winner == "this":
 
647
            if file_status == "modified":
 
648
                executability = self.this_tree.is_executable(file_id)
 
649
                if executability is not None:
 
650
                    trans_id = self.tt.get_trans_id(file_id)
 
651
                    self.tt.set_executability(executability, trans_id)
 
652
        else:
 
653
            assert winner == "other"
 
654
            if file_id in self.other_tree:
 
655
                executability = self.other_tree.is_executable(file_id)
 
656
            elif file_id in self.this_tree:
 
657
                executability = self.this_tree.is_executable(file_id)
 
658
            elif file_id in self.base_tree:
 
659
                executability = self.base_tree.is_executable(file_id)
 
660
            if executability is not None:
 
661
                trans_id = self.tt.get_trans_id(file_id)
 
662
                self.tt.set_executability(executability, trans_id)
 
663
 
 
664
    def cook_conflicts(self):
 
665
        """Convert all conflicts into a form that doesn't depend on trans_id"""
 
666
        name_conflicts = {}
 
667
        fp = FinalPaths(self.tt)
 
668
        for conflict in self._raw_conflicts:
 
669
            conflict_type = conflict[0]
 
670
            if conflict_type in ('name conflict', 'parent conflict'):
 
671
                trans_id = conflict[1]
 
672
                conflict_args = conflict[2:]
 
673
                if trans_id not in name_conflicts:
 
674
                    name_conflicts[trans_id] = {}
 
675
                unique_add(name_conflicts[trans_id], conflict_type, 
 
676
                           conflict_args)
 
677
            if conflict_type == 'contents conflict':
 
678
                for trans_id in conflict[1]:
 
679
                    file_id = self.tt.final_file_id(trans_id)
 
680
                    if file_id is not None:
 
681
                        break
 
682
                path = fp.get_path(trans_id)
 
683
                for suffix in ('.BASE', '.THIS', '.OTHER'):
 
684
                    if path.endswith(suffix):
 
685
                        path = path[:-len(suffix)]
 
686
                        break
 
687
                self.cooked_conflicts.append((conflict_type, file_id, path))
 
688
            if conflict_type == 'text conflict':
 
689
                trans_id = conflict[1]
 
690
                path = fp.get_path(trans_id)
 
691
                file_id = self.tt.final_file_id(trans_id)
 
692
                self.cooked_conflicts.append((conflict_type, file_id, path))
 
693
 
 
694
        for trans_id, conflicts in name_conflicts.iteritems():
 
695
            try:
 
696
                this_parent, other_parent = conflicts['parent conflict']
 
697
                assert this_parent != other_parent
 
698
            except KeyError:
 
699
                this_parent = other_parent = \
 
700
                    self.tt.final_file_id(self.tt.final_parent(trans_id))
 
701
            try:
 
702
                this_name, other_name = conflicts['name conflict']
 
703
                assert this_name != other_name
 
704
            except KeyError:
 
705
                this_name = other_name = self.tt.final_name(trans_id)
 
706
            other_path = fp.get_path(trans_id)
 
707
            if this_parent is not None:
 
708
                this_parent_path = \
 
709
                    fp.get_path(self.tt.get_trans_id(this_parent))
 
710
                this_path = os.path.join(this_parent_path, this_name)
 
711
            else:
 
712
                this_path = "<deleted>"
 
713
            file_id = self.tt.final_file_id(trans_id)
 
714
            self.cooked_conflicts.append(('path conflict', file_id, this_path, 
 
715
                                         other_path))
 
716
 
 
717
 
 
718
def conflicts_strings(conflicts):
 
719
    """Generate strings for the provided conflicts"""
 
720
    for conflict in conflicts:
 
721
        conflict_type = conflict[0]
 
722
        if conflict_type == 'text conflict':
 
723
            yield 'Text conflict in %s' % conflict[2]
 
724
        elif conflict_type == 'contents conflict':
 
725
            yield 'Contents conflict in %s' % conflict[2]
 
726
        elif conflict_type == 'path conflict':
 
727
            yield 'Path conflict: %s / %s' % conflict[2:]
 
728
 
 
729
 
 
730
class WeaveMerger(Merge3Merger):
 
731
    """Merger that does weave merges."""
 
732
    supports_reprocess = False
 
733
    supports_show_base = False
 
734
 
 
735
    def __init__(self, working_tree, this_tree, base_tree, other_tree):
 
736
        self.this_revision_tree = self._get_revision_tree(this_tree)
 
737
        self.other_revision_tree = self._get_revision_tree(other_tree)
 
738
        super(WeaveMerger, self).__init__(working_tree, this_tree, 
 
739
                                          base_tree, other_tree)
 
740
 
 
741
    def _get_revision_tree(self, tree):
 
742
        """Return a revision tree releated to this tree.
 
743
        If the tree is a WorkingTree, the basis will be returned.
 
744
        """
 
745
        if getattr(tree, 'get_weave', False) is False:
 
746
            # If we have a WorkingTree, try using the basis
 
747
            return tree.branch.basis_tree()
 
748
        else:
 
749
            return tree
 
750
 
 
751
    def _check_file(self, file_id):
 
752
        """Check that the revision tree's version of the file matches."""
 
753
        for tree, rt in ((self.this_tree, self.this_revision_tree), 
 
754
                         (self.other_tree, self.other_revision_tree)):
 
755
            if rt is tree:
 
756
                continue
 
757
            if tree.get_file_sha1(file_id) != rt.get_file_sha1(file_id):
 
758
                raise WorkingTreeNotRevision(self.this_tree)
 
759
 
 
760
    def _merged_lines(self, file_id):
 
761
        """Generate the merged lines.
 
762
        There is no distinction between lines that are meant to contain <<<<<<<
 
763
        and conflicts.
 
764
        """
 
765
        weave = self.this_revision_tree.get_weave(file_id)
 
766
        this_revision_id = self.this_revision_tree.inventory[file_id].revision
 
767
        other_revision_id = \
 
768
            self.other_revision_tree.inventory[file_id].revision
 
769
        this_i = weave.lookup(this_revision_id)
 
770
        other_i = weave.lookup(other_revision_id)
 
771
        plan =  weave.plan_merge(this_i, other_i)
 
772
        return weave.weave_merge(plan)
 
773
 
 
774
    def text_merge(self, file_id, trans_id):
 
775
        """Perform a (weave) text merge for a given file and file-id.
 
776
        If conflicts are encountered, .THIS and .OTHER files will be emitted,
 
777
        and a conflict will be noted.
 
778
        """
 
779
        self._check_file(file_id)
 
780
        lines = self._merged_lines(file_id)
 
781
        conflicts = '<<<<<<<\n' in lines
 
782
        self.tt.create_file(lines, trans_id)
 
783
        if conflicts:
 
784
            self._raw_conflicts.append(('text conflict', trans_id))
 
785
            name = self.tt.final_name(trans_id)
 
786
            parent_id = self.tt.final_parent(trans_id)
 
787
            file_group = self._dump_conflicts(name, parent_id, file_id, 
 
788
                                              no_base=True)
 
789
            file_group.append(trans_id)
 
790
 
 
791
 
 
792
class Diff3Merger(Merge3Merger):
 
793
    """Use good ol' diff3 to do text merges"""
 
794
    def dump_file(self, temp_dir, name, tree, file_id):
 
795
        out_path = pathjoin(temp_dir, name)
 
796
        out_file = file(out_path, "wb")
 
797
        in_file = tree.get_file(file_id)
 
798
        for line in in_file:
 
799
            out_file.write(line)
 
800
        return out_path
 
801
 
 
802
    def text_merge(self, file_id, trans_id):
 
803
        """Perform a diff3 merge using a specified file-id and trans-id.
 
804
        If conflicts are encountered, .BASE, .THIS. and .OTHER conflict files
 
805
        will be dumped, and a will be conflict noted.
 
806
        """
 
807
        import bzrlib.patch
 
808
        temp_dir = mkdtemp(prefix="bzr-")
 
809
        try:
 
810
            new_file = os.path.join(temp_dir, "new")
 
811
            this = self.dump_file(temp_dir, "this", self.this_tree, file_id)
 
812
            base = self.dump_file(temp_dir, "base", self.base_tree, file_id)
 
813
            other = self.dump_file(temp_dir, "other", self.other_tree, file_id)
 
814
            status = bzrlib.patch.diff3(new_file, this, base, other)
 
815
            if status not in (0, 1):
 
816
                raise BzrError("Unhandled diff3 exit code")
 
817
            self.tt.create_file(file(new_file, "rb"), trans_id)
 
818
            if status == 1:
 
819
                name = self.tt.final_name(trans_id)
 
820
                parent_id = self.tt.final_parent(trans_id)
 
821
                self._dump_conflicts(name, parent_id, file_id)
 
822
            self._raw_conflicts.append(('text conflict', trans_id))
 
823
        finally:
 
824
            rmtree(temp_dir)
 
825
 
 
826
 
 
827
def merge_inner(this_branch, other_tree, base_tree, ignore_zero=False,
 
828
                backup_files=False, 
 
829
                merge_type=Merge3Merger, 
 
830
                interesting_ids=None, 
 
831
                show_base=False, 
 
832
                reprocess=False, 
 
833
                other_rev_id=None,
 
834
                interesting_files=None,
 
835
                this_tree=None):
 
836
    """Primary interface for merging. 
 
837
 
 
838
        typical use is probably 
 
839
        'merge_inner(branch, branch.get_revision_tree(other_revision),
 
840
                     branch.get_revision_tree(base_revision))'
 
841
        """
 
842
    if this_tree is None:
 
843
        this_tree = this_branch.working_tree()
 
844
    merger = Merger(this_branch, other_tree, base_tree, this_tree=this_tree)
 
845
    merger.backup_files = backup_files
 
846
    merger.merge_type = merge_type
 
847
    merger.interesting_ids = interesting_ids
 
848
    if interesting_files:
 
849
        assert not interesting_ids, ('Only supply interesting_ids'
 
850
                                     ' or interesting_files')
 
851
        merger._set_interesting_files(interesting_files)
 
852
    merger.show_base = show_base 
 
853
    merger.reprocess = reprocess
 
854
    merger.other_rev_id = other_rev_id
 
855
    merger.other_basis = other_rev_id
 
856
    return merger.do_merge()
 
857
 
 
858
 
 
859
merge_types = {     "merge3": (Merge3Merger, "Native diff3-style merge"), 
 
860
                     "diff3": (Diff3Merger,  "Merge using external diff3"),
 
861
                     'weave': (WeaveMerger, "Weave-based merge")
 
862
              }