/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: 2005-10-24 16:49:18 UTC
  • mto: (1185.25.1)
  • mto: This revision was merged to the branch mainline in revision 1488.
  • Revision ID: abentley@panoramicfeedback.com-20051024164918-4e3f89415dc6d7cd
Support for forcing merges of unrelated trees

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 tempfile
 
20
import shutil
 
21
import errno
 
22
 
 
23
import bzrlib.osutils
 
24
import bzrlib.revision
 
25
from bzrlib.merge_core import merge_flex, ApplyMerge3, BackupBeforeChange
 
26
from bzrlib.merge_core import WeaveMerge
 
27
from bzrlib.changeset import generate_changeset, ExceptionConflictHandler
 
28
from bzrlib.changeset import Inventory, Diff3Merge, ReplaceContents
 
29
from bzrlib.branch import Branch
 
30
from bzrlib.errors import BzrCommandError, UnrelatedBranches, NoCommonAncestor
 
31
from bzrlib.errors import NoCommits, WorkingTreeNotRevision
 
32
from bzrlib.delta import compare_trees
 
33
from bzrlib.trace import mutter, warning, note
 
34
from bzrlib.fetch import greedy_fetch, fetch
 
35
from bzrlib.revision import is_ancestor, NULL_REVISION
 
36
from bzrlib.osutils import rename
 
37
from bzrlib.revision import common_ancestor, MultipleRevisionSources
 
38
from bzrlib.errors import NoSuchRevision
 
39
 
 
40
# TODO: Report back as changes are merged in
 
41
 
 
42
# TODO: build_working_dir can be built on something simpler than merge()
 
43
 
 
44
# FIXME: merge() parameters seem oriented towards the command line
 
45
# NOTABUG: merge is a helper for commandline functions.  merge_inner is the
 
46
#          the core functionality.
 
47
 
 
48
# comments from abentley on irc: merge happens in two stages, each
 
49
# of which generates a changeset object
 
50
 
 
51
# stage 1: generate OLD->OTHER,
 
52
# stage 2: use MINE and OLD->OTHER to generate MINE -> RESULT
 
53
 
 
54
class MergeConflictHandler(ExceptionConflictHandler):
 
55
    """Handle conflicts encountered while merging.
 
56
 
 
57
    This subclasses ExceptionConflictHandler, so that any types of
 
58
    conflict that are not explicitly handled cause an exception and
 
59
    terminate the merge.
 
60
    """
 
61
    def __init__(self, this_tree, base_tree, other_tree, ignore_zero=False):
 
62
        ExceptionConflictHandler.__init__(self)
 
63
        self.conflicts = 0
 
64
        self.ignore_zero = ignore_zero
 
65
        self.this_tree = this_tree
 
66
        self.base_tree = base_tree
 
67
        self.other_tree = other_tree
 
68
 
 
69
    def copy(self, source, dest):
 
70
        """Copy the text and mode of a file
 
71
        :param source: The path of the file to copy
 
72
        :param dest: The distination file to create
 
73
        """
 
74
        s_file = file(source, "rb")
 
75
        d_file = file(dest, "wb")
 
76
        for line in s_file:
 
77
            d_file.write(line)
 
78
        os.chmod(dest, 0777 & os.stat(source).st_mode)
 
79
 
 
80
    def dump(self, lines, dest):
 
81
        """Copy the text and mode of a file
 
82
        :param source: The path of the file to copy
 
83
        :param dest: The distination file to create
 
84
        """
 
85
        d_file = file(dest, "wb")
 
86
        for line in lines:
 
87
            d_file.write(line)
 
88
 
 
89
    def add_suffix(self, name, suffix, last_new_name=None):
 
90
        """Rename a file to append a suffix.  If the new name exists, the
 
91
        suffix is added repeatedly until a non-existant name is found
 
92
 
 
93
        :param name: The path of the file
 
94
        :param suffix: The suffix to append
 
95
        :param last_new_name: (used for recursive calls) the last name tried
 
96
        """
 
97
        if last_new_name is None:
 
98
            last_new_name = name
 
99
        new_name = last_new_name+suffix
 
100
        try:
 
101
            rename(name, new_name)
 
102
            return new_name
 
103
        except OSError, e:
 
104
            if e.errno != errno.EEXIST and e.errno != errno.ENOTEMPTY:
 
