~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/merge.py

  • Committer: Martin Pool
  • Date: 2005-08-11 17:56:27 UTC
  • Revision ID: mbp@sourcefrog.net-20050811175627-0bb70e4f236e1d26
- add some test cases for mdiff

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
from bzrlib.merge_core import merge_flex, ApplyMerge3, BackupBeforeChange
 
2
from bzrlib.changeset import generate_changeset, ExceptionConflictHandler
 
3
from bzrlib.changeset import Inventory, Diff3Merge
 
4
from bzrlib import find_branch
 
5
import bzrlib.osutils
 
6
from bzrlib.errors import BzrCommandError
 
7
from bzrlib.delta import compare_trees
 
8
from trace import mutter, warning
 
9
import os.path
 
10
import tempfile
 
11
import shutil
 
12
import errno
 
13
 
 
14
 
 
15
# comments from abentley on irc: merge happens in two stages, each
 
16
# of which generates a changeset object
 
17
 
 
18
# stage 1: generate OLD->OTHER,
 
19
# stage 2: use MINE and OLD->OTHER to generate MINE -> RESULT
 
20
 
 
21
class UnrelatedBranches(BzrCommandError):
 
22
    def __init__(self):
 
23
        msg = "Branches have no common ancestor, and no base revision"\
 
24
            " specified."
 
25
        BzrCommandError.__init__(self, msg)
 
26
 
 
27
 
 
28
class MergeConflictHandler(ExceptionConflictHandler):
 
29
    """Handle conflicts encountered while merging.
 
30
 
 
31
    This subclasses ExceptionConflictHandler, so that any types of
 
32
    conflict that are not explicitly handled cause an exception and
 
33
    terminate the merge.
 
34
    """
 
35
    def __init__(self, dir, ignore_zero=False):
 
36
        ExceptionConflictHandler.__init__(self, dir)
 
37
        self.conflicts = 0
 
38
        self.ignore_zero = ignore_zero
 
39
 
 
40
    def copy(self, source, dest):
 
41
        """Copy the text and mode of a file
 
42
        :param source: The path of the file to copy
 
43
        :param dest: The distination file to create
 
44
        """
 
45
        s_file = file(source, "rb")
 
46
        d_file = file(dest, "wb")
 
47
        for line in s_file:
 
48
            d_file.write(line)
 
49
        os.chmod(dest, 0777 & os.stat(source).st_mode)
 
50
 
 
51
    def add_suffix(self, name, suffix, last_new_name=None):
 
52
        """Rename a file to append a suffix.  If the new name exists, the
 
53
        suffix is added repeatedly until a non-existant name is found
 
54
 
 
55
        :param name: The path of the file
 
56
        :param suffix: The suffix to append
 
57
        :param last_new_name: (used for recursive calls) the last name tried
 
58
        """
 
59
        if last_new_name is None:
 
60
            last_new_name = name
 
61
        new_name = last_new_name+suffix
 
62
        try:
 
63
            os.rename(name, new_name)
 
64
            return new_name
 
65
        except OSError, e:
 
66
            if e.errno != errno.EEXIST and e.errno != errno.ENOTEMPTY:
 
67
                raise
 
68
            return self.add_suffix(name, suffix, last_new_name=new_name)
 
69
 
 
70
    def conflict(self, text):
 
71
        warning(text)
 
72
        self.conflicts += 1
 
73
        
 
74
 
 
75
    def merge_conflict(self, new_file, this_path, base_path, other_path):
 
76
        """
 
77
        Handle diff3 conflicts by producing a .THIS, .BASE and .OTHER.  The
 
78
        main file will be a version with diff3 conflicts.
 
79
        :param new_file: Path to the output file with diff3 markers
 
80
        :param this_path: Path to the file text for the THIS tree
 
81
        :param base_path: Path to the file text for the BASE tree
 
82
        :param other_path: Path to the file text for the OTHER tree
 
83
        """
 
84
        self.add_suffix(this_path, ".THIS")
 
