~bzr-pqm/bzr/bzr.dev

1047 by Martin Pool
- add some comments on merge from talking to aaron
1
from bzrlib.merge_core import merge_flex, ApplyMerge3, BackupBeforeChange
1045 by Martin Pool
Doc
2
from bzrlib.changeset import generate_changeset, ExceptionConflictHandler
3
from bzrlib.changeset import Inventory, Diff3Merge
622 by Martin Pool
Updated merge patch from Aaron
4
from bzrlib import find_branch
493 by Martin Pool
- Merge aaron's merge command
5
import bzrlib.osutils
622 by Martin Pool
Updated merge patch from Aaron
6
from bzrlib.errors import BzrCommandError
1005 by Martin Pool
- split TreeDelta and compare_trees out into new module bzrlib.delta
7
from bzrlib.delta import compare_trees
622 by Martin Pool
Updated merge patch from Aaron
8
from trace import mutter, warning
493 by Martin Pool
- Merge aaron's merge command
9
import os.path
10
import tempfile
11
import shutil
12
import errno
13
1047 by Martin Pool
- add some comments on merge from talking to aaron
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
622 by Martin Pool
Updated merge patch from Aaron
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
493 by Martin Pool
- Merge aaron's merge command
28
class MergeConflictHandler(ExceptionConflictHandler):
1045 by Martin Pool
Doc
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
    """
622 by Martin Pool
Updated merge patch from Aaron
35
    def __init__(self, dir, ignore_zero=False):
36
        ExceptionConflictHandler.__init__(self, dir)
37
        self.conflicts = 0
38
        self.ignore_zero = ignore_zero
39
493 by Martin Pool
- Merge aaron's merge command
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)
622 by Martin Pool
Updated merge patch from Aaron
64
            return new_name
493 by Martin Pool
- Merge aaron's merge command
65
        except OSError, e:
66
            if e.errno != errno.EEXIST and e.errno != errno.ENOTEMPTY:
67
                raise
622 by Martin Pool
Updated merge patch from Aaron
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
        
493 by Martin Pool
- Merge aaron's merge command
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)
622 by Martin Pool
Updated merge patch from Aaron
88
        self.conflict("Diff3 conflict encountered in %s" % this_path)
493 by Martin Pool
- Merge aaron's merge command
89
1049 by Martin Pool
- add better handler for new_contents_conflict collision
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
493 by Martin Pool
- Merge aaron's merge command
96
    def target_exists(self, entry, target, old_path):
97
        """Handle the case when the target file or dir exists"""
622 by Martin Pool
Updated merge patch from Aaron
98
        moved_path = self.add_suffix(target, ".moved")
99
        self.conflict("Moved existing %s to %s" % (target, moved_path))
100
850 by Martin Pool
- Merge merge updates from aaron
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
622 by Martin Pool
Updated merge patch from Aaron
107
    def finalize(self):
108
        if not self.ignore_zero:
109
            print "%d conflicts encountered.\n" % self.conflicts
493 by Martin Pool
- Merge aaron's merge command
110
            
558 by Martin Pool
- All top-level classes inherit from object
111
class SourceFile(object):
493 by Martin Pool
- Merge aaron's merge command
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):
622 by Martin Pool
Updated merge patch from Aaron
123
    location, revno = treespec
124
    branch = find_branch(location)
493 by Martin Pool
- Merge aaron's merge command
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)
622 by Martin Pool
Updated merge patch from Aaron
133
    return branch, MergeTree(base_tree, temp_path)
493 by Martin Pool
- Merge aaron's merge command
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
1051 by Martin Pool
- merge aaron's merge improvements up to
166
    def __contains__(self, file_id):
167
        return id in self.tree
168
169
    def get_file_sha1(self, id):
170
        return self.tree.get_file_sha1(id)
171
493 by Martin Pool
- Merge aaron's merge command
172
    def readonly_path(self, id):
850 by Martin Pool
- Merge merge updates from aaron
173
        if id not in self.tree:
174
            return None
493 by Martin Pool
- Merge aaron's merge command
175
        if self.root is not None:
176
            return self.tree.abspath(self.tree.id2path(id))
177
        else:
178
            if self.tree.inventory[id].kind in ("directory", "root_directory"):
179
                return self.tempdir
180
            if not self.cached.has_key(id):
181
                path = os.path.join(self.tempdir, "texts", id)
182
                outfile = file(path, "wb")
183
                outfile.write(self.tree.get_file(id).read())
184
                assert(os.path.exists(path))
185
                self.cached[id] = path
186
            return self.cached[id]
187
628 by Martin Pool
- merge aaron's updated merge/pull code
188
189
190
def merge(other_revision, base_revision,
191
          check_clean=True, ignore_zero=False,
974.1.10 by aaron.bentley at utoronto
Added file-selection to merge-revert
192
          this_dir=None, backup_files=False, merge_type=ApplyMerge3,
193
          file_list=None):
628 by Martin Pool
- merge aaron's updated merge/pull code
194
    """Merge changes into a tree.
