/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

Merged John Meinel's integration

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