/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: Martin Pool
  • Date: 2005-08-05 20:51:14 UTC
  • Revision ID: mbp@sourcefrog.net-20050805205114-8b7aba0fd1992c8a
todo

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 readonly_path(self, id):
 
167
        if id not in self.tree:
 
168
            return None
 
169
        if self.root is not None:
 
170
            return self.tree.abspath(self.tree.id2path(id))
 
171
        else:
 
172
            if self.tree.inventory[id].kind in ("directory", "root_directory"):
 
173
                return self.tempdir
 
174
            if not self.cached.has_key(id):
 
175
                path = os.path.join(self.tempdir, "texts", id)
 
176
                outfile = file(path, "wb")
 
177
                outfile.write(self.tree.get_file(id).read())
 
178
                assert(os.path.exists(path))
 
179
                self.cached[id] = path
 
180
            return self.cached[id]
 
181
 
 
182
 
 
183
 
 
184
def merge(other_revision, base_revision,
 
185
          check_clean=True, ignore_zero=False,
 
186
          this_dir=None, backup_files=False, merge_type=ApplyMerge3,
 
187
          file_list=None):
 
188
    """Merge changes into a tree.
 
189
 
 
190
    base_revision
 
191
        Base for three-way merge.
 
192
    other_revision
 
193
        Other revision for three-way merge.
 
194
    this_dir
 
195
        Directory to merge changes into; '.' by default.
 
196
    check_clean
 
197
        If true, this_dir must have no uncommitted changes before the
 
198
        merge begins.
 
199
    """
 
200
    tempdir = tempfile.mkdtemp(prefix="bzr-")
 
201
    try:
 
202
        if this_dir is None:
 
203
            this_dir = '.'
 
204
        this_branch = find_branch(this_dir)
 
205
        if check_clean:
 
206
            changes = compare_trees(this_branch.working_tree(), 
 
207
                                    this_branch.basis_tree(), False)
 
208
            if changes.has_changed():
 
209
                raise BzrCommandError("Working tree has uncommitted changes.")
 
210
        other_branch, other_tree = get_tree(other_revision, tempdir, "other")
 
211
        if base_revision == [None, None]:
 
212
            if other_revision[1] == -1:
 
213
                o_revno = None
 
214
            else:
 
215
                o_revno = other_revision[1]
 
216
            base_revno = this_branch.common_ancestor(other_branch, 
 
217
                                                     other_revno=o_revno)[0]
 
218
            if base_revno is None:
 
219
                raise UnrelatedBranches()
 
220
            base_revision = ['.', base_revno]
 
221
        base_branch, base_tree = get_tree(base_revision, tempdir, "base")
 
222
        if file_list is None:
 
223
            interesting_ids = None
 
224
        else:
 
225
            interesting_ids = set()
 
226
            this_tree = this_branch.working_tree()
 
227
            for fname in file_list:
 
228
                path = this_branch.relpath(fname)
 
229
                found_id = False
 
230
                for tree in (this_tree, base_tree.tree, other_tree.tree):
 
231
                    file_id = tree.inventory.path2id(path)
 
232
                    if file_id is not None:
 
233
                        interesting_ids.add(file_id)
 
234
                        found_id = True
 
235
                if not found_id:
 
236
                    raise BzrCommandError("%s is not a source file in any"
 
237
                                          " tree." % fname)
 
238
        merge_inner(this_branch, other_tree, base_tree, tempdir, 
 
239
                    ignore_zero=ignore_zero, backup_files=backup_files, 
 
240
                    merge_type=merge_type, interesting_ids=interesting_ids)
 
241
    finally:
 
242
        shutil.rmtree(tempdir)
 
243
 
 
244
 
 
245
def set_interesting(inventory_a, inventory_b, interesting_ids):
 
246
    """Mark files whose ids are in interesting_ids as interesting
 
247
    """
 
248
    for inventory in (inventory_a, inventory_b):
 
249
        for path, source_file in inventory.iteritems():
 
250
             source_file.interesting = source_file.id in interesting_ids
 
251
 
 
252
 
 
253
def set_optimized(tree_a, tree_b, inventory_a, inventory_b):
 
254
    """Mark files that have changed texts as interesting
 
255
    """
 
256
    for file_id in tree_a.tree.inventory:
 
257
        if file_id not in tree_b.tree.inventory:
 
258
            continue
 
259
        entry_a = tree_a.tree.inventory[file_id]
 
260
        entry_b = tree_b.tree.inventory[file_id]
 
261
        if (entry_a.kind, entry_b.kind) != ("file", "file"):
 
262
            continue
 
263
        if None in (entry_a.text_id, entry_b.text_id):
 
264
            continue
 
265
        if entry_a.text_id != entry_b.text_id:
 
266
            continue
 
