/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

Unbreak bzr push sftp:////

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