105
                raise
 
106
            return self.add_suffix(name, suffix, last_new_name=new_name)
 
107
 
 
108
    def conflict(self, text):
 
109
        warning(text)
 
110
        self.conflicts += 1
 
111
        
 
112
 
 
113
    def merge_conflict(self, new_file, this_path, base_lines, other_lines):
 
114
        """
 
115
        Handle diff3 conflicts by producing a .THIS, .BASE and .OTHER.  The
 
116
        main file will be a version with diff3 conflicts.
 
117
        :param new_file: Path to the output file with diff3 markers
 
118
        :param this_path: Path to the file text for the THIS tree
 
119
        :param base_path: Path to the file text for the BASE tree
 
120
        :param other_path: Path to the file text for the OTHER tree
 
121
        """
 
122
        self.add_suffix(this_path, ".THIS")
 
123
        self.dump(base_lines, this_path+".BASE")
 
124
        self.dump(other_lines, this_path+".OTHER")
 
125
        rename(new_file, this_path)
 
126
        self.conflict("Diff3 conflict encountered in %s" % this_path)
 
127
 
 
128
    def weave_merge_conflict(self, filename, weave, other_i, out_file):
 
129
        """
 
130
        Handle weave conflicts by producing a .THIS, and .OTHER.  The
 
131
        main file will be a version with diff3-style conflicts.
 
132
        """
 
133
        self.add_suffix(filename, ".THIS")
 
134
        out_file.commit()
 
135
        self.dump(weave.get_iter(other_i), filename+".OTHER")
 
136
        self.conflict("Text conflict encountered in %s" % filename)
 
137
 
 
138
    def new_contents_conflict(self, filename, other_contents):
 
139
        """Conflicting contents for newly added file."""
 
140
        other_contents(filename + ".OTHER", self, False)
 
141
        self.conflict("Conflict in newly added file %s" % filename)
 
142
    
 
143
 
 
144
    def target_exists(self, entry, target, old_path):
 
145
        """Handle the case when the target file or dir exists"""
 
146
        moved_path = self.add_suffix(target, ".moved")
 
147
        self.conflict("Moved existing %s to %s" % (target, moved_path))
 
148
 
 
149
    def rmdir_non_empty(self, filename):
 
150
        """Handle the case where the dir to be removed still has contents"""
 
151
        self.conflict("Directory %s not removed because it is not empty"\
 
152
            % filename)
 
153
        return "skip"
 
154
 
 
155
    def rem_contents_conflict(self, filename, this_contents, base_contents):
 
156
        base_contents(filename+".BASE", self, False)
 
157
        this_contents(filename+".THIS", self, False)
 
158
        return ReplaceContents(this_contents, None)
 
159
 
 
160
    def rem_contents_conflict(self, filename, this_contents, base_contents):
 
161
        base_contents(filename+".BASE", self, False)
 
162
        this_contents(filename+".THIS", self, False)
 
163
        self.conflict("Other branch deleted locally modified file %s" %
 
164
                      filename)
 
165
        return ReplaceContents(this_contents, None)
 
166
 
 
167
    def abs_this_path(self, file_id):
 
168
        """Return the absolute path for a file_id in the this tree."""
 
169
        return self.this_tree.id2abspath(file_id)
 
170
 
 
171
    def add_missing_parents(self, file_id, tree):
 
172
        """If some of the parents for file_id are missing, add them."""
 
173
        entry = tree.inventory[file_id]
 
174
        if entry.parent_id not in self.this_tree:
 
175
            return self.create_all_missing(entry.parent_id, tree)
 
176
        else:
 
177
            return self.abs_this_path(entry.parent_id)
 
178
 
 
179
    def create_all_missing(self, file_id, tree):
 
180
        """Add contents for a file_id and all its parents to a tree."""
 
181
        entry = tree.inventory[file_id]
 
182
        if entry.parent_id is not None and entry.parent_id not in self.this_tree:
 
183
            abspath = self.create_all_missing(entry.parent_id, tree)
 
184
        else:
 
185
            abspath = self.abs_this_path(entry.parent_id)
 
186
        entry_path = os.path.join(abspath, entry.name)
 
187
        if not os.path.isdir(entry_path):
 
188
            self.create(file_id, entry_path, tree)
 
189
        return entry_path
 
190
 
 
191
    def create(self, file_id, path, tree, reverse=False):
 
