/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-25 00:55:22 UTC
  • Revision ID: mbp@sourcefrog.net-20050825005522-1288ce330e3fba0b
- more cleanups of error reporting

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, UnrelatedBranches
 
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 MergeConflictHandler(ExceptionConflictHandler):
 
22
    """Handle conflicts encountered while merging.
 
23
 
 
24
    This subclasses ExceptionConflictHandler, so that any types of
 
25
    conflict that are not explicitly handled cause an exception and
 
26
    terminate the merge.
 
27
    """
 
28
    def __init__(self, dir, ignore_zero=False):
 
29
        ExceptionConflictHandler.__init__(self, dir)
 
30
        self.conflicts = 0
 
31
        self.ignore_zero = ignore_zero
 
32
 
 
33
    def copy(self, source, dest):
 
34
        """Copy the text and mode of a file
 
35
        :param source: The path of the file to copy
 
36
        :param dest: The distination file to create
 
37
        """
 
38
        s_file = file(source, "rb")
 
39
        d_file = file(dest, "wb")
 
40
        for line in s_file:
 
41
            d_file.write(line)
 
42
        os.chmod(dest, 0777 & os.stat(source).st_mode)
 
43
 
 
44
    def dump(self, lines, dest):
 
45
        """Copy the text and mode of a file
 
46
        :param source: The path of the file to copy
 
47
        :param dest: The distination file to create
 
48
        """
 
49
        d_file = file(dest, "wb")
 
50
        for line in lines:
 
51
            d_file.write(line)
 
52
 
 
53
    def add_suffix(self, name, suffix, last_new_name=None):
 
54
        """Rename a file to append a suffix.  If the new name exists, the
 
55
        suffix is added repeatedly until a non-existant name is found
 
56
 
 
57
        :param name: The path of the file
 
58
        :param suffix: The suffix to append
 
59
        :param last_new_name: (used for recursive calls) the last name tried
 
60
        """
 
61
        if last_new_name is None:
 
62
            last_new_name = name
 
63
        new_name = last_new_name+suffix
 
64
        try:
 
65
            os.rename(name, new_name)
 
66
            return new_name
 
67
        except OSError, e:
 
68
            if e.errno != errno.EEXIST and e.errno != errno.ENOTEMPTY:
 
69
                raise
 
70
            return self.add_suffix(name, suffix, last_new_name=new_name)
 
71
 
 
72
    def conflict(self, text):
 
73
        warning(text)
 
74
        self.conflicts += 1
 
75
        
 
76
 
 
77
    def merge_conflict(self, new_file, this_path, base_lines, other_lines):
 
78
        """
 
79
        Handle diff3 conflicts by producing a .THIS, .BASE and .OTHER.  The
 
80
        main file will be a version with diff3 conflicts.
 
81
        :param new_file: Path to the output file with diff3 markers
 
82
        :param this_path: Path to the file text for the THIS tree
 
83
        :param base_path: Path to the file text for the BASE tree
 
84
        :param other_path: Path to the file text for the OTHER tree
 
85
        """
 
86
        self.add_suffix(this_path, ".THIS")
 
87
        self.dump(base_lines, this_path+".BASE")
 
88
        self.dump(other_lines, this_path+".OTHER")
 
89
        os.rename(new_file, this_path)
 
90
        self.conflict("Diff3 conflict encountered in %s" % this_path)
 
91
 
 
92
    def new_contents_conflict(self, filename, other_contents):
 
93
        """Conflicting contents for newly added file."""
 
94
        self.copy(other_contents, filename + ".OTHER")
 
95
        self.conflict("Conflict in newly added file %s" % filename)
 
96
    
 
97
 
 
98
    def target_exists(self, entry, target, old_path):
 
99
        """Handle the case when the target file or dir exists"""
 
100
        moved_path = self.add_suffix(target, ".moved")
 
101
        self.conflict("Moved existing %s to %s" % (target, moved_path))
 
102
 
 
103
    def rmdir_non_empty(self, filename):
 
104
        """Handle the case where the dir to be removed still has contents"""
 
105
        self.conflict("Directory %s not removed because it is not empty"\
 
106
            % filename)
 
107
        return "skip"
 
108
 
 
109
    def finalize(self):
 
110
        if not self.ignore_zero:
 
111
            print "%d conflicts encountered.\n" % self.conflicts
 
112
            
 
113
def get_tree(treespec, temp_root, label):
 
114
    location, revno = treespec
 
115
    branch = find_branch(location)
 
116
    if revno is None:
 
117
        base_tree = branch.working_tree()
 
118
    elif revno == -1:
 
119
        base_tree = branch.basis_tree()
 
120
    else:
 
121
        base_tree = branch.revision_tree(branch.lookup_revision(revno))
 
122
    temp_path = os.path.join(temp_root, label)
 
123
    os.mkdir(temp_path)
 
124
    return branch, MergeTree(base_tree, temp_path)
 
125
 
 
126
 
 
127
def file_exists(tree, file_id):
 
128
    return tree.has_filename(tree.id2path(file_id))
 
129
    
 
130
 
 
131
class MergeTree(object):
 
132
    def __init__(self, tree, tempdir):
 
133
        object.__init__(self)
 
134
        if hasattr(tree, "basedir"):
 
135
            self.root = tree.basedir
 
136
        else:
 
137
            self.root = None
 
138
        self.tree = tree
 
139
        self.tempdir = tempdir
 
140
        os.mkdir(os.path.join(self.tempdir, "texts"))
 
141
        self.cached = {}
 
