~bzr-pqm/bzr/bzr.dev

493 by Martin Pool
- Merge aaron's merge command
1
import changeset
2
from changeset import Inventory, apply_changeset, invert_dict
3
import os.path
974.1.8 by Aaron Bentley
Added default backups for merge-revert
4
from osutils import backup_file
974.1.4 by Aaron Bentley
Implemented merge3 as the default text merge
5
from merge3 import Merge3
6
7
class ApplyMerge3:
8
    """Contents-change wrapper around merge3.Merge3"""
1069 by Martin Pool
- merge merge improvements from aaron
9
    def __init__(self, file_id, base, other):
10
        self.file_id = file_id
11
        self.base = base
12
        self.other = other
974.1.4 by Aaron Bentley
Implemented merge3 as the default text merge
13
 
14
    def __eq__(self, other):
15
        if not isinstance(other, ApplyMerge3):
16
            return False
1069 by Martin Pool
- merge merge improvements from aaron
17
        return (self.base == other.base and 
18
                self.other == other.other and self.file_id == other.file_id)
974.1.4 by Aaron Bentley
Implemented merge3 as the default text merge
19
20
    def __ne__(self, other):
21
        return not (self == other)
22
23
24
    def apply(self, filename, conflict_handler, reverse=False):
25
        new_file = filename+".new" 
26
        if not reverse:
1069 by Martin Pool
- merge merge improvements from aaron
27
            base = self.base
28
            other = self.other
974.1.4 by Aaron Bentley
Implemented merge3 as the default text merge
29
        else:
1069 by Martin Pool
- merge merge improvements from aaron
30
            base = self.other
31
            other = self.base
32
        def get_lines(tree):
33
            if self.file_id not in tree:
34
                raise Exception("%s not in tree" % self.file_id)
35
                return ()
36
            return tree.get_file(self.file_id).readlines()
37
        base_lines = get_lines(base)
38
        other_lines = get_lines(other)
39
        m3 = Merge3(base_lines, file(filename, "rb").readlines(), other_lines)
974.1.4 by Aaron Bentley
Implemented merge3 as the default text merge
40
41
        new_conflicts = False
42
        output_file = file(new_file, "wb")
43
        start_marker = "!START OF MERGE CONFLICT!" + "I HOPE THIS IS UNIQUE"
44
        for line in m3.merge_lines(name_a = "TREE", name_b = "MERGE-SOURCE", 
45
                       start_marker=start_marker):
46
            if line.startswith(start_marker):
47
                new_conflicts = True
48
                output_file.write(line.replace(start_marker, '<<<<<<<<'))
49
            else:
50
                output_file.write(line)
51
        output_file.close()
52
        if not new_conflicts:
53
            os.chmod(new_file, os.stat(filename).st_mode)
54
            os.rename(new_file, filename)
55
            return
56
        else:
1069 by Martin Pool
- merge merge improvements from aaron
57
            conflict_handler.merge_conflict(new_file, filename, base_lines,
58
                                            other_lines)
493 by Martin Pool
- Merge aaron's merge command
59
974.1.8 by Aaron Bentley
Added default backups for merge-revert
60
61
class BackupBeforeChange:
62
    """Contents-change wrapper to back up file first"""
63
    def __init__(self, contents_change):
64
        self.contents_change = contents_change
65
 
66
    def __eq__(self, other):
67
        if not isinstance(other, BackupBeforeChange):
68
            return False
69
        return (self.contents_change == other.contents_change)
70
71
    def __ne__(self, other):
72
        return not (self == other)
73
74
    def apply(self, filename, conflict_handler, reverse=False):
75
        backup_file(filename)
76
        self.contents_change.apply(filename, conflict_handler, reverse)
77
78
493 by Martin Pool
- Merge aaron's merge command
79
def invert_invent(inventory):
80
    invert_invent = {}
1069 by Martin Pool
- merge merge improvements from aaron
81
    for file_id in inventory:
82
        path = inventory.id2path(file_id)
83
        if path == '':
84
            path = './.'
85
        else:
86
            path = './' + path
87
        invert_invent[file_id] = path
493 by Martin Pool
- Merge aaron's merge command
88
    return invert_invent
89
90
91
def merge_flex(this, base, other, changeset_function, inventory_function,
1069 by Martin Pool
- merge merge improvements from aaron
92
               conflict_handler, merge_factory, interesting_ids):
93
    cset = changeset_function(base, other, interesting_ids)
94
    new_cset = make_merge_changeset(cset, this, base, other, 
974.1.3 by Aaron Bentley
Added merge_factory parameter to merge_flex
95
                                    conflict_handler, merge_factory)
1069 by Martin Pool
- merge merge improvements from aaron
96
    result = apply_changeset(new_cset, invert_invent(this.tree.inventory),
622 by Martin Pool
Updated merge patch from Aaron
97
                             this.root, conflict_handler, False)