192
        """Uses tree data to create a filesystem object for the file_id"""
 
193
        from changeset import get_contents
 
194
        get_contents(tree, file_id)(path, self, reverse)
 
195
 
 
196
    def missing_for_merge(self, file_id, other_path):
 
197
        """The file_id doesn't exist in THIS, but does in OTHER and BASE"""
 
198
        self.conflict("Other branch modified locally deleted file %s" %
 
199
                      other_path)
 
200
        parent_dir = self.add_missing_parents(file_id, self.other_tree)
 
201
        stem = os.path.join(parent_dir, os.path.basename(other_path))
 
202
        self.create(file_id, stem+".OTHER", self.other_tree)
 
203
        self.create(file_id, stem+".BASE", self.base_tree)
 
204
 
 
205
    def threeway_contents_conflict(filename, this_contents, base_contents,
 
206
                                   other_contents):
 
207
        self.conflict("Three-way conflict merging %s" % filename)
 
208
 
 
209
    def finalize(self):
 
210
        if not self.ignore_zero:
 
211
            note("%d conflicts encountered.\n" % self.conflicts)
 
212
            
 
213
def get_tree(treespec, local_branch=None):
 
214
    location, revno = treespec
 
215
    branch = Branch.open_containing(location)[0]
 
216
    if revno is None:
 
217
        revision = None
 
218
    elif revno == -1:
 
219
        revision = branch.last_revision()
 
220
    else:
 
221
        revision = branch.get_rev_id(revno)
 
222
        if revision is None:
 
223
            revision = NULL_REVISION
 
224
    return branch, get_revid_tree(branch, revision, local_branch)
 
225
 
 
226
def get_revid_tree(branch, revision, local_branch):
 
227
    if revision is None:
 
228
        base_tree = branch.working_tree()
 
229
    else:
 
230
        if local_branch is not None:
 
231
            greedy_fetch(local_branch, branch, revision)
 
232
            base_tree = local_branch.revision_tree(revision)
 
233
        else:
 
234
            base_tree = branch.revision_tree(revision)
 
235
    return base_tree
 
236
 
 
237
 
 
238
def file_exists(tree, file_id):
 
239
    return tree.has_filename(tree.id2path(file_id))
 
240
    
 
241
 
 
242
def build_working_dir(to_dir):
 
243
    """Build a working directory in an empty directory.
 
244
 
 
245
    to_dir is a directory containing branch metadata but no working files,
 
246
    typically constructed by cloning an existing branch. 
 
247
 
 
248
    This is split out as a special idiomatic case of merge.  It could
 
249
    eventually be done by just building the tree directly calling into 
 
250
    lower-level code (e.g. constructing a changeset).
 
251
    """
 
252
    # RBC 20051019 is this not just 'export' ?
 
253
    # Well, export doesn't take care of inventory...
 
254
    this_branch = Branch.open_containing(to_dir)[0]
 
255
    transform_tree(this_branch.working_tree(), this_branch.basis_tree())
 
256
 
 
257
def transform_tree(from_tree, to_tree):
 
258
    merge_inner(from_tree.branch, to_tree, from_tree, ignore_zero=True)
 
259
 
 
260
def merge(other_revision, base_revision,
 
261
          check_clean=True, ignore_zero=False,
 
262
          this_dir=None, backup_files=False, merge_type=ApplyMerge3,
 
263
          file_list=None, show_base=False):
 
264
    """Merge changes into a tree.
 
265
 
 
266
    base_revision
 
267
        list(path, revno) Base for three-way merge.  
 
268
        If [None, None] then a base will be automatically determined.
 
269
    other_revision
 
270
        list(path, revno) Other revision for three-way merge.
 
271
    this_dir
 
272
        Directory to merge changes into; '.' by default.
 
273
    check_clean
 
274
        If true, this_dir must have no uncommitted changes before the
 
275
        merge begins.
 
276
    ignore_zero - If true, suppress the "zero conflicts" message when 
 
277
        there are no conflicts; should be set when doing something we expect
 
278
        to complete perfectly.
 
279
    file_list - If supplied, merge only changes to selected files.
 
280
 
 
281
    All available ancestors of other_revision and base_revision are
 
282
    automatically pulled into the branch.
 
283
 
 
284
    The revno may be -1 to indicate the last revision on the branch, which is
 
285
    the typical case.
 
286
 
 
287
    This function is intended for use from the command line; programmatic
 
288
    clients might prefer to call merge_inner(), which has less magic behavior.
 
289
    """
 