142
 
 
143
    def __iter__(self):
 
144
        return self.tree.__iter__()
 
145
 
 
146
    def __contains__(self, file_id):
 
147
        return file_id in self.tree
 
148
 
 
149
    def get_file(self, file_id):
 
150
        return self.tree.get_file(file_id)
 
151
 
 
152
    def get_file_sha1(self, id):
 
153
        return self.tree.get_file_sha1(id)
 
154
 
 
155
    def id2path(self, file_id):
 
156
        return self.tree.id2path(file_id)
 
157
 
 
158
    def has_id(self, file_id):
 
159
        return self.tree.has_id(file_id)
 
160
 
 
161
    def has_or_had_id(self, file_id):
 
162
        if file_id == self.tree.inventory.root.file_id:
 
163
            return True
 
164
        return self.tree.inventory.has_id(file_id)
 
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 generate_cset_optimized(tree_a, tree_b, interesting_ids=None):
 
254
    """Generate a changeset.  If interesting_ids is supplied, only changes
 
255
    to those files will be shown.  Metadata changes are stripped.
 
256
    """ 
 
257
    cset =  generate_changeset(tree_a, tree_b, interesting_ids)
 
258
    for entry in cset.entries.itervalues():
 
259
        entry.metadata_change = None
 
260
    return cset
 
261
 
 
262
 
 
263
def merge_inner(this_branch, other_tree, base_tree, tempdir, 
 
264
                ignore_zero=False, merge_type=ApplyMerge3, backup_files=False,
 
265
                interesting_ids=None):
 
266
 
 
267
    def merge_factory(file_id, base, other):
 
268
        contents_change = merge_type(file_id, base, other)
 
269
        if backup_files:
 
270
            contents_change = BackupBeforeChange(contents_change)
 
271
        return contents_change
 
272
 
 
273
    this_tree = get_tree((this_branch.base, None), tempdir, "this")[1]
 
274
 
 
275
    def get_inventory(tree):
 
276
        return tree.tree.inventory
 
277
 
 
278
    inv_changes = merge_flex(this_tree, base_tree, other_tree,
 
279
                             generate_cset_optimized, get_inventory,
 
280
                             MergeConflictHandler(base_tree.root,
 
281
                                                  ignore_zero=ignore_zero),
 
282
                             merge_factory=merge_factory, 
 
283
                             interesting_ids=interesting_ids)
 
284
 
 
285
    adjust_ids = []
 
286
    for id, path in inv_changes.iteritems():
 
287
        if path is not None:
 
288
            if path == '.':
 
289
                path = ''
 
290
            else:
 
291
                assert path.startswith('./'), "path is %s" % path
 
292
            path = path[2:]
 
293
        adjust_ids.append((path, id))
 
294
    if len(adjust_ids) > 0:
 
295
        this_branch.set_inventory(regen_inventory(this_branch, this_tree.root,
 
296
                                                  adjust_ids))
 
297
 
 
298
 
 
299
def regen_inventory(this_branch, root, new_entries):
 
300
    old_entries = this_branch.read_working_inventory()
 
301
    new_inventory = {}
 
302
    by_path = {}
 
303
    new_entries_map = {} 
 
304
    for path, file_id in new_entries:
 
305
        if path is None:
 
306
            continue
 
307
        new_entries_map[file_id] = path
 
308
 
 
309
    def id2path(file_id):
 
310
        path = new_entries_map.get(file_id)
 
311
        if path is not None:
 
312
            return path
 
313
        entry = old_entries[file_id]
 
314
        if entry.parent_id is None:
 
315
            return entry.name
 
316
        return os.path.join(id2path(entry.parent_id), entry.name)
 
317
        
 
318
    for file_id in old_entries:
 
319
        entry = old_entries[file_id]
 
320
        path = id2path(file_id)
 
321
        new_inventory[file_id] = (path, file_id, entry.parent_id, entry.kind)
 
322
        by_path[path] = file_id
 
323
    
 
324
    deletions = 0
 
325
    insertions = 0
 
326
    new_path_list = []
 
327
    for path, file_id in new_entries:
 
328
        if path is None:
 
329
            del new_inventory[file_id]
 
330
            deletions += 1
 
331
        else:
 
332
            new_path_list.append((path, file_id))
 
333
            if file_id not in old_entries:
 
334
                insertions += 1
 
335
    # Ensure no file is added before its parent
 
336
    new_path_list.sort()
 
337
    for path, file_id in new_path_list:
 
338
        if path == '':
 
339
            parent = None
 
340
        else:
 
341
            parent = by_path[os.path.dirname(path)]
 
342
        kind = bzrlib.osutils.file_kind(os.path.join(root, path))
 
343
        new_inventory[file_id] = (path, file_id, parent, kind)
 
344
        by_path[path] = file_id 
 
345
 
 
346
    # Get a list in insertion order
 
347
    new_inventory_list = new_inventory.values()
 
348
    mutter ("""Inventory regeneration:
 
349
old length: %i insertions: %i deletions: %i new_length: %i"""\
 
350
        % (len(old_entries), insertions, deletions, len(new_inventory_list)))
 
351
    assert len(new_inventory_list) == len(old_entries) + insertions - deletions
 
352
    new_inventory_list.sort()
 
353
    return new_inventory_list
 
354
 
 
355
merge_types = {     "merge3": (ApplyMerge3, "Native diff3-style merge"), 
 
356
                     "diff3": (Diff3Merge,  "Merge using external diff3")
 
357
              }
 
358