85
        self.copy(base_path, this_path+".BASE")
 
86
        self.copy(other_path, this_path+".OTHER")
 
87
        os.rename(new_file, this_path)
 
88
        self.conflict("Diff3 conflict encountered in %s" % this_path)
 
89
 
 
90
    def new_contents_conflict(self, filename, other_contents):
 
91
        """Conflicting contents for newly added file."""
 
92
        self.copy(other_contents, filename + ".OTHER")
 
93
        self.conflict("Conflict in newly added file %s" % filename)
 
94
    
 
95
 
 
96
    def target_exists(self, entry, target, old_path):
 
97
        """Handle the case when the target file or dir exists"""
 
98
        moved_path = self.add_suffix(target, ".moved")
 
99
        self.conflict("Moved existing %s to %s" % (target, moved_path))
 
100
 
 
101
    def rmdir_non_empty(self, filename):
 
102
        """Handle the case where the dir to be removed still has contents"""
 
103
        self.conflict("Directory %s not removed because it is not empty"\
 
104
            % filename)
 
105
        return "skip"
 
106
 
 
107
    def finalize(self):
 
108
        if not self.ignore_zero:
 
109
            print "%d conflicts encountered.\n" % self.conflicts
 
110
            
 
111
class SourceFile(object):
 
112
    def __init__(self, path, id, present=None, isdir=None):
 
113
        self.path = path
 
114
        self.id = id
 
115
        self.present = present
 
116
        self.isdir = isdir
 
117
        self.interesting = True
 
118
 
 
119
    def __repr__(self):
 
120
        return "SourceFile(%s, %s)" % (self.path, self.id)
 
121
 
 
122
def get_tree(treespec, temp_root, label):
 
123
    location, revno = treespec
 
124
    branch = find_branch(location)
 
125
    if revno is None:
 
126
        base_tree = branch.working_tree()
 
127
    elif revno == -1:
 
128
        base_tree = branch.basis_tree()
 
129
    else:
 
130
        base_tree = branch.revision_tree(branch.lookup_revision(revno))
 
131
    temp_path = os.path.join(temp_root, label)
 
132
    os.mkdir(temp_path)
 
133
    return branch, MergeTree(base_tree, temp_path)
 
134
 
 
135
 
 
136
def abspath(tree, file_id):
 
137
    path = tree.inventory.id2path(file_id)
 
138
    if path == "":
 
139
        return "./."
 
140
    return "./" + path
 
141
 
 
142
def file_exists(tree, file_id):
 
143
    return tree.has_filename(tree.id2path(file_id))
 
144
    
 
145
def inventory_map(tree):
 
146
    inventory = {}
 
147
    for file_id in tree.inventory:
 
148
        path = abspath(tree, file_id)
 
149
        inventory[path] = SourceFile(path, file_id)
 
150
    return inventory
 
151
 
 
152
 
 
153
class MergeTree(object):
 
154
    def __init__(self, tree, tempdir):
 
155
        object.__init__(self)
 
156
        if hasattr(tree, "basedir"):
 
157
            self.root = tree.basedir
 
158
        else:
 
159
            self.root = None
 
160
        self.inventory = inventory_map(tree)
 
161
        self.tree = tree
 
162
        self.tempdir = tempdir
 
163
        os.mkdir(os.path.join(self.tempdir, "texts"))
 
164
        self.cached = {}
 
165
 
 
166
    def __contains__(self, file_id):
 
167
        return id in self.tree
 
168
 
 
169
    def get_file_sha1(self, id):
 
170
        return self.tree.get_file_sha1(id)
 
171
 
 
172
    def readonly_path(self, id):
 
173
        if id not in self.tree:
 
174
            return None
 
175
        if self.root is not None:
 
176
            return self.tree.abspath(self.tree.id2path(id))
 
177
        else:
 
178
            if self.tree.inventory[id].kind in ("directory", "root_directory"):
 
179
                return self.tempdir
 
180
            if not self.cached.has_key(id):
 