98
    conflict_handler.finalize()
99
    return result
493 by Martin Pool
- Merge aaron's merge command
100
101
    
102
1069 by Martin Pool
- merge merge improvements from aaron
103
def make_merge_changeset(cset, this, base, other, 
974.1.3 by Aaron Bentley
Added merge_factory parameter to merge_flex
104
                         conflict_handler, merge_factory):
493 by Martin Pool
- Merge aaron's merge command
105
    new_cset = changeset.Changeset()
106
    def get_this_contents(id):
1069 by Martin Pool
- merge merge improvements from aaron
107
        path = this.readonly_path(id)
493 by Martin Pool
- Merge aaron's merge command
108
        if os.path.isdir(path):
109
            return changeset.dir_create
110
        else:
111
            return changeset.FileCreate(file(path, "rb").read())
112
113
    for entry in cset.entries.itervalues():
114
        if entry.is_boring():
115
            new_cset.add_entry(entry)
116
        else:
1069 by Martin Pool
- merge merge improvements from aaron
117
            new_entry = make_merged_entry(entry, this, base, other, 
118
                                          conflict_handler)
909.1.4 by Aaron Bentley
Fixed conflict handling for missing merge targets
119
            new_contents = make_merged_contents(entry, this, base, other, 
1069 by Martin Pool
- merge merge improvements from aaron
120
                                                conflict_handler,
974.1.3 by Aaron Bentley
Added merge_factory parameter to merge_flex
121
                                                merge_factory)
850 by Martin Pool
- Merge merge updates from aaron
122
            new_entry.contents_change = new_contents
123
            new_entry.metadata_change = make_merged_metadata(entry, base, other)
124
            new_cset.add_entry(new_entry)
125
493 by Martin Pool
- Merge aaron's merge command
126
    return new_cset
127
1069 by Martin Pool
- merge merge improvements from aaron
128
class ThreeWayConflict(Exception):
129
    def __init__(self, this, base, other):
130
        self.this = this
131
        self.base = base
132
        self.other = other
133
        msg = "Conflict merging %s %s and %s" % (this, base, other)
134
        Exception.__init__(self, msg)
135
136
def threeway_select(this, base, other):
137
    """Returns a value selected by the three-way algorithm.
138
    Raises ThreewayConflict if the algorithm yields a conflict"""
139
    if base == other:
140
        return this
141
    elif base == this:
142
        return other
143
    elif other == this:
144
        return this
145
    else:
146
        raise ThreeWayConflict(this, base, other)
147
148
149
def make_merged_entry(entry, this, base, other, conflict_handler):
909.1.2 by aaron.bentley at utoronto
Fixed rename handling in merge
150
    from bzrlib.trace import mutter
1069 by Martin Pool
- merge merge improvements from aaron
151
    def entry_data(file_id, tree):
152
        assert hasattr(tree, "__contains__"), "%s" % tree
1092.1.18 by Robert Collins
merge from mpool
153
        if not tree.has_or_had_id(file_id):
1069 by Martin Pool
- merge merge improvements from aaron
154
            return (None, None, "")
155
        entry = tree.tree.inventory[file_id]
156
        my_dir = tree.id2path(entry.parent_id)
157
        if my_dir is None:
158
            my_dir = ""
159
        return entry.name, entry.parent_id, my_dir 
160
    this_name, this_parent, this_dir = entry_data(entry.id, this)
161
    base_name, base_parent, base_dir = entry_data(entry.id, base)
162
    other_name, other_parent, other_dir = entry_data(entry.id, other)
909.1.2 by aaron.bentley at utoronto
Fixed rename handling in merge
163
    mutter("Dirs: this, base, other %r %r %r" % (this_dir, base_dir, other_dir))
164
    mutter("Names: this, base, other %r %r %r" % (this_name, base_name, other_name))
1069 by Martin Pool
- merge merge improvements from aaron
165
    old_name = this_name
166
    try:
167
        new_name = threeway_select(this_name, base_name, other_name)
168
    except ThreeWayConflict:
169
        new_name = conflict_handler.rename_conflict(entry.id, this_name, 
170
                                                    base_name, other_name)
171
172
    old_parent = this_parent
173
    try:
174
        new_parent = threeway_select(this_parent, base_parent, other_parent)
175
    except ThreeWayConflict:
176
        new_parent = conflict_handler.move_conflict(entry.id, this_dir,
177
                                                    base_dir, other_dir)
178
    def get_path(name, parent):
1092.1.18 by Robert Collins
merge from mpool
179
        if name is not None:
180
            if name == "":
181
                assert parent is None
182
                return './.'
1069 by Martin Pool
- merge merge improvements from aaron
183
            parent_dir = {this_parent: this_dir, other_parent: other_dir, 
184
                          base_parent: base_dir}
