/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 19:23:12 UTC
  • Revision ID: mbp@sourcefrog.net-20050805192312-273968b3145cbcf6
- cleanup re-raise of exception

Show diffs side-by-side

added added

removed removed

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