290
    if this_dir is None:
 
291
        this_dir = '.'
 
292
    this_branch = Branch.open_containing(this_dir)[0]
 
293
    if show_base and not merge_type is ApplyMerge3:
 
294
        raise BzrCommandError("Show-base is not supported for this merge"
 
295
                              " type. %s" % merge_type)
 
296
    merger = Merger(this_branch)
 
297
    merger.check_basis(check_clean)
 
298
    merger.set_other(other_revision)
 
299
    merger.set_base(base_revision)
 
300
    merger.backup_files = backup_files
 
301
    merger.merge_type = merge_type 
 
302
    merger.set_interesting_files(file_list)
 
303
    merger.show_base = show_base 
 
304
    merger.conflict_handler = MergeConflictHandler(merger.this_tree, 
 
305
                                                   merger.base_tree, 
 
306
                                                   merger.other_tree,
 
307
                                                   ignore_zero=ignore_zero)
 
308
    conflicts = merger.do_merge()
 
309
    merger.set_pending()
 
310
    return conflicts
 
311
 
 
312
def merge_inner(this_branch, other_tree, base_tree, ignore_zero=False,
 
313
                backup_files=False, merge_type=ApplyMerge3, 
 
314
                interesting_ids=None, show_base=False):
 
315
    """Primary interface for merging. 
 
316
 
 
317
        typical use is probably 
 
318
        'merge_inner(branch, branch.get_revision_tree(other_revision),
 
319
                     branch.get_revision_tree(base_revision))'
 
320
        """
 
321
    merger = Merger(this_branch, other_tree, base_tree)
 
322
    merger.backup_files = False
 
323
    merger.merge_type = ApplyMerge3
 
324
    merger.interesting_ids = interesting_ids
 
325
    merger.show_base = show_base 
 
326
    merger.conflict_handler = MergeConflictHandler(merger.this_tree, base_tree, 
 
327
                                                   other_tree,
 
328
                                                   ignore_zero=ignore_zero)
 
329
    return merger.do_merge()
 
330
 
 
331
 
 
332
class Merger(object):
 
333
    def __init__(self, this_branch, other_tree=None, base_tree=None):
 
334
        object.__init__(self)
 
335
        self.this_branch = this_branch
 
336
        self.this_basis = this_branch.last_revision()
 
337
        self.this_rev_id = None
 
338
        self.this_tree = this_branch.working_tree()
 
339
        self.this_revision_tree = None
 
340
        self.other_tree = other_tree
 
341
        self.base_tree = base_tree
 
342
        self.ignore_zero = False
 
343
        self.backup_files = False
 
344
        self.interesting_ids = None
 
345
        self.show_base = False
 
346
        self.conflict_handler = MergeConflictHandler(self.this_tree, base_tree, 
 
347
                                                     other_tree)
 
348
 
 
349
    def revision_tree(self, revision_id):
 
350
        return self.this_branch.revision_tree(revision_id)
 
351
 
 
352
    def ensure_revision_trees(self):
 
353
        if self.this_revision_tree is None:
 
354
            if self.this_rev_id is None:
 
355
                self.compare_basis()
 
356
            if self.this_rev_id is None:
 
357
                raise WorkingTreeNotRevision(self.this_tree)
 
358
            self.this_revision_tree = self.this_branch.revision_tree(
 
359
                self.this_rev_id)
 
360
 
 
361
        if self.other_rev_id is None:
 
362
            other_basis_tree = self.revision_tree(self.other_basis)
 
363
            changes = compare_trees(self.other_tree, other_basis_tree)
 
364
            if changes.has_changed():
 
365
                raise WorkingTreeNotRevision(self.this_tree)
 
366
            other_rev_id = other_basis
 
367
            self.other_tree = other_basis_tree
 
368
 
 
369
 
 
370
    def file_revisions(self, file_id):
 
371
        self.ensure_revision_trees()
 
372
        def get_id(tree, file_id):
 
373
            revision_id = tree.inventory[file_id].revision
 
374
            assert revision_id is not None
 
375
            return revision_id
 
376
        trees = (self.this_revision_tree, self.other_tree)
 