181
                path = os.path.join(self.tempdir, "texts", id)
 
182
                outfile = file(path, "wb")
 
183
                outfile.write(self.tree.get_file(id).read())
 
184
                assert(os.path.exists(path))
 
185
                self.cached[id] = path
 
186
            return self.cached[id]
 
187
 
 
188
 
 
189
 
 
190
def merge(other_revision, base_revision,
 
191
          check_clean=True, ignore_zero=False,
 
192
          this_dir=None, backup_files=False, merge_type=ApplyMerge3,
 
193
          file_list=None):
 
194
    """Merge changes into a tree.
 
195
 
 
196
    base_revision
 
197
        Base for three-way merge.
 
198
    other_revision
 
199
        Other revision for three-way merge.
 
200
    this_dir
 
201
        Directory to merge changes into; '.' by default.
 
202
    check_clean
 
203
        If true, this_dir must have no uncommitted changes before the
 
204
        merge begins.
 
205
    """
 
206
    tempdir = tempfile.mkdtemp(prefix="bzr-")
 
207
    try:
 
208
        if this_dir is None:
 
209
            this_dir = '.'
 
210
        this_branch = find_branch(this_dir)
 
211
        if check_clean:
 
212
            changes = compare_trees(this_branch.working_tree(), 
 
213
                                    this_branch.basis_tree(), False)
 
214
            if changes.has_changed():
 
215
                raise BzrCommandError("Working tree has uncommitted changes.")
 
216
        other_branch, other_tree = get_tree(other_revision, tempdir, "other")
 
217
        if base_revision == [None, None]:
 
218
            if other_revision[1] == -1:
 
219
                o_revno = None
 
220
            else:
 
221
                o_revno = other_revision[1]
 
222
            base_revno = this_branch.common_ancestor(other_branch, 
 
223
                                                     other_revno=o_revno)[0]
 
224
            if base_revno is None:
 
225
                raise UnrelatedBranches()
 
226
            base_revision = ['.', base_revno]
 
227
        base_branch, base_tree = get_tree(base_revision, tempdir, "base")
 
228
        if file_list is None:
 
229
            interesting_ids = None
 
230
        else:
 
231
            interesting_ids = set()
 
232
            this_tree = this_branch.working_tree()
 
233
            for fname in file_list:
 
234
                path = this_branch.relpath(fname)
 
235
                found_id = False
 
236
                for tree in (this_tree, base_tree.tree, other_tree.tree):
 
237
                    file_id = tree.inventory.path2id(path)
 
238
                    if file_id is not None:
 
239
                        interesting_ids.add(file_id)
 
240
                        found_id = True
 
241
                if not found_id:
 
242
                    raise BzrCommandError("%s is not a source file in any"
 
243
                                          " tree." % fname)
 
244
        merge_inner(this_branch, other_tree, base_tree, tempdir, 
 
245
                    ignore_zero=ignore_zero, backup_files=backup_files, 
 
246
                    merge_type=merge_type, interesting_ids=interesting_ids)
 
247
    finally:
 
248
        shutil.rmtree(tempdir)
 
249
 
 
250
 
 
251
def set_interesting(inventory_a, inventory_b, interesting_ids):
 
252
    """Mark files whose ids are in interesting_ids as interesting
 
253
    """
 
254
    for inventory in (inventory_a, inventory_b):
 
255
        for path, source_file in inventory.iteritems():
 
256
             source_file.interesting = source_file.id in interesting_ids
 
257
 
 
258
 
 
259
def generate_cset_optimized(tree_a, tree_b, inventory_a, inventory_b,
 
260
                            interesting_ids=None):
 
261
    """Generate a changeset.  If interesting_ids is supplied, only changes
 
262
    to those files will be shown.  Metadata changes are stripped.
 
263
    """ 
 
264
    if interesting_ids is not None:
 
265
        set_interesting(inventory_a, inventory_b, interesting_ids)
 
266
    cset =  generate_changeset(tree_a, tree_b, inventory_a, inventory_b)
 
