/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

  • Committer: Aaron Bentley
  • Date: 2006-09-28 22:56:04 UTC
  • mto: (1731.2.8 nested-trees)
  • mto: This revision was merged to the branch mainline in revision 2078.
  • Revision ID: aaron.bentley@utoronto.ca-20060928225604-415f1396ea1d2b97
Fix EmptyTree's default include_root

Show diffs side-by-side

added added

removed removed

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