195
196
    base_revision
197
        Base for three-way merge.
198
    other_revision
199
        Other revision for three-way merge.
200
    this_dir
201
        Directory to merge changes into; '.' by default.
202
    check_clean
203
        If true, this_dir must have no uncommitted changes before the
204
        merge begins.
205
    """
493 by Martin Pool
- Merge aaron's merge command
206
    tempdir = tempfile.mkdtemp(prefix="bzr-")
207
    try:
628 by Martin Pool
- merge aaron's updated merge/pull code
208
        if this_dir is None:
209
            this_dir = '.'
210
        this_branch = find_branch(this_dir)
211
        if check_clean:
622 by Martin Pool
Updated merge patch from Aaron
212
            changes = compare_trees(this_branch.working_tree(), 
213
                                    this_branch.basis_tree(), False)
214
            if changes.has_changed():
215
                raise BzrCommandError("Working tree has uncommitted changes.")
216
        other_branch, other_tree = get_tree(other_revision, tempdir, "other")
217
        if base_revision == [None, None]:
218
            if other_revision[1] == -1:
219
                o_revno = None
220
            else:
221
                o_revno = other_revision[1]
222
            base_revno = this_branch.common_ancestor(other_branch, 
223
                                                     other_revno=o_revno)[0]
224
            if base_revno is None:
225
                raise UnrelatedBranches()
226
            base_revision = ['.', base_revno]
227
        base_branch, base_tree = get_tree(base_revision, tempdir, "base")
974.1.10 by aaron.bentley at utoronto
Added file-selection to merge-revert
228
        if file_list is None:
229
            interesting_ids = None
230
        else:
231
            interesting_ids = set()
232
            this_tree = this_branch.working_tree()
233
            for fname in file_list:
234
                path = this_branch.relpath(fname)
235
                found_id = False
236
                for tree in (this_tree, base_tree.tree, other_tree.tree):
237
                    file_id = tree.inventory.path2id(path)
238
                    if file_id is not None:
239
                        interesting_ids.add(file_id)
240
                        found_id = True
241
                if not found_id:
242
                    raise BzrCommandError("%s is not a source file in any"
243
                                          " tree." % fname)
622 by Martin Pool
Updated merge patch from Aaron
244
        merge_inner(this_branch, other_tree, base_tree, tempdir, 
974.1.9 by Aaron Bentley
Added merge-type parameter to merge.
245
                    ignore_zero=ignore_zero, backup_files=backup_files, 
974.1.10 by aaron.bentley at utoronto
Added file-selection to merge-revert
246
                    merge_type=merge_type, interesting_ids=interesting_ids)
493 by Martin Pool
- Merge aaron's merge command
247
    finally:
248
        shutil.rmtree(tempdir)
249
250
974.1.10 by aaron.bentley at utoronto
Added file-selection to merge-revert
251
def set_interesting(inventory_a, inventory_b, interesting_ids):
252
    """Mark files whose ids are in interesting_ids as interesting
253
    """
254
    for inventory in (inventory_a, inventory_b):
255
        for path, source_file in inventory.iteritems():
256
             source_file.interesting = source_file.id in interesting_ids
257
258
259
def generate_cset_optimized(tree_a, tree_b, inventory_a, inventory_b,
260
                            interesting_ids=None):
1051 by Martin Pool
- merge aaron's merge improvements up to
261
    """Generate a changeset.  If interesting_ids is supplied, only changes
262
    to those files will be shown.  Metadata changes are stripped.