377
        return [get_id(tree, file_id) for tree in trees]
 
378
            
 
379
 
 
380
    def merge_factory(self, file_id, base, other):
 
381
        if self.merge_type.history_based:
 
382
            t_revid, o_revid = self.file_revisions(file_id)
 
383
            weave = self.this_revision_tree.get_weave(file_id)
 
384
            contents_change = self.merge_type(weave, t_revid, o_revid)
 
385
        else:
 
386
            if self.show_base is True:
 
387
                contents_change = self.merge_type(file_id, base, other, 
 
388
                                                  show_base=True)
 
389
            else:
 
390
                contents_change = self.merge_type(file_id, base, other)
 
391
        if self.backup_files:
 
392
            contents_change = BackupBeforeChange(contents_change)
 
393
        return contents_change
 
394
 
 
395
    def check_basis(self, check_clean):
 
396
        if self.this_basis is None:
 
397
            raise BzrCommandError("This branch has no commits")
 
398
        if check_clean:
 
399
            self.compare_basis()
 
400
            if self.this_basis != self.this_rev_id:
 
401
                raise BzrCommandError("Working tree has uncommitted changes.")
 
402
 
 
403
    def compare_basis(self):
 
404
        changes = compare_trees(self.this_branch.working_tree(), 
 
405
                                self.this_branch.basis_tree(), False)
 
406
        if not changes.has_changed():
 
407
            self.this_rev_id = self.this_basis
 
408
 
 
409
    def set_interesting_files(self, file_list):
 
410
        if file_list is None:
 
411
            self.interesting_ids = None
 
412
            return
 
413
 
 
414
        interesting_ids = set()
 
415
        for fname in file_list:
 
416
            path = self.this_tree.relpath(fname)
 
417
            found_id = False
 
418
            for tree in (self.this_tree, self.base_tree, self.other_tree):
 
419
                file_id = tree.inventory.path2id(path)
 
420
                if file_id is not None:
 
421
                    interesting_ids.add(file_id)
 
422
                    found_id = True
 
423
            if not found_id:
 
424
                raise BzrCommandError("%s is not a source file in any"
 
425
                                      " tree." % fname)
 
426
        self.interesting_ids = interesting_ids
 
427
 
 
428
    def set_pending(self):
 
429
        if not self.base_is_ancestor:
 
430
            return
 
431
        if self.other_rev_id is None:
 
432
            return
 
433
        if self.other_rev_id in self.this_branch.get_ancestry(self.this_basis):
 
434
            return
 
435
        self.this_branch.add_pending_merge(self.other_rev_id)
 
436
 
 
437
    def set_other(self, other_revision):
 
438
        other_branch, self.other_tree = get_tree(other_revision, 
 
439
                                                 self.this_branch)
 
440
        if other_revision[1] == -1:
 
441
            self.other_rev_id = other_branch.last_revision()
 
442
            if self.other_rev_id is None:
 
443
                raise NoCommits(other_branch)
 
444
            self.other_basis = self.other_rev_id
 
445
        elif other_revision[1] is not None:
 
446
            self.other_rev_id = other_branch.get_rev_id(other_revision[1])
 
447
            self.other_basis = self.other_rev_id
 
448
        else:
 
449
            self.other_rev_id = None
 
450
            self.other_basis = other_branch.last_revision()
 
451
            if self.other_basis is None:
 
452
                raise NoCommits(other_branch)
 
453
        fetch(from_branch=other_branch, to_branch=self.this_branch, 
 
454
              last_revision=self.other_basis)
 
455
 
 
456
    def set_base(self, base_revision):
 
457
        mutter("doing merge() with no base_revision specified")
 
458
        if base_revision == [None, None]:
 
459
            try:
 
460
                self.base_rev_id = common_ancestor(self.this_basis, 
 
461
                                                   self.other_basis, 
 
462
                                                   self.this_branch)
 
463
            except NoCommonAncestor:
 
464
                raise UnrelatedBranches()
 
465
            self.base_tree = get_revid_tree(self.this_branch, self.base_rev_id,
 
466
                                            None)
 
467
            self.base_is_ancestor = True
 
468
        else:
 
469
            base_branch, self.base_tree = get_tree(base_revision)
 
470
            if base_revision[1] == -1:
 
471
                self.base_rev_id = base_branch.last_revision()
 
