~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/merge.py

  • Committer: Martin Pool
  • Date: 2005-05-09 03:03:55 UTC
  • Revision ID: mbp@sourcefrog.net-20050509030355-ad6ab558d1362959
- Don't give an error if the trace file can't be opened

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