974.1.10 by aaron.bentley at utoronto
Added file-selection to merge-revert
263
    """ 
1051 by Martin Pool
- merge aaron's merge improvements up to
264
    if interesting_ids is not None:
974.1.10 by aaron.bentley at utoronto
Added file-selection to merge-revert
265
        set_interesting(inventory_a, inventory_b, interesting_ids)
493 by Martin Pool
- Merge aaron's merge command
266
    cset =  generate_changeset(tree_a, tree_b, inventory_a, inventory_b)
267
    for entry in cset.entries.itervalues():
268
        entry.metadata_change = None
269
    return cset
270
271
622 by Martin Pool
Updated merge patch from Aaron
272
def merge_inner(this_branch, other_tree, base_tree, tempdir, 
974.1.10 by aaron.bentley at utoronto
Added file-selection to merge-revert
273
                ignore_zero=False, merge_type=ApplyMerge3, backup_files=False,
274
                interesting_ids=None):
974.1.4 by Aaron Bentley
Implemented merge3 as the default text merge
275
276
    def merge_factory(base_file, other_file):
974.1.9 by Aaron Bentley
Added merge-type parameter to merge.
277
        contents_change = merge_type(base_file, other_file)
974.1.8 by Aaron Bentley
Added default backups for merge-revert
278
        if backup_files:
279
            contents_change = BackupBeforeChange(contents_change)
280
        return contents_change
974.1.10 by aaron.bentley at utoronto
Added file-selection to merge-revert
281
    
282
    def generate_cset(tree_a, tree_b, inventory_a, inventory_b):
283
        return generate_cset_optimized(tree_a, tree_b, inventory_a, inventory_b,
284
                                       interesting_ids)
974.1.4 by Aaron Bentley
Implemented merge3 as the default text merge
285
628 by Martin Pool
- merge aaron's updated merge/pull code
286
    this_tree = get_tree((this_branch.base, None), tempdir, "this")[1]
493 by Martin Pool
- Merge aaron's merge command
287
288
    def get_inventory(tree):
289
        return tree.inventory
290
291
    inv_changes = merge_flex(this_tree, base_tree, other_tree,
974.1.10 by aaron.bentley at utoronto
Added file-selection to merge-revert
292
                             generate_cset, get_inventory,
622 by Martin Pool
Updated merge patch from Aaron
293
                             MergeConflictHandler(base_tree.root,
974.1.3 by Aaron Bentley
Added merge_factory parameter to merge_flex
294
                                                  ignore_zero=ignore_zero),
974.1.4 by Aaron Bentley
Implemented merge3 as the default text merge
295
                             merge_factory=merge_factory)
493 by Martin Pool
- Merge aaron's merge command
296
297
    adjust_ids = []
298
    for id, path in inv_changes.iteritems():
299
        if path is not None:
300
            if path == '.':
301
                path = ''
302
            else:
303
                assert path.startswith('./')
304
            path = path[2:]
305
        adjust_ids.append((path, id))
1051 by Martin Pool
- merge aaron's merge improvements up to
306
    if len(adjust_ids) > 0:
307
        this_branch.set_inventory(regen_inventory(this_branch, this_tree.root,
308
                                                  adjust_ids))
493 by Martin Pool
- Merge aaron's merge command
309
310
311
def regen_inventory(this_branch, root, new_entries):
312
    old_entries = this_branch.read_working_inventory()
313
    new_inventory = {}
314
    by_path = {}
315
    for file_id in old_entries:
316
        entry = old_entries[file_id]
317
        path = old_entries.id2path(file_id)
318
        new_inventory[file_id] = (path, file_id, entry.parent_id, entry.kind)
319
        by_path[path] = file_id
320
    
321
    deletions = 0
322
    insertions = 0
323
    new_path_list = []
324
    for path, file_id in new_entries:
325
        if path is None:
326
            del new_inventory[file_id]
327
            deletions += 1
328
        else:
329
            new_path_list.append((path, file_id))
330
            if file_id not in old_entries:
331
                insertions += 1
332
    # Ensure no file is added before its parent
333
    new_path_list.sort()
334
    for path, file_id in new_path_list:
335
        if path == '':
336
            parent = None
337
        else:
338
            parent = by_path[os.path.dirname(path)]
339
        kind = bzrlib.osutils.file_kind(os.path.join(root, path))
340
        new_inventory[file_id] = (path, file_id, parent, kind)
341
        by_path[path] = file_id 
342
343
    # Get a list in insertion order
344
    new_inventory_list = new_inventory.values()
345
    mutter ("""Inventory regeneration:
346
old length: %i insertions: %i deletions: %i new_length: %i"""\
347
        % (len(old_entries), insertions, deletions, len(new_inventory_list)))
348
    assert len(new_inventory_list) == len(old_entries) + insertions - deletions
349
    new_inventory_list.sort()
350
    return new_inventory_list
974.1.9 by Aaron Bentley
Added merge-type parameter to merge.
351
352
merge_types = {     "merge3": (ApplyMerge3, "Native diff3-style merge"), 
353
                     "diff3": (Diff3Merge,  "Merge using external diff3")
354
              }
355