/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

MergeĀ fromĀ mainline

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