/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:00:14 UTC
  • Revision ID: mbp@sourcefrog.net-20050805200014-75236893f666c4bb
- basic rules for hacking bzr

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 target_exists(self, entry, target, old_path):
 
91
        """Handle the case when the target file or dir exists"""
 
92
        moved_path = self.add_suffix(target, ".moved")
 
93
        self.conflict("Moved existing %s to %s" % (target, moved_path))
 
94
 
 
95
    def rmdir_non_empty(self, filename):
 
96
        """Handle the case where the dir to be removed still has contents"""
 
97
        self.conflict("Directory %s not removed because it is not empty"\
 
98
            % filename)
 
99
        return "skip"
 
100
 
 
101
    def finalize(self):
 
102
        if not self.ignore_zero:
 
103
            print "%d conflicts encountered.\n" % self.conflicts
 
104
            
 
105
class SourceFile(object):
 
106
    def __init__(self, path, id, present=None, isdir=None):
 
107
        self.path = path
 
108
        self.id = id
 
109
        self.present = present
 
110
        self.isdir = isdir
 
111
        self.interesting = True
 
112
 
 
113
    def __repr__(self):
 
114
        return "SourceFile(%s, %s)" % (self.path, self.id)
 
115
 
 
116
def get_tree(treespec, temp_root, label):
 
117
    location, revno = treespec
 
118
    branch = find_branch(location)
 
119
    if revno is None:
 
120
        base_tree = branch.working_tree()
 
121
    elif revno == -1:
 
122
        base_tree = branch.basis_tree()
 
123
    else:
 
124
        base_tree = branch.revision_tree(branch.lookup_revision(revno))
 
125
    temp_path = os.path.join(temp_root, label)
 
126
    os.mkdir(temp_path)
 
127
    return branch, MergeTree(base_tree, temp_path)
 
128
 
 
129
 
 
130
def abspath(tree, file_id):
 
131
    path = tree.inventory.id2path(file_id)
 
132
    if path == "":
 
133
        return "./."
 
134
    return "./" + path
 
135
 
 
136
def file_exists(tree, file_id):
 
137
    return tree.has_filename(tree.id2path(file_id))
 
138
    
 
139
def inventory_map(tree):
 
140
    inventory = {}
 
141
    for file_id in tree.inventory:
 
142
        path = abspath(tree, file_id)
 
143
        inventory[path] = SourceFile(path, file_id)
 
144
    return inventory
 
145
 
 
146
 
 
147
class MergeTree(object):
 
148
    def __init__(self, tree, tempdir):
 
149
        object.__init__(self)
 
150
        if hasattr(tree, "basedir"):
 
151
            self.root = tree.basedir
 
152
        else:
 
153
            self.root = None
 
154
        self.inventory = inventory_map(tree)
 
155
        self.tree = tree
 
156
        self.tempdir = tempdir
 
157
        os.mkdir(os.path.join(self.tempdir, "texts"))
 
158
        self.cached = {}
 
159
 
 
160
    def readonly_path(self, id):
 
161
        if id not in self.tree:
 
162
            return None
 
163
        if self.root is not None:
 
164
            return self.tree.abspath(self.tree.id2path(id))
 
165
        else:
 
166
            if self.tree.inventory[id].kind in ("directory", "root_directory"):
 
167
                return self.tempdir
 
168
            if not self.cached.has_key(id):
 
169
                path = os.path.join(self.tempdir, "texts", id)
 
170
                outfile = file(path, "wb")
 
171
                outfile.write(self.tree.get_file(id).read())
 
172
                assert(os.path.exists(path))
 
173
                self.cached[id] = path
 
174
            return self.cached[id]
 
175
 
 
176
 
 
177
 
 
178
def merge(other_revision, base_revision,
 
179
          check_clean=True, ignore_zero=False,
 
180
          this_dir=None, backup_files=False, merge_type=ApplyMerge3,
 
181
          file_list=None):
 