185
            directory = parent_dir[parent]
186
            return os.path.join(directory, name)
187
        else:
1092.1.18 by Robert Collins
merge from mpool
188
            assert parent is None
1069 by Martin Pool
- merge merge improvements from aaron
189
            return None
190
191
    old_path = get_path(old_name, old_parent)
192
        
909.1.2 by aaron.bentley at utoronto
Fixed rename handling in merge
193
    new_entry = changeset.ChangesetEntry(entry.id, old_parent, old_path)
1069 by Martin Pool
- merge merge improvements from aaron
194
    new_entry.new_path = get_path(new_name, new_parent)
493 by Martin Pool
- Merge aaron's merge command
195
    new_entry.new_parent = new_parent
909.1.2 by aaron.bentley at utoronto
Fixed rename handling in merge
196
    mutter(repr(new_entry))
850 by Martin Pool
- Merge merge updates from aaron
197
    return new_entry
198
199
1092.1.18 by Robert Collins
merge from mpool
200
def get_contents(entry, tree):
201
    """Get a contents change element suitable for use with ReplaceContents
202
    """
203
    tree_entry = tree.tree.inventory[entry.id]
204
    if tree_entry.kind == "file":
205
        return changeset.FileCreate(tree.get_file(entry.id).read())
206
    else:
207
        assert tree_entry.kind in ("root_directory", "directory")
208
        return changeset.dir_create
209
210
1069 by Martin Pool
- merge merge improvements from aaron
211
def make_merged_contents(entry, this, base, other, conflict_handler,
974.1.3 by Aaron Bentley
Added merge_factory parameter to merge_flex
212
                         merge_factory):
850 by Martin Pool
- Merge merge updates from aaron
213
    contents = entry.contents_change
214
    if contents is None:
215
        return None
216
    this_path = this.readonly_path(entry.id)
974.1.3 by Aaron Bentley
Added merge_factory parameter to merge_flex
217
    def make_merge():
850 by Martin Pool
- Merge merge updates from aaron
218
        if this_path is None:
1069 by Martin Pool
- merge merge improvements from aaron
219
            return conflict_handler.missing_for_merge(entry.id, 
220
                                                      other.id2path(entry.id))
221
        return merge_factory(entry.id, base, other)
850 by Martin Pool
- Merge merge updates from aaron
222
223
    if isinstance(contents, changeset.ReplaceContents):
224
        if contents.old_contents is None and contents.new_contents is None:
225
            return None
226
        if contents.new_contents is None:
227
            if this_path is not None and os.path.exists(this_path):
228
                return contents
229
            else:
230
                return None
231
        elif contents.old_contents is None:
232
            if this_path is None or not os.path.exists(this_path):
233
                return contents
234
            else:
1092.1.18 by Robert Collins
merge from mpool
235
                this_contents = get_contents(entry, this)
850 by Martin Pool
- Merge merge updates from aaron
236
                if this_contents == contents.new_contents:
237
                    return None
238
                else:
239
                    other_path = other.readonly_path(entry.id)    
240
                    conflict_handler.new_contents_conflict(this_path, 
241
                                                           other_path)
242
        elif isinstance(contents.old_contents, changeset.FileCreate) and \
243
            isinstance(contents.new_contents, changeset.FileCreate):
974.1.3 by Aaron Bentley
Added merge_factory parameter to merge_flex
244
            return make_merge()
850 by Martin Pool
- Merge merge updates from aaron
245
        else:
246
            raise Exception("Unhandled merge scenario")
247
248
def make_merged_metadata(entry, base, other):
249
    if entry.metadata_change is not None:
250
        base_path = base.readonly_path(entry.id)
251
        other_path = other.readonly_path(entry.id)    
252
        return PermissionsMerge(base_path, other_path)
493 by Martin Pool
- Merge aaron's merge command
253
    
254
558 by Martin Pool
- All top-level classes inherit from object
255
class PermissionsMerge(object):
493 by Martin Pool
- Merge aaron's merge command
256
    def __init__(self, base_path, other_path):
257
        self.base_path = base_path
258
        self.other_path = other_path
259
260
    def apply(self, filename, conflict_handler, reverse=False):
261
        if not reverse:
262
            base = self.base_path
263
            other = self.other_path
264
        else:
265
            base = self.other_path
266
            other = self.base_path
267
        base_stat = os.stat(base).st_mode
268
        other_stat = os.stat(other).st_mode
269
        this_stat = os.stat(filename).st_mode
270
        if base_stat &0777 == other_stat &0777:
271
            return
272
        elif this_stat &0777 == other_stat &0777:
273
            return
274
        elif this_stat &0777 == base_stat &0777:
275
            os.chmod(filename, other_stat)
276
        else:
277
            conflict_handler.permission_conflict(filename, base, other)