267
    for entry in cset.entries.itervalues():
 
268
        entry.metadata_change = None
 
269
    return cset
 
270
 
 
271
 
 
272
def merge_inner(this_branch, other_tree, base_tree, tempdir, 
 
273
                ignore_zero=False, merge_type=ApplyMerge3, backup_files=False,
 
274
                interesting_ids=None):
 
275
 
 
276
    def merge_factory(base_file, other_file):
 
277
        contents_change = merge_type(base_file, other_file)
 
278
        if backup_files:
 
279
            contents_change = BackupBeforeChange(contents_change)
 
280
        return contents_change
 
281
    
 
282
    def generate_cset(tree_a, tree_b, inventory_a, inventory_b):
 
283
        return generate_cset_optimized(tree_a, tree_b, inventory_a, inventory_b,
 
284
                                       interesting_ids)
 
285
 
 
286
    this_tree = get_tree((this_branch.base, None), tempdir, "this")[1]
 
287
 
 
288
    def get_inventory(tree):
 
289
        return tree.inventory
 
290
 
 
291
    inv_changes = merge_flex(this_tree, base_tree, other_tree,
 
292
                             generate_cset, get_inventory,
 
293
                             MergeConflictHandler(base_tree.root,
 
294
                                                  ignore_zero=ignore_zero),
 
295
                             merge_factory=merge_factory)
 
296
 
 
297
    adjust_ids = []
 
298
    for id, path in inv_changes.iteritems():
 
299
        if path is not None:
 
300
            if path == '.':
 
301
                path = ''
 
302
            else:
 
303
                assert path.startswith('./')
 
304
            path = path[2:]
 
305
        adjust_ids.append((path, id))
 
306
    if len(adjust_ids) > 0:
 
307
        this_branch.set_inventory(regen_inventory(this_branch, this_tree.root,
 
308
                                                  adjust_ids))
 
309
 
 
310
 
 
311
def regen_inventory(this_branch, root, new_entries):
 
312
    old_entries = this_branch.read_working_inventory()
 
313
    new_inventory = {}
 
314
    by_path = {}
 
315
    for file_id in old_entries:
 
316
        entry = old_entries[file_id]
 
317
        path = old_entries.id2path(file_id)
 
318
        new_inventory[file_id] = (path, file_id, entry.parent_id, entry.kind)
 
319
        by_path[path] = file_id
 
320
    
 
321
    deletions = 0
 
322
    insertions = 0
 
323
    new_path_list = []
 
324
    for path, file_id in new_entries:
 
325
        if path is None:
 
326
            del new_inventory[file_id]
 
327
            deletions += 1
 
328
        else:
 
329
            new_path_list.append((path, file_id))
 
330
            if file_id not in old_entries:
 
331
                insertions += 1
 
332
    # Ensure no file is added before its parent
 
333
    new_path_list.sort()
 
334
    for path, file_id in new_path_list:
 
335
        if path == '':
 
336
            parent = None
 
337
        else:
 
338
            parent = by_path[os.path.dirname(path)]
 
339
        kind = bzrlib.osutils.file_kind(os.path.join(root, path))
 
340
        new_inventory[file_id] = (path, file_id, parent, kind)
 
341
        by_path[path] = file_id 
 
342
 
 
343
    # Get a list in insertion order
 
344
    new_inventory_list = new_inventory.values()
 
345
    mutter ("""Inventory regeneration:
 
346
old length: %i insertions: %i deletions: %i new_length: %i"""\
 
347
        % (len(old_entries), insertions, deletions, len(new_inventory_list)))
 
348
    assert len(new_inventory_list) == len(old_entries) + insertions - deletions
 
349
    new_inventory_list.sort()
 
350
    return new_inventory_list
 
351
 
 
352
merge_types = {     "merge3": (ApplyMerge3, "Native diff3-style merge"), 
 
353
                     "diff3": (Diff3Merge,  "Merge using external diff3")
 
354
              }
 
355