182
    """Merge changes into a tree.
 
183
 
 
184
    base_revision
 
185
        Base for three-way merge.
 
186
    other_revision
 
187
        Other revision for three-way merge.
 
188
    this_dir
 
189
        Directory to merge changes into; '.' by default.
 
190
    check_clean
 
191
        If true, this_dir must have no uncommitted changes before the
 
192
        merge begins.
 
193
    """
 
194
    tempdir = tempfile.mkdtemp(prefix="bzr-")
 
195
    try:
 
196
        if this_dir is None:
 
197
            this_dir = '.'
 
198
        this_branch = find_branch(this_dir)
 
199
        if check_clean:
 
200
            changes = compare_trees(this_branch.working_tree(), 
 
201
                                    this_branch.basis_tree(), False)
 
202
            if changes.has_changed():
 
203
                raise BzrCommandError("Working tree has uncommitted changes.")
 
204
        other_branch, other_tree = get_tree(other_revision, tempdir, "other")
 
205
        if base_revision == [None, None]:
 
206
            if other_revision[1] == -1:
 
207
                o_revno = None
 
208
            else:
 
209
                o_revno = other_revision[1]
 
210
            base_revno = this_branch.common_ancestor(other_branch, 
 
211
                                                     other_revno=o_revno)[0]
 
212
            if base_revno is None:
 
213
                raise UnrelatedBranches()
 
214
            base_revision = ['.', base_revno]
 
215
        base_branch, base_tree = get_tree(base_revision, tempdir, "base")
 
216
        if file_list is None:
 
217
            interesting_ids = None
 
218
        else:
 
219
            interesting_ids = set()
 
220
            this_tree = this_branch.working_tree()
 
221
            for fname in file_list:
 
222
                path = this_branch.relpath(fname)
 
223
                found_id = False
 
224
                for tree in (this_tree, base_tree.tree, other_tree.tree):
 
225
                    file_id = tree.inventory.path2id(path)
 
226
                    if file_id is not None:
 
227
                        interesting_ids.add(file_id)
 
228
                        found_id = True
 
229
                if not found_id:
 
230
                    raise BzrCommandError("%s is not a source file in any"
 
231
                                          " tree." % fname)
 
232
        merge_inner(this_branch, other_tree, base_tree, tempdir, 
 
233
                    ignore_zero=ignore_zero, backup_files=backup_files, 
 
234
                    merge_type=merge_type, interesting_ids=interesting_ids)
 
235
    finally:
 
236
        shutil.rmtree(tempdir)
 
237
 
 
238
 
 
239
def set_interesting(inventory_a, inventory_b, interesting_ids):
 
240
    """Mark files whose ids are in interesting_ids as interesting
 
241
    """
 
242
    for inventory in (inventory_a, inventory_b):
 
243
        for path, source_file in inventory.iteritems():
 
244
             source_file.interesting = source_file.id in interesting_ids
 
245
 
 
246
 
 
247
def set_optimized(tree_a, tree_b, inventory_a, inventory_b):
 
248
    """Mark files that have changed texts as interesting
 
249
    """
 
250
    for file_id in tree_a.tree.inventory:
 
251
        if file_id not in tree_b.tree.inventory:
 
252
            continue
 
253
        entry_a = tree_a.tree.inventory[file_id]
 
254
        entry_b = tree_b.tree.inventory[file_id]
 
255
        if (entry_a.kind, entry_b.kind) != ("file", "file"):
 
256
            continue
 
257
        if None in (entry_a.text_id, entry_b.text_id):
 
258
            continue
 
259
        if entry_a.text_id != entry_b.text_id:
 
260
            continue
 
261
        inventory_a[abspath(tree_a.tree, file_id)].interesting = False
 
262
        inventory_b[abspath(tree_b.tree, file_id)].interesting = False
 
263
 
 
264
 
 
265
def generate_cset_optimized(tree_a, tree_b, inventory_a, inventory_b,
 
266
                            interesting_ids=None):
 
267
    """Generate a changeset, with preprocessing to select interesting files.
 
268
    using the text_id to mark really-changed files.
 
269
    This permits blazing comparisons when text_ids are present.  It also
 
270
    disables metadata comparison for files with identical texts.
 
271
    """ 
 
272
    if interesting_ids is None:
 