278
279
280
import unittest
281
import tempfile
282
import shutil
1069 by Martin Pool
- merge merge improvements from aaron
283
from bzrlib.inventory import InventoryEntry, RootEntry
284
from osutils import file_kind
285
class FalseTree(object):
286
    def __init__(self, realtree):
287
        self._realtree = realtree
288
        self.inventory = self
289
290
    def __getitem__(self, file_id):
291
        entry = self.make_inventory_entry(file_id)
292
        if entry is None:
293
            raise KeyError(file_id)
294
        return entry
295
        
296
    def make_inventory_entry(self, file_id):
297
        path = self._realtree.inventory.get(file_id)
298
        if path is None:
299
            return None
300
        if path == "":
301
            return RootEntry(file_id)
302
        dir, name = os.path.split(path)
303
        kind = file_kind(self._realtree.abs_path(path))
304
        for parent_id, path in self._realtree.inventory.iteritems():
305
            if path == dir:
306
                break
307
        if path != dir:
308
            raise Exception("Can't find parent for %s" % name)
309
        return InventoryEntry(file_id, name, kind, parent_id)
310
311
558 by Martin Pool
- All top-level classes inherit from object
312
class MergeTree(object):
493 by Martin Pool
- Merge aaron's merge command
313
    def __init__(self, dir):
314
        self.dir = dir;
315
        os.mkdir(dir)
316
        self.inventory = {'0': ""}
1069 by Martin Pool
- merge merge improvements from aaron
317
        self.tree = FalseTree(self)
493 by Martin Pool
- Merge aaron's merge command
318
    
319
    def child_path(self, parent, name):
320
        return os.path.join(self.inventory[parent], name)
321
322
    def add_file(self, id, parent, name, contents, mode):
323
        path = self.child_path(parent, name)
324
        full_path = self.abs_path(path)
325
        assert not os.path.exists(full_path)
326
        file(full_path, "wb").write(contents)
327
        os.chmod(self.abs_path(path), mode)
328
        self.inventory[id] = path
329
909.1.4 by Aaron Bentley
Fixed conflict handling for missing merge targets
330
    def remove_file(self, id):
331
        os.unlink(self.full_path(id))
332
        del self.inventory[id]
333
493 by Martin Pool
- Merge aaron's merge command
334
    def add_dir(self, id, parent, name, mode):
335
        path = self.child_path(parent, name)
336
        full_path = self.abs_path(path)
337
        assert not os.path.exists(full_path)
338
        os.mkdir(self.abs_path(path))
339
        os.chmod(self.abs_path(path), mode)
340
        self.inventory[id] = path
341
342
    def abs_path(self, path):
343
        return os.path.join(self.dir, path)
344
345
    def full_path(self, id):
909.1.4 by Aaron Bentley
Fixed conflict handling for missing merge targets
346
        try:
347
            tree_path = self.inventory[id]
348
        except KeyError:
349
            return None
350
        return self.abs_path(tree_path)
493 by Martin Pool
- Merge aaron's merge command
351
850 by Martin Pool
- Merge merge updates from aaron
352
    def readonly_path(self, id):
353
        return self.full_path(id)
354
1069 by Martin Pool
- merge merge improvements from aaron
355
    def __contains__(self, file_id):
356
        return file_id in self.inventory
357
1092.1.18 by Robert Collins
merge from mpool
358
    def has_or_had_id(self, file_id):
359
        return file_id in self
360
1069 by Martin Pool
- merge merge improvements from aaron
361
    def get_file(self, file_id):
362
        path = self.readonly_path(file_id)
363
        return file(path, "rb")
364
365
    def id2path(self, file_id):
366
        return self.inventory[file_id]
367
493 by Martin Pool
- Merge aaron's merge command
368
    def change_path(self, id, path):
369
        new = os.path.join(self.dir, self.inventory[id])
370
        os.rename(self.abs_path(self.inventory[id]), self.abs_path(path))
371
        self.inventory[id] = path
372
558 by Martin Pool
- All top-level classes inherit from object
373
class MergeBuilder(object):
493 by Martin Pool
- Merge aaron's merge command
374
    def __init__(self):
375
        self.dir = tempfile.mkdtemp(prefix="BaZing")
376
        self.base = MergeTree(os.path.join(self.dir, "base"))
377
        self.this = MergeTree(os.path.join(self.dir, "this"))
378
        self.other = MergeTree(os.path.join(self.dir, "other"))
379
        
380
        self.cset = changeset.Changeset()
381
        self.cset.add_entry(changeset.ChangesetEntry("0", 
382
                                                     changeset.NULL_ID, "./."))
383
    def get_cset_path(self, parent, name):
384
        if name is None:
385
            assert (parent is None)
386
            return None
387
        return os.path.join(self.cset.entries[parent].path, name)
388
389
    def add_file(self, id, parent, name, contents, mode):