267
        inventory_a[abspath(tree_a.tree, file_id)].interesting = False
 
268
        inventory_b[abspath(tree_b.tree, file_id)].interesting = False
 
269
 
 
270
 
 
271
def generate_cset_optimized(tree_a, tree_b, inventory_a, inventory_b,
 
272
                            interesting_ids=None):
 
273
    """Generate a changeset, with preprocessing to select interesting files.
 
274
    using the text_id to mark really-changed files.
 
275
    This permits blazing comparisons when text_ids are present.  It also
 
276
    disables metadata comparison for files with identical texts.
 
277
    """ 
 
278
    if interesting_ids is None:
 
279
        set_optimized(tree_a, tree_b, inventory_a, inventory_b)
 
280
    else:
 
281
        set_interesting(inventory_a, inventory_b, interesting_ids)
 
282
    cset =  generate_changeset(tree_a, tree_b, inventory_a, inventory_b)
 
283
    for entry in cset.entries.itervalues():
 
284
        entry.metadata_change = None
 
285
    return cset
 
286
 
 
287
 
 
288
def merge_inner(this_branch, other_tree, base_tree, tempdir, 
 
289
                ignore_zero=False, merge_type=ApplyMerge3, backup_files=False,
 
290
                interesting_ids=None):
 
291
 
 
292
    def merge_factory(base_file, other_file):
 
293
        contents_change = merge_type(base_file, other_file)
 
294
        if backup_files:
 
295
            contents_change = BackupBeforeChange(contents_change)
 
296
        return contents_change
 
297
    
 
298
    def generate_cset(tree_a, tree_b, inventory_a, inventory_b):
 
299
        return generate_cset_optimized(tree_a, tree_b, inventory_a, inventory_b,
 
300
                                       interesting_ids)
 
301
 
 
302
    this_tree = get_tree((this_branch.base, None), tempdir, "this")[1]
 
303
 
 
304
    def get_inventory(tree):
 
305
        return tree.inventory
 
306
 
 
307
    inv_changes = merge_flex(this_tree, base_tree, other_tree,
 
308
                             generate_cset, get_inventory,
 
309
                             MergeConflictHandler(base_tree.root,
 
310
                                                  ignore_zero=ignore_zero),
 
311
                             merge_factory=merge_factory)
 
312
 
 
313
    adjust_ids = []
 
314
    for id, path in inv_changes.iteritems():
 
315
        if path is not None:
 
316
            if path == '.':
 
317
                path = ''
 
318
            else:
 
319
                assert path.startswith('./')
 
320
            path = path[2:]
 
321
        adjust_ids.append((path, id))
 
322
    this_branch.set_inventory(regen_inventory(this_branch, this_tree.root, adjust_ids))
 
323
 
 
324
 
 
325
def regen_inventory(this_branch, root, new_entries):
 
326
    old_entries = this_branch.read_working_inventory()
 
327
    new_inventory = {}
 
328
    by_path = {}
 
329
    for file_id in old_entries:
 
330
        entry = old_entries[file_id]
 
331
        path = old_entries.id2path(file_id)
 
332
        new_inventory[file_id] = (path, file_id, entry.parent_id, entry.kind)
 
333
        by_path[path] = file_id
 
334
    
 
335
    deletions = 0
 
336
    insertions = 0
 
337
    new_path_list = []
 
338
    for path, file_id in new_entries:
 
339
        if path is None:
 
340
            del new_inventory[file_id]
 
341
            deletions += 1
 
342
        else:
 
343
            new_path_list.append((path, file_id))
 
344
            if file_id not in old_entries:
 
345
                insertions += 1
 
346
    # Ensure no file is added before its parent
 
347
    new_path_list.sort()
 
348
    for path, file_id in new_path_list:
 
349
        if path == '':
 
350
            parent = None
 
351
        else:
 
352
            parent = by_path[os.path.dirname(path)]
 
353
        kind = bzrlib.osutils.file_kind(os.path.join(root, path))
 
354
        new_inventory[file_id] = (path, file_id, parent, kind)
 
355
        by_path[path] = file_id 
 
356
 
 
357
    # Get a list in insertion order
 
358
    new_inventory_list = new_inventory.values()
 
359
    mutter ("""Inventory regeneration:
 
360
old length: %i insertions: %i deletions: %i new_length: %i"""\
 
361
        % (len(old_entries), insertions, deletions, len(new_inventory_list)))
 
362
    assert len(new_inventory_list) == len(old_entries) + insertions - deletions
 
363
    new_inventory_list.sort()
 
364
    return new_inventory_list
 
365
 
 
366
merge_types = {     "merge3": (ApplyMerge3, "Native diff3-style merge"), 
 
367
                     "diff3": (Diff3Merge,  "Merge using external diff3")
 
368
              }
 
369