273
        set_optimized(tree_a, tree_b, inventory_a, inventory_b)
 
274
    else:
 
275
        set_interesting(inventory_a, inventory_b, interesting_ids)
 
276
    cset =  generate_changeset(tree_a, tree_b, inventory_a, inventory_b)
 
277
    for entry in cset.entries.itervalues():
 
278
        entry.metadata_change = None
 
279
    return cset
 
280
 
 
281
 
 
282
def merge_inner(this_branch, other_tree, base_tree, tempdir, 
 
283
                ignore_zero=False, merge_type=ApplyMerge3, backup_files=False,
 
284
                interesting_ids=None):
 
285
 
 
286
    def merge_factory(base_file, other_file):
 
287
        contents_change = merge_type(base_file, other_file)
 
288
        if backup_files:
 
289
            contents_change = BackupBeforeChange(contents_change)
 
290
        return contents_change
 
291
    
 
292
    def generate_cset(tree_a, tree_b, inventory_a, inventory_b):
 
293
        return generate_cset_optimized(tree_a, tree_b, inventory_a, inventory_b,
 
294
                                       interesting_ids)
 
295
 
 
296
    this_tree = get_tree((this_branch.base, None), tempdir, "this")[1]
 
297
 
 
298
    def get_inventory(tree):
 
299
        return tree.inventory
 
300
 
 
301
    inv_changes = merge_flex(this_tree, base_tree, other_tree,
 
302
                             generate_cset, get_inventory,
 
303
                             MergeConflictHandler(base_tree.root,
 
304
                                                  ignore_zero=ignore_zero),
 
305
                             merge_factory=merge_factory)
 
306
 
 
307
    adjust_ids = []
 
308
    for id, path in inv_changes.iteritems():
 
309
        if path is not None:
 
310
            if path == '.':
 
311
                path = ''
 
312
            else:
 
313
                assert path.startswith('./')
 
314
            path = path[2:]
 
315
        adjust_ids.append((path, id))
 
316
    this_branch.set_inventory(regen_inventory(this_branch, this_tree.root, adjust_ids))
 
317
 
 
318
 
 
319
def regen_inventory(this_branch, root, new_entries):
 
320
    old_entries = this_branch.read_working_inventory()
 
321
    new_inventory = {}
 
322
    by_path = {}
 
323
    for file_id in old_entries:
 
324
        entry = old_entries[file_id]
 
325
        path = old_entries.id2path(file_id)
 
326
        new_inventory[file_id] = (path, file_id, entry.parent_id, entry.kind)
 
327
        by_path[path] = file_id
 
328
    
 
329
    deletions = 0
 
330
    insertions = 0
 
331
    new_path_list = []
 
332
    for path, file_id in new_entries:
 
333
        if path is None:
 
334
            del new_inventory[file_id]
 
335
            deletions += 1
 
336
        else:
 
337
            new_path_list.append((path, file_id))
 
338
            if file_id not in old_entries:
 
339
                insertions += 1
 
340
    # Ensure no file is added before its parent
 
341
    new_path_list.sort()
 
342
    for path, file_id in new_path_list:
 
343
        if path == '':
 
344
            parent = None
 
345
        else:
 
346
            parent = by_path[os.path.dirname(path)]
 
347
        kind = bzrlib.osutils.file_kind(os.path.join(root, path))
 
348
        new_inventory[file_id] = (path, file_id, parent, kind)
 
349
        by_path[path] = file_id 
 
350
 
 
351
    # Get a list in insertion order
 
352
    new_inventory_list = new_inventory.values()
 
353
    mutter ("""Inventory regeneration:
 
354
old length: %i insertions: %i deletions: %i new_length: %i"""\
 
355
        % (len(old_entries), insertions, deletions, len(new_inventory_list)))
 
356
    assert len(new_inventory_list) == len(old_entries) + insertions - deletions
 
357
    new_inventory_list.sort()
 
358
    return new_inventory_list
 
359
 
 
360
merge_types = {     "merge3": (ApplyMerge3, "Native diff3-style merge"), 
 
361
                     "diff3": (Diff3Merge,  "Merge using external diff3")
 
362
              }
 
363