472
            elif base_revision[1] is None:
 
473
                self.base_rev_id = None
 
474
            else:
 
475
                self.base_rev_id = base_branch.get_rev_id(base_revision[1])
 
476
            fetch(from_branch=base_branch, to_branch=self.this_branch)
 
477
            self.base_is_ancestor = is_ancestor(self.this_basis, 
 
478
                                                self.base_rev_id,
 
479
                                                self.this_branch)
 
480
 
 
481
    def do_merge(self):
 
482
        def get_inventory(tree):
 
483
            return tree.inventory
 
484
 
 
485
        inv_changes = merge_flex(self.this_tree, self.base_tree, 
 
486
                                 self.other_tree,
 
487
                                 generate_changeset, get_inventory,
 
488
                                 self.conflict_handler,
 
489
                                 merge_factory=self.merge_factory, 
 
490
                                 interesting_ids=self.interesting_ids)
 
491
 
 
492
        adjust_ids = []
 
493
        for id, path in inv_changes.iteritems():
 
494
            if path is not None:
 
495
                if path == '.':
 
496
                    path = ''
 
497
                else:
 
498
                    assert path.startswith('.' + os.sep), "path is %s" % path
 
499
                path = path[2:]
 
500
            adjust_ids.append((path, id))
 
501
        if len(adjust_ids) > 0:
 
502
            self.this_branch.set_inventory(self.regen_inventory(adjust_ids))
 
503
        conflicts = self.conflict_handler.conflicts
 
504
        self.conflict_handler.finalize()
 
505
        return conflicts
 
506
 
 
507
    def regen_inventory(self, new_entries):
 
508
        old_entries = self.this_branch.read_working_inventory()
 
509
        new_inventory = {}
 
510
        by_path = {}
 
511
        new_entries_map = {} 
 
512
        for path, file_id in new_entries:
 
513
            if path is None:
 
514
                continue
 
515
            new_entries_map[file_id] = path
 
516
 
 
517
        def id2path(file_id):
 
518
            path = new_entries_map.get(file_id)
 
519
            if path is not None:
 
520
                return path
 
521
            entry = old_entries[file_id]
 
522
            if entry.parent_id is None:
 
523
                return entry.name
 
524
            return os.path.join(id2path(entry.parent_id), entry.name)
 
525
            
 
526
        for file_id in old_entries:
 
527
            entry = old_entries[file_id]
 
528
            path = id2path(file_id)
 
529
            new_inventory[file_id] = (path, file_id, entry.parent_id, 
 
530
                                      entry.kind)
 
531
            by_path[path] = file_id
 
532
        
 
533
        deletions = 0
 
534
        insertions = 0
 
535
        new_path_list = []
 
536
        for path, file_id in new_entries:
 
537
            if path is None:
 
538
                del new_inventory[file_id]
 
539
                deletions += 1
 
540
            else:
 
541
                new_path_list.append((path, file_id))
 
542
                if file_id not in old_entries:
 
543
                    insertions += 1
 
544
        # Ensure no file is added before its parent
 
545
        new_path_list.sort()
 
546
        for path, file_id in new_path_list:
 
547
            if path == '':
 
548
                parent = None
 
549
            else:
 
550
                parent = by_path[os.path.dirname(path)]
 
551
            abspath = os.path.join(self.this_tree.basedir, path)
 
552
            kind = bzrlib.osutils.file_kind(abspath)
 
553
            new_inventory[file_id] = (path, file_id, parent, kind)
 
554
            by_path[path] = file_id 
 
555
 
 
556
        # Get a list in insertion order
 
557
        new_inventory_list = new_inventory.values()
 
558
        mutter ("""Inventory regeneration:
 
559
    old length: %i insertions: %i deletions: %i new_length: %i"""\
 
560
            % (len(old_entries), insertions, deletions, 
 
561
               len(new_inventory_list)))
 
562
        assert len(new_inventory_list) == len(old_entries) + insertions\
 
563
            - deletions
 
564
        new_inventory_list.sort()
 
565
        return new_inventory_list
 
566
 
 
567
merge_types = {     "merge3": (ApplyMerge3, "Native diff3-style merge"), 
 
568
                     "diff3": (Diff3Merge,  "Merge using external diff3"),
 
569
                     'weave': (WeaveMerge, "Weave-based merge")
 
570
              }
 
571