/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

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