390
        self.base.add_file(id, parent, name, contents, mode)
391
        self.this.add_file(id, parent, name, contents, mode)
392
        self.other.add_file(id, parent, name, contents, mode)
393
        path = self.get_cset_path(parent, name)
394
        self.cset.add_entry(changeset.ChangesetEntry(id, parent, path))
395
909.1.4 by Aaron Bentley
Fixed conflict handling for missing merge targets
396
    def remove_file(self, id, base=False, this=False, other=False):
397
        for option, tree in ((base, self.base), (this, self.this), 
398
                             (other, self.other)):
399
            if option:
400
                tree.remove_file(id)
401
            if other or base:
402
                change = self.cset.entries[id].contents_change
1069 by Martin Pool
- merge merge improvements from aaron
403
                if change is None:
404
                    change = changeset.ReplaceContents(None, None)
405
                    self.cset.entries[id].contents_change = change
406
                    def create_file(tree):
407
                        return changeset.FileCreate(tree.get_file(id).read())
408
                    if not other:
409
                        change.new_contents = create_file(self.other)
410
                    if not base:
411
                        change.old_contents = create_file(self.base)
412
                else:
413
                    assert isinstance(change, changeset.ReplaceContents)
909.1.4 by Aaron Bentley
Fixed conflict handling for missing merge targets
414
                if other:
415
                    change.new_contents=None
416
                if base:
417
                    change.old_contents=None
418
                if change.old_contents is None and change.new_contents is None:
419
                    change = None
420
421
493 by Martin Pool
- Merge aaron's merge command
422
    def add_dir(self, id, parent, name, mode):
423
        path = self.get_cset_path(parent, name)
424
        self.base.add_dir(id, parent, name, mode)
425
        self.cset.add_entry(changeset.ChangesetEntry(id, parent, path))
426
        self.this.add_dir(id, parent, name, mode)
427
        self.other.add_dir(id, parent, name, mode)
428
429
430
    def change_name(self, id, base=None, this=None, other=None):
431
        if base is not None:
432
            self.change_name_tree(id, self.base, base)
433
            self.cset.entries[id].name = base
434
435
        if this is not None:
436
            self.change_name_tree(id, self.this, this)
437
438
        if other is not None:
439
            self.change_name_tree(id, self.other, other)
440
            self.cset.entries[id].new_name = other
441
442
    def change_parent(self, id, base=None, this=None, other=None):
443
        if base is not None:
444
            self.change_parent_tree(id, self.base, base)
445
            self.cset.entries[id].parent = base
446
            self.cset.entries[id].dir = self.cset.entries[base].path
447
448
        if this is not None:
449
            self.change_parent_tree(id, self.this, this)
450
451
        if other is not None:
452
            self.change_parent_tree(id, self.other, other)
453
            self.cset.entries[id].new_parent = other
454
            self.cset.entries[id].new_dir = \
455
                self.cset.entries[other].new_path
456
457
    def change_contents(self, id, base=None, this=None, other=None):
458
        if base is not None:
459
            self.change_contents_tree(id, self.base, base)
460
461
        if this is not None:
462
            self.change_contents_tree(id, self.this, this)
463
464
        if other is not None:
465
            self.change_contents_tree(id, self.other, other)
466
467
        if base is not None or other is not None:
468
            old_contents = file(self.base.full_path(id)).read()
469
            new_contents = file(self.other.full_path(id)).read()
470
            contents = changeset.ReplaceFileContents(old_contents, 
471
                                                     new_contents)
472
            self.cset.entries[id].contents_change = contents
473
474
    def change_perms(self, id, base=None, this=None, other=None):
475
        if base is not None:
476
            self.change_perms_tree(id, self.base, base)
477
478
        if this is not None:
479
            self.change_perms_tree(id, self.this, this)
480
481
        if other is not None:
482
            self.change_perms_tree(id, self.other, other)
483
484
        if base is not None or other is not None:
485
            old_perms = os.stat(self.base.full_path(id)).st_mode &077
486
            new_perms = os.stat(self.other.full_path(id)).st_mode &077
487
            contents = changeset.ChangeUnixPermissions(old_perms, 
488
                                                       new_perms)
489
            self.cset.entries[id].metadata_change = contents
490
491
    def change_name_tree(self, id, tree, name):
492
        new_path = tree.child_path(self.cset.entries[id].parent, name)
493
        tree.change_path(id, new_path)
494
495
    def change_parent_tree(self, id, tree, parent):
496
        new_path = tree.child_path(parent, self.cset.entries[id].name)
497
        tree.change_path(id, new_path)
498
499
    def change_contents_tree(self, id, tree, contents):
500
        path = tree.full_path(id)
501
        mode = os.stat(path).st_mode
502
        file(path, "w").write(contents)
503
        os.chmod(path, mode)
504
505
    def change_perms_tree(self, id, tree, mode):
506
        os.chmod(tree.full_path(id), mode)
507
974.1.7 by Aaron Bentley
Fixed handling of merge factory in test suite
508
    def merge_changeset(self, merge_factory):
493 by Martin Pool
- Merge aaron's merge command
509
        conflict_handler = changeset.ExceptionConflictHandler(self.this.dir)
1069 by Martin Pool
- merge merge improvements from aaron
510
        return make_merge_changeset(self.cset, self.this, self.base,
511
                                    self.other, conflict_handler,
974.1.7 by Aaron Bentley
Fixed handling of merge factory in test suite
512
                                    merge_factory)
850 by Martin Pool
- Merge merge updates from aaron
513
514
    def apply_inv_change(self, inventory_change, orig_inventory):
515
        orig_inventory_by_path = {}
516
        for file_id, path in orig_inventory.iteritems():
517
            orig_inventory_by_path[path] = file_id
518
519
        def parent_id(file_id):
520
            try:
521
                parent_dir = os.path.dirname(orig_inventory[file_id])
522
            except:
523
                print file_id
524
                raise
525
            if parent_dir == "":
526
                return None
527
            return orig_inventory_by_path[parent_dir]
528
        
529
        def new_path(file_id):
530
            if inventory_change.has_key(file_id):
531
                return inventory_change[file_id]
532
            else:
533
                parent = parent_id(file_id)
534
                if parent is None:
535
                    return orig_inventory[file_id]
536
                dirname = new_path(parent)
537
                return os.path.join(dirname, orig_inventory[file_id])
538
539
        new_inventory = {}
540
        for file_id in orig_inventory.iterkeys():
541
            path = new_path(file_id)
542
            if path is None:
543
                continue
544
            new_inventory[file_id] = path
545
546
        for file_id, path in inventory_change.iteritems():
547
            if orig_inventory.has_key(file_id):
548
                continue
549
            new_inventory[file_id] = path
550
        return new_inventory
551
552
        
553
493 by Martin Pool
- Merge aaron's merge command
554
    def apply_changeset(self, cset, conflict_handler=None, reverse=False):
850 by Martin Pool
- Merge merge updates from aaron
555
        inventory_change = changeset.apply_changeset(cset,
556
                                                     self.this.inventory,
557
                                                     self.this.dir,
558
                                                     conflict_handler, reverse)
559
        self.this.inventory =  self.apply_inv_change(inventory_change, 
560
                                                     self.this.inventory)
561
562
                    
563
        
564
493 by Martin Pool
- Merge aaron's merge command
565
        
566
    def cleanup(self):
567
        shutil.rmtree(self.dir)
568
569
570
class MergeTest(unittest.TestCase):
571
    def test_change_name(self):
572
        """Test renames"""
573
        builder = MergeBuilder()
574
        builder.add_file("1", "0", "name1", "hello1", 0755)
575
        builder.change_name("1", other="name2")
576
        builder.add_file("2", "0", "name3", "hello2", 0755)
577
        builder.change_name("2", base="name4")
578
        builder.add_file("3", "0", "name5", "hello3", 0755)
579
        builder.change_name("3", this="name6")
974.1.7 by Aaron Bentley
Fixed handling of merge factory in test suite
580
        cset = builder.merge_changeset(ApplyMerge3)
493 by Martin Pool
- Merge aaron's merge command
581
        assert(cset.entries["2"].is_boring())
582
        assert(cset.entries["1"].name == "name1")
583
        assert(cset.entries["1"].new_name == "name2")
584
        assert(cset.entries["3"].is_boring())
585
        for tree in (builder.this, builder.other, builder.base):
586
            assert(tree.dir != builder.dir and 
587
                   tree.dir.startswith(builder.dir))
588
            for path in tree.inventory.itervalues():
589
                fullpath = tree.abs_path(path)
590
                assert(fullpath.startswith(tree.dir))
591
                assert(not path.startswith(tree.dir))
592
                assert os.path.exists(fullpath)
593
        builder.apply_changeset(cset)
594
        builder.cleanup()
595
        builder = MergeBuilder()
596
        builder.add_file("1", "0", "name1", "hello1", 0644)
597
        builder.change_name("1", other="name2", this="name3")
598
        self.assertRaises(changeset.RenameConflict, 
974.1.7 by Aaron Bentley
Fixed handling of merge factory in test suite
599
                          builder.merge_changeset, ApplyMerge3)
493 by Martin Pool
- Merge aaron's merge command
600
        builder.cleanup()
601
        
602
    def test_file_moves(self):
603
        """Test moves"""
604
        builder = MergeBuilder()
605
        builder.add_dir("1", "0", "dir1", 0755)
606
        builder.add_dir("2", "0", "dir2", 0755)
607
        builder.add_file("3", "1", "file1", "hello1", 0644)
608
        builder.add_file("4", "1", "file2", "hello2", 0644)
609
        builder.add_file("5", "1", "file3", "hello3", 0644)
610
        builder.change_parent("3", other="2")
611
        assert(Inventory(builder.other.inventory).get_parent("3") == "2")
612
        builder.change_parent("4", this="2")
613
        assert(Inventory(builder.this.inventory).get_parent("4") == "2")
614
        builder.change_parent("5", base="2")
615
        assert(Inventory(builder.base.inventory).get_parent("5") == "2")
974.1.7 by Aaron Bentley
Fixed handling of merge factory in test suite
616
        cset = builder.merge_changeset(ApplyMerge3)
493 by Martin Pool
- Merge aaron's merge command
617
        for id in ("1", "2", "4", "5"):
618
            assert(cset.entries[id].is_boring())
619
        assert(cset.entries["3"].parent == "1")
620
        assert(cset.entries["3"].new_parent == "2")
621
        builder.apply_changeset(cset)
622
        builder.cleanup()
623
624
        builder = MergeBuilder()
625
        builder.add_dir("1", "0", "dir1", 0755)
626
        builder.add_dir("2", "0", "dir2", 0755)
627
        builder.add_dir("3", "0", "dir3", 0755)
628
        builder.add_file("4", "1", "file1", "hello1", 0644)
629
        builder.change_parent("4", other="2", this="3")
630
        self.assertRaises(changeset.MoveConflict, 
974.1.7 by Aaron Bentley
Fixed handling of merge factory in test suite
631
                          builder.merge_changeset, ApplyMerge3)
493 by Martin Pool
- Merge aaron's merge command
632
        builder.cleanup()
633
634
    def test_contents_merge(self):
974.1.4 by Aaron Bentley
Implemented merge3 as the default text merge
635
        """Test merge3 merging"""
636
        self.do_contents_test(ApplyMerge3)
637
638
    def test_contents_merge2(self):
493 by Martin Pool
- Merge aaron's merge command
639
        """Test diff3 merging"""
974.1.4 by Aaron Bentley
Implemented merge3 as the default text merge
640
        self.do_contents_test(changeset.Diff3Merge)
641
974.1.8 by Aaron Bentley
Added default backups for merge-revert
642
    def test_contents_merge3(self):
643
        """Test diff3 merging"""
1069 by Martin Pool
- merge merge improvements from aaron
644
        def backup_merge(file_id, base, other):
645
            return BackupBeforeChange(ApplyMerge3(file_id, base, other))
974.1.8 by Aaron Bentley
Added default backups for merge-revert
646
        builder = self.contents_test_success(backup_merge)
647
        def backup_exists(file_id):
648
            return os.path.exists(builder.this.full_path(file_id)+"~")
649
        assert backup_exists("1")
650
        assert backup_exists("2")
651
        assert not backup_exists("3")
652
        builder.cleanup()
653
974.1.4 by Aaron Bentley
Implemented merge3 as the default text merge
654
    def do_contents_test(self, merge_factory):
655
        """Test merging with specified ContentsChange factory"""
974.1.8 by Aaron Bentley
Added default backups for merge-revert
656
        builder = self.contents_test_success(merge_factory)
657
        builder.cleanup()
658
        self.contents_test_conflicts(merge_factory)
659
660
    def contents_test_success(self, merge_factory):
974.1.7 by Aaron Bentley
Fixed handling of merge factory in test suite
661
        from inspect import isclass
493 by Martin Pool
- Merge aaron's merge command
662
        builder = MergeBuilder()
663
        builder.add_file("1", "0", "name1", "text1", 0755)
664
        builder.change_contents("1", other="text4")
665
        builder.add_file("2", "0", "name3", "text2", 0655)
666
        builder.change_contents("2", base="text5")
667
        builder.add_file("3", "0", "name5", "text3", 0744)
1069 by Martin Pool
- merge merge improvements from aaron
668
        builder.add_file("4", "0", "name6", "text4", 0744)
669
        builder.remove_file("4", base=True)
670
        assert not builder.cset.entries["4"].is_boring()
493 by Martin Pool
- Merge aaron's merge command
671
        builder.change_contents("3", this="text6")
974.1.7 by Aaron Bentley
Fixed handling of merge factory in test suite
672
        cset = builder.merge_changeset(merge_factory)
493 by Martin Pool
- Merge aaron's merge command
673
        assert(cset.entries["1"].contents_change is not None)
974.1.7 by Aaron Bentley
Fixed handling of merge factory in test suite
674
        if isclass(merge_factory):
675
            assert(isinstance(cset.entries["1"].contents_change,
676
                          merge_factory))
677
            assert(isinstance(cset.entries["2"].contents_change,
678
                          merge_factory))
493 by Martin Pool
- Merge aaron's merge command
679
        assert(cset.entries["3"].is_boring())
1069 by Martin Pool
- merge merge improvements from aaron
680
        assert(cset.entries["4"].is_boring())
493 by Martin Pool
- Merge aaron's merge command
681
        builder.apply_changeset(cset)
682
        assert(file(builder.this.full_path("1"), "rb").read() == "text4" )
683
        assert(file(builder.this.full_path("2"), "rb").read() == "text2" )
684
        assert(os.stat(builder.this.full_path("1")).st_mode &0777 == 0755)
685
        assert(os.stat(builder.this.full_path("2")).st_mode &0777 == 0655)
686
        assert(os.stat(builder.this.full_path("3")).st_mode &0777 == 0744)
974.1.8 by Aaron Bentley
Added default backups for merge-revert
687
        return builder
493 by Martin Pool
- Merge aaron's merge command
688
974.1.8 by Aaron Bentley
Added default backups for merge-revert
689
    def contents_test_conflicts(self, merge_factory):
493 by Martin Pool
- Merge aaron's merge command
690
        builder = MergeBuilder()
691
        builder.add_file("1", "0", "name1", "text1", 0755)
692
        builder.change_contents("1", other="text4", this="text3")
974.1.7 by Aaron Bentley
Fixed handling of merge factory in test suite
693
        cset = builder.merge_changeset(merge_factory)
493 by Martin Pool
- Merge aaron's merge command
694
        self.assertRaises(changeset.MergeConflict, builder.apply_changeset,
695
                          cset)
696
        builder.cleanup()
697
909.1.4 by Aaron Bentley
Fixed conflict handling for missing merge targets
698
        builder = MergeBuilder()
699
        builder.add_file("1", "0", "name1", "text1", 0755)
700
        builder.change_contents("1", other="text4", base="text3")
701
        builder.remove_file("1", base=True)
702
        self.assertRaises(changeset.NewContentsConflict,
974.1.7 by Aaron Bentley
Fixed handling of merge factory in test suite
703
                          builder.merge_changeset, merge_factory)
909.1.4 by Aaron Bentley
Fixed conflict handling for missing merge targets
704
        builder.cleanup()
705
706
        builder = MergeBuilder()
707
        builder.add_file("1", "0", "name1", "text1", 0755)
708
        builder.change_contents("1", other="text4", base="text3")
709
        builder.remove_file("1", this=True)
974.1.7 by Aaron Bentley
Fixed handling of merge factory in test suite
710
        self.assertRaises(changeset.MissingForMerge, builder.merge_changeset, 
711
                          merge_factory)
909.1.4 by Aaron Bentley
Fixed conflict handling for missing merge targets
712
        builder.cleanup()
713
493 by Martin Pool
- Merge aaron's merge command
714
    def test_perms_merge(self):
715
        builder = MergeBuilder()
716
        builder.add_file("1", "0", "name1", "text1", 0755)
717
        builder.change_perms("1", other=0655)
718
        builder.add_file("2", "0", "name2", "text2", 0755)
719
        builder.change_perms("2", base=0655)
720
        builder.add_file("3", "0", "name3", "text3", 0755)
721
        builder.change_perms("3", this=0655)
974.1.7 by Aaron Bentley
Fixed handling of merge factory in test suite
722
        cset = builder.merge_changeset(ApplyMerge3)
493 by Martin Pool
- Merge aaron's merge command
723
        assert(cset.entries["1"].metadata_change is not None)
724
        assert(isinstance(cset.entries["1"].metadata_change,
725
                          PermissionsMerge))
726
        assert(isinstance(cset.entries["2"].metadata_change,
727
                          PermissionsMerge))
728
        assert(cset.entries["3"].is_boring())
729
        builder.apply_changeset(cset)
730
        assert(os.stat(builder.this.full_path("1")).st_mode &0777 == 0655)
731
        assert(os.stat(builder.this.full_path("2")).st_mode &0777 == 0755)
732
        assert(os.stat(builder.this.full_path("3")).st_mode &0777 == 0655)
733
        builder.cleanup();
734
        builder = MergeBuilder()
735
        builder.add_file("1", "0", "name1", "text1", 0755)
736
        builder.change_perms("1", other=0655, base=0555)
974.1.7 by Aaron Bentley
Fixed handling of merge factory in test suite
737
        cset = builder.merge_changeset(ApplyMerge3)
493 by Martin Pool
- Merge aaron's merge command
738
        self.assertRaises(changeset.MergePermissionConflict, 
739
                     builder.apply_changeset, cset)
740
        builder.cleanup()
741
742
def test():        
743
    changeset_suite = unittest.makeSuite(MergeTest, 'test_')
744
    runner = unittest.TextTestRunner()
745
    runner.run(changeset_suite)
746
        
747
if __name__ == "__main__":
748
    test()