/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: aaron.bentley at utoronto
  • Date: 2005-08-26 06:34:07 UTC
  • mto: (1185.3.4)
  • mto: This revision was merged to the branch mainline in revision 1178.
  • Revision ID: aaron.bentley@utoronto.ca-20050826063406-84d09e206c6c5e73
Shortened conflict markers to 7 characters, to please smerge

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