~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"""
974.1.23 by Aaron Bentley
Avoided unnecessary temp files
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
974.1.23 by Aaron Bentley
Avoided unnecessary temp files
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:
974.1.23 by Aaron Bentley
Avoided unnecessary temp files
27
            base = self.base
28
            other = self.other
974.1.4 by Aaron Bentley
Implemented merge3 as the default text merge
29
        else:
974.1.23 by Aaron Bentley
Avoided unnecessary temp files
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:
974.1.23 by Aaron Bentley
Avoided unnecessary temp files
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 = {}
974.1.19 by Aaron Bentley
Removed MergeTree.inventory
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,
974.1.17 by Aaron Bentley
Switched to using a set of interesting file_ids instead of SourceFile attribute
92
               conflict_handler, merge_factory, interesting_ids):
974.1.19 by Aaron Bentley
Removed MergeTree.inventory
93
    cset = changeset_function(base, other, interesting_ids)
974.1.20 by Aaron Bentley
Eliminated ThreeWayInventory
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)
974.1.20 by Aaron Bentley
Eliminated ThreeWayInventory
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
974.1.20 by Aaron Bentley
Eliminated ThreeWayInventory
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):
974.1.20 by Aaron Bentley
Eliminated ThreeWayInventory
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:
974.1.20 by Aaron Bentley
Eliminated ThreeWayInventory
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, 
974.1.20 by Aaron Bentley
Eliminated ThreeWayInventory
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
974.1.22 by Aaron Bentley
Refactored code
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
974.1.20 by Aaron Bentley
Eliminated ThreeWayInventory
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
974.1.20 by Aaron Bentley
Eliminated ThreeWayInventory
151
    def entry_data(file_id, tree):
152
        assert hasattr(tree, "__contains__"), "%s" % tree
153
        if file_id not in tree:
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))
974.1.22 by Aaron Bentley
Refactored code
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):
179
        if name is not None and parent is not None:
180
            parent_dir = {this_parent: this_dir, other_parent: other_dir, 
181
                          base_parent: base_dir}
182
            directory = parent_dir[parent]
183
            return os.path.join(directory, name)
184
        else:
185
            assert name is None and parent is None
186
            return None
187
188
    old_path = get_path(old_name, old_parent)
189
        
909.1.2 by aaron.bentley at utoronto
Fixed rename handling in merge
190
    new_entry = changeset.ChangesetEntry(entry.id, old_parent, old_path)
974.1.22 by Aaron Bentley
Refactored code
191
    new_entry.new_path = get_path(new_name, new_parent)
493 by Martin Pool
- Merge aaron's merge command
192
    new_entry.new_parent = new_parent
909.1.2 by aaron.bentley at utoronto
Fixed rename handling in merge
193
    mutter(repr(new_entry))
850 by Martin Pool
- Merge merge updates from aaron
194
    return new_entry
195
196
974.1.20 by Aaron Bentley
Eliminated ThreeWayInventory
197
def make_merged_contents(entry, this, base, other, conflict_handler,
974.1.3 by Aaron Bentley
Added merge_factory parameter to merge_flex
198
                         merge_factory):
850 by Martin Pool
- Merge merge updates from aaron
199
    contents = entry.contents_change
200
    if contents is None:
201
        return None
202
    this_path = this.readonly_path(entry.id)
974.1.3 by Aaron Bentley
Added merge_factory parameter to merge_flex
203
    def make_merge():
850 by Martin Pool
- Merge merge updates from aaron
204
        if this_path is None:
974.1.20 by Aaron Bentley
Eliminated ThreeWayInventory
205
            return conflict_handler.missing_for_merge(entry.id, 
206
                                                      other.id2path(entry.id))
974.1.23 by Aaron Bentley
Avoided unnecessary temp files
207
        return merge_factory(entry.id, base, other)
850 by Martin Pool
- Merge merge updates from aaron
208
209
    if isinstance(contents, changeset.ReplaceContents):
210
        if contents.old_contents is None and contents.new_contents is None:
211
            return None
212
        if contents.new_contents is None:
213
            if this_path is not None and os.path.exists(this_path):
214
                return contents
215
            else:
216
                return None
217
        elif contents.old_contents is None:
218
            if this_path is None or not os.path.exists(this_path):
219
                return contents
220
            else:
974.1.24 by Aaron Bentley
Fixed spurious new_contents_conflict
221
                this_contents = changeset.FileCreate(file(this_path, 
222
                                                     "rb").read())
850 by Martin Pool
- Merge merge updates from aaron
223
                if this_contents == contents.new_contents:
224
                    return None
225
                else:
226
                    other_path = other.readonly_path(entry.id)    
227
                    conflict_handler.new_contents_conflict(this_path, 
228
                                                           other_path)
229
        elif isinstance(contents.old_contents, changeset.FileCreate) and \
230
            isinstance(contents.new_contents, changeset.FileCreate):
974.1.3 by Aaron Bentley
Added merge_factory parameter to merge_flex
231
            return make_merge()
850 by Martin Pool
- Merge merge updates from aaron
232
        else:
233
            raise Exception("Unhandled merge scenario")
234
235
def make_merged_metadata(entry, base, other):
236
    if entry.metadata_change is not None:
237
        base_path = base.readonly_path(entry.id)
238
        other_path = other.readonly_path(entry.id)    
239
        return PermissionsMerge(base_path, other_path)
493 by Martin Pool
- Merge aaron's merge command
240
    
241
558 by Martin Pool
- All top-level classes inherit from object
242
class PermissionsMerge(object):
493 by Martin Pool
- Merge aaron's merge command
243
    def __init__(self, base_path, other_path):
244
        self.base_path = base_path
245
        self.other_path = other_path
246
247
    def apply(self, filename, conflict_handler, reverse=False):
248
        if not reverse:
249
            base = self.base_path
250
            other = self.other_path
251
        else:
252
            base = self.other_path
253
            other = self.base_path
254
        base_stat = os.stat(base).st_mode
255
        other_stat = os.stat(other).st_mode
256
        this_stat = os.stat(filename).st_mode
257
        if base_stat &0777 == other_stat &0777:
258
            return
259
        elif this_stat &0777 == other_stat &0777:
260
            return
261
        elif this_stat &0777 == base_stat &0777:
262
            os.chmod(filename, other_stat)
263
        else:
264
            conflict_handler.permission_conflict(filename, base, other)
265
266
267
import unittest
268
import tempfile
269
import shutil
974.1.20 by Aaron Bentley
Eliminated ThreeWayInventory
270
from bzrlib.inventory import InventoryEntry, RootEntry
271
from osutils import file_kind
272
class FalseTree(object):
273
    def __init__(self, realtree):
274
        self._realtree = realtree
275
        self.inventory = self
276
277
    def __getitem__(self, file_id):
278
        entry = self.make_inventory_entry(file_id)
279
        if entry is None:
280
            raise KeyError(file_id)
281
        return entry
282
        
283
    def make_inventory_entry(self, file_id):
284
        path = self._realtree.inventory.get(file_id)
285
        if path is None:
286
            return None
287
        if path == "":
288
            return RootEntry(file_id)
289
        dir, name = os.path.split(path)
290
        kind = file_kind(self._realtree.abs_path(path))
291
        for parent_id, path in self._realtree.inventory.iteritems():
292
            if path == dir:
293
                break
294
        if path != dir:
295
            raise Exception("Can't find parent for %s" % name)
296
        return InventoryEntry(file_id, name, kind, parent_id)
297
298
558 by Martin Pool
- All top-level classes inherit from object
299
class MergeTree(object):
493 by Martin Pool
- Merge aaron's merge command
300
    def __init__(self, dir):
301
        self.dir = dir;
302
        os.mkdir(dir)
303
        self.inventory = {'0': ""}
974.1.20 by Aaron Bentley
Eliminated ThreeWayInventory
304
        self.tree = FalseTree(self)
493 by Martin Pool
- Merge aaron's merge command
305
    
306
    def child_path(self, parent, name):
307
        return os.path.join(self.inventory[parent], name)
308
309
    def add_file(self, id, parent, name, contents, mode):
310
        path = self.child_path(parent, name)
311
        full_path = self.abs_path(path)
312
        assert not os.path.exists(full_path)
313
        file(full_path, "wb").write(contents)
314
        os.chmod(self.abs_path(path), mode)
315
        self.inventory[id] = path
316
909.1.4 by Aaron Bentley
Fixed conflict handling for missing merge targets
317
    def remove_file(self, id):
318
        os.unlink(self.full_path(id))
319
        del self.inventory[id]
320
493 by Martin Pool
- Merge aaron's merge command
321
    def add_dir(self, id, parent, name, mode):
322
        path = self.child_path(parent, name)
323
        full_path = self.abs_path(path)
324
        assert not os.path.exists(full_path)
325
        os.mkdir(self.abs_path(path))
326
        os.chmod(self.abs_path(path), mode)
327
        self.inventory[id] = path
328
329
    def abs_path(self, path):
330
        return os.path.join(self.dir, path)
331
332
    def full_path(self, id):
909.1.4 by Aaron Bentley
Fixed conflict handling for missing merge targets
333
        try:
334
            tree_path = self.inventory[id]
335
        except KeyError:
336
            return None
337
        return self.abs_path(tree_path)
493 by Martin Pool
- Merge aaron's merge command
338
850 by Martin Pool
- Merge merge updates from aaron
339
    def readonly_path(self, id):
340
        return self.full_path(id)
341
974.1.20 by Aaron Bentley
Eliminated ThreeWayInventory
342
    def __contains__(self, file_id):
343
        return file_id in self.inventory
344
974.1.23 by Aaron Bentley
Avoided unnecessary temp files
345
    def get_file(self, file_id):
346
        path = self.readonly_path(file_id)
347
        return file(path, "rb")
348
974.1.20 by Aaron Bentley
Eliminated ThreeWayInventory
349
    def id2path(self, file_id):
350
        return self.inventory[file_id]
351
493 by Martin Pool
- Merge aaron's merge command
352
    def change_path(self, id, path):
353
        new = os.path.join(self.dir, self.inventory[id])
354
        os.rename(self.abs_path(self.inventory[id]), self.abs_path(path))
355
        self.inventory[id] = path
356
558 by Martin Pool
- All top-level classes inherit from object
357
class MergeBuilder(object):
493 by Martin Pool
- Merge aaron's merge command
358
    def __init__(self):
359
        self.dir = tempfile.mkdtemp(prefix="BaZing")
360
        self.base = MergeTree(os.path.join(self.dir, "base"))
361
        self.this = MergeTree(os.path.join(self.dir, "this"))
362
        self.other = MergeTree(os.path.join(self.dir, "other"))
363
        
364
        self.cset = changeset.Changeset()
365
        self.cset.add_entry(changeset.ChangesetEntry("0", 
366
                                                     changeset.NULL_ID, "./."))
367
    def get_cset_path(self, parent, name):
368
        if name is None:
369
            assert (parent is None)
370
            return None
371
        return os.path.join(self.cset.entries[parent].path, name)
372
373
    def add_file(self, id, parent, name, contents, mode):
374
        self.base.add_file(id, parent, name, contents, mode)
375
        self.this.add_file(id, parent, name, contents, mode)
376
        self.other.add_file(id, parent, name, contents, mode)
377
        path = self.get_cset_path(parent, name)
378
        self.cset.add_entry(changeset.ChangesetEntry(id, parent, path))
379
909.1.4 by Aaron Bentley
Fixed conflict handling for missing merge targets
380
    def remove_file(self, id, base=False, this=False, other=False):
381
        for option, tree in ((base, self.base), (this, self.this), 
382
                             (other, self.other)):
383
            if option:
384
                tree.remove_file(id)
385
            if other or base:
386
                change = self.cset.entries[id].contents_change
974.1.24 by Aaron Bentley
Fixed spurious new_contents_conflict
387
                if change is None:
388
                    change = changeset.ReplaceContents(None, None)
389
                    self.cset.entries[id].contents_change = change
390
                    def create_file(tree):
391
                        return changeset.FileCreate(tree.get_file(id).read())
392
                    if not other:
393
                        change.new_contents = create_file(self.other)
394
                    if not base:
395
                        change.old_contents = create_file(self.base)
396
                else:
397
                    assert isinstance(change, changeset.ReplaceContents)
909.1.4 by Aaron Bentley
Fixed conflict handling for missing merge targets
398
                if other:
399
                    change.new_contents=None
400
                if base:
401
                    change.old_contents=None
402
                if change.old_contents is None and change.new_contents is None:
403
                    change = None
404
405
493 by Martin Pool
- Merge aaron's merge command
406
    def add_dir(self, id, parent, name, mode):
407
        path = self.get_cset_path(parent, name)
408
        self.base.add_dir(id, parent, name, mode)
409
        self.cset.add_entry(changeset.ChangesetEntry(id, parent, path))
410
        self.this.add_dir(id, parent, name, mode)
411
        self.other.add_dir(id, parent, name, mode)
412
413
414
    def change_name(self, id, base=None, this=None, other=None):
415
        if base is not None:
416
            self.change_name_tree(id, self.base, base)
417
            self.cset.entries[id].name = base
418
419
        if this is not None:
420
            self.change_name_tree(id, self.this, this)
421
422
        if other is not None:
423
            self.change_name_tree(id, self.other, other)
424
            self.cset.entries[id].new_name = other
425
426
    def change_parent(self, id, base=None, this=None, other=None):
427
        if base is not None:
428
            self.change_parent_tree(id, self.base, base)
429
            self.cset.entries[id].parent = base
430
            self.cset.entries[id].dir = self.cset.entries[base].path
431
432
        if this is not None:
433
            self.change_parent_tree(id, self.this, this)
434
435
        if other is not None:
436
            self.change_parent_tree(id, self.other, other)
437
            self.cset.entries[id].new_parent = other
438
            self.cset.entries[id].new_dir = \
439
                self.cset.entries[other].new_path
440
441
    def change_contents(self, id, base=None, this=None, other=None):
442
        if base is not None:
443
            self.change_contents_tree(id, self.base, base)
444
445
        if this is not None:
446
            self.change_contents_tree(id, self.this, this)
447
448
        if other is not None:
449
            self.change_contents_tree(id, self.other, other)
450
451
        if base is not None or other is not None:
452
            old_contents = file(self.base.full_path(id)).read()
453
            new_contents = file(self.other.full_path(id)).read()
454
            contents = changeset.ReplaceFileContents(old_contents, 
455
                                                     new_contents)
456
            self.cset.entries[id].contents_change = contents
457
458
    def change_perms(self, id, base=None, this=None, other=None):
459
        if base is not None:
460
            self.change_perms_tree(id, self.base, base)
461
462
        if this is not None:
463
            self.change_perms_tree(id, self.this, this)
464
465
        if other is not None:
466
            self.change_perms_tree(id, self.other, other)
467
468
        if base is not None or other is not None:
469
            old_perms = os.stat(self.base.full_path(id)).st_mode &077
470
            new_perms = os.stat(self.other.full_path(id)).st_mode &077
471
            contents = changeset.ChangeUnixPermissions(old_perms, 
472
                                                       new_perms)
473
            self.cset.entries[id].metadata_change = contents
474
475
    def change_name_tree(self, id, tree, name):
476
        new_path = tree.child_path(self.cset.entries[id].parent, name)
477
        tree.change_path(id, new_path)
478
479
    def change_parent_tree(self, id, tree, parent):
480
        new_path = tree.child_path(parent, self.cset.entries[id].name)
481
        tree.change_path(id, new_path)
482
483
    def change_contents_tree(self, id, tree, contents):
484
        path = tree.full_path(id)
485
        mode = os.stat(path).st_mode
486
        file(path, "w").write(contents)
487
        os.chmod(path, mode)
488
489
    def change_perms_tree(self, id, tree, mode):
490
        os.chmod(tree.full_path(id), mode)
491
974.1.7 by Aaron Bentley
Fixed handling of merge factory in test suite
492
    def merge_changeset(self, merge_factory):
493 by Martin Pool
- Merge aaron's merge command
493
        conflict_handler = changeset.ExceptionConflictHandler(self.this.dir)
974.1.20 by Aaron Bentley
Eliminated ThreeWayInventory
494
        return make_merge_changeset(self.cset, self.this, self.base,
495
                                    self.other, conflict_handler,
974.1.7 by Aaron Bentley
Fixed handling of merge factory in test suite
496
                                    merge_factory)
850 by Martin Pool
- Merge merge updates from aaron
497
498
    def apply_inv_change(self, inventory_change, orig_inventory):
499
        orig_inventory_by_path = {}
500
        for file_id, path in orig_inventory.iteritems():
501
            orig_inventory_by_path[path] = file_id
502
503
        def parent_id(file_id):
504
            try:
505
                parent_dir = os.path.dirname(orig_inventory[file_id])
506
            except:
507
                print file_id
508
                raise
509
            if parent_dir == "":
510
                return None
511
            return orig_inventory_by_path[parent_dir]
512
        
513
        def new_path(file_id):
514
            if inventory_change.has_key(file_id):
515
                return inventory_change[file_id]
516
            else:
517
                parent = parent_id(file_id)
518
                if parent is None:
519
                    return orig_inventory[file_id]
520
                dirname = new_path(parent)
521
                return os.path.join(dirname, orig_inventory[file_id])
522
523
        new_inventory = {}
524
        for file_id in orig_inventory.iterkeys():
525
            path = new_path(file_id)
526
            if path is None:
527
                continue
528
            new_inventory[file_id] = path
529
530
        for file_id, path in inventory_change.iteritems():
531
            if orig_inventory.has_key(file_id):
532
                continue
533
            new_inventory[file_id] = path
534
        return new_inventory
535
536
        
537
493 by Martin Pool
- Merge aaron's merge command
538
    def apply_changeset(self, cset, conflict_handler=None, reverse=False):
850 by Martin Pool
- Merge merge updates from aaron
539
        inventory_change = changeset.apply_changeset(cset,
540
                                                     self.this.inventory,
541
                                                     self.this.dir,
542
                                                     conflict_handler, reverse)
543
        self.this.inventory =  self.apply_inv_change(inventory_change, 
544
                                                     self.this.inventory)
545
546
                    
547
        
548
493 by Martin Pool
- Merge aaron's merge command
549
        
550
    def cleanup(self):
551
        shutil.rmtree(self.dir)
552
553
554
class MergeTest(unittest.TestCase):
555
    def test_change_name(self):
556
        """Test renames"""
557
        builder = MergeBuilder()
558
        builder.add_file("1", "0", "name1", "hello1", 0755)
559
        builder.change_name("1", other="name2")
560
        builder.add_file("2", "0", "name3", "hello2", 0755)
561
        builder.change_name("2", base="name4")
562
        builder.add_file("3", "0", "name5", "hello3", 0755)
563
        builder.change_name("3", this="name6")
974.1.7 by Aaron Bentley
Fixed handling of merge factory in test suite
564
        cset = builder.merge_changeset(ApplyMerge3)
493 by Martin Pool
- Merge aaron's merge command
565
        assert(cset.entries["2"].is_boring())
566
        assert(cset.entries["1"].name == "name1")
567
        assert(cset.entries["1"].new_name == "name2")
568
        assert(cset.entries["3"].is_boring())
569
        for tree in (builder.this, builder.other, builder.base):
570
            assert(tree.dir != builder.dir and 
571
                   tree.dir.startswith(builder.dir))
572
            for path in tree.inventory.itervalues():
573
                fullpath = tree.abs_path(path)
574
                assert(fullpath.startswith(tree.dir))
575
                assert(not path.startswith(tree.dir))
576
                assert os.path.exists(fullpath)
577
        builder.apply_changeset(cset)
578
        builder.cleanup()
579
        builder = MergeBuilder()
580
        builder.add_file("1", "0", "name1", "hello1", 0644)
581
        builder.change_name("1", other="name2", this="name3")
582
        self.assertRaises(changeset.RenameConflict, 
974.1.7 by Aaron Bentley
Fixed handling of merge factory in test suite
583
                          builder.merge_changeset, ApplyMerge3)
493 by Martin Pool
- Merge aaron's merge command
584
        builder.cleanup()
585
        
586
    def test_file_moves(self):
587
        """Test moves"""
588
        builder = MergeBuilder()
589
        builder.add_dir("1", "0", "dir1", 0755)
590
        builder.add_dir("2", "0", "dir2", 0755)
591
        builder.add_file("3", "1", "file1", "hello1", 0644)
592
        builder.add_file("4", "1", "file2", "hello2", 0644)
593
        builder.add_file("5", "1", "file3", "hello3", 0644)
594
        builder.change_parent("3", other="2")
595
        assert(Inventory(builder.other.inventory).get_parent("3") == "2")
596
        builder.change_parent("4", this="2")
597
        assert(Inventory(builder.this.inventory).get_parent("4") == "2")
598
        builder.change_parent("5", base="2")
599
        assert(Inventory(builder.base.inventory).get_parent("5") == "2")
974.1.7 by Aaron Bentley
Fixed handling of merge factory in test suite
600
        cset = builder.merge_changeset(ApplyMerge3)
493 by Martin Pool
- Merge aaron's merge command
601
        for id in ("1", "2", "4", "5"):
602
            assert(cset.entries[id].is_boring())
603
        assert(cset.entries["3"].parent == "1")
604
        assert(cset.entries["3"].new_parent == "2")
605
        builder.apply_changeset(cset)
606
        builder.cleanup()
607
608
        builder = MergeBuilder()
609
        builder.add_dir("1", "0", "dir1", 0755)
610
        builder.add_dir("2", "0", "dir2", 0755)
611
        builder.add_dir("3", "0", "dir3", 0755)
612
        builder.add_file("4", "1", "file1", "hello1", 0644)
613
        builder.change_parent("4", other="2", this="3")
614
        self.assertRaises(changeset.MoveConflict, 
974.1.7 by Aaron Bentley
Fixed handling of merge factory in test suite
615
                          builder.merge_changeset, ApplyMerge3)
493 by Martin Pool
- Merge aaron's merge command
616
        builder.cleanup()
617
618
    def test_contents_merge(self):
974.1.4 by Aaron Bentley
Implemented merge3 as the default text merge
619
        """Test merge3 merging"""
620
        self.do_contents_test(ApplyMerge3)
621
622
    def test_contents_merge2(self):
493 by Martin Pool
- Merge aaron's merge command
623
        """Test diff3 merging"""
974.1.4 by Aaron Bentley
Implemented merge3 as the default text merge
624
        self.do_contents_test(changeset.Diff3Merge)
625
974.1.8 by Aaron Bentley
Added default backups for merge-revert
626
    def test_contents_merge3(self):
627
        """Test diff3 merging"""
974.1.23 by Aaron Bentley
Avoided unnecessary temp files
628
        def backup_merge(file_id, base, other):
629
            return BackupBeforeChange(ApplyMerge3(file_id, base, other))
974.1.8 by Aaron Bentley
Added default backups for merge-revert
630
        builder = self.contents_test_success(backup_merge)
631
        def backup_exists(file_id):
632
            return os.path.exists(builder.this.full_path(file_id)+"~")
633
        assert backup_exists("1")
634
        assert backup_exists("2")
635
        assert not backup_exists("3")
636
        builder.cleanup()
637
974.1.4 by Aaron Bentley
Implemented merge3 as the default text merge
638
    def do_contents_test(self, merge_factory):
639
        """Test merging with specified ContentsChange factory"""
974.1.8 by Aaron Bentley
Added default backups for merge-revert
640
        builder = self.contents_test_success(merge_factory)
641
        builder.cleanup()
642
        self.contents_test_conflicts(merge_factory)
643
644
    def contents_test_success(self, merge_factory):
974.1.7 by Aaron Bentley
Fixed handling of merge factory in test suite
645
        from inspect import isclass
493 by Martin Pool
- Merge aaron's merge command
646
        builder = MergeBuilder()
647
        builder.add_file("1", "0", "name1", "text1", 0755)
648
        builder.change_contents("1", other="text4")
649
        builder.add_file("2", "0", "name3", "text2", 0655)
650
        builder.change_contents("2", base="text5")
651
        builder.add_file("3", "0", "name5", "text3", 0744)
974.1.24 by Aaron Bentley
Fixed spurious new_contents_conflict
652
        builder.add_file("4", "0", "name6", "text4", 0744)
653
        builder.remove_file("4", base=True)
654
        assert not builder.cset.entries["4"].is_boring()
493 by Martin Pool
- Merge aaron's merge command
655
        builder.change_contents("3", this="text6")
974.1.7 by Aaron Bentley
Fixed handling of merge factory in test suite
656
        cset = builder.merge_changeset(merge_factory)
493 by Martin Pool
- Merge aaron's merge command
657
        assert(cset.entries["1"].contents_change is not None)
974.1.7 by Aaron Bentley
Fixed handling of merge factory in test suite
658
        if isclass(merge_factory):
659
            assert(isinstance(cset.entries["1"].contents_change,
660
                          merge_factory))
661
            assert(isinstance(cset.entries["2"].contents_change,
662
                          merge_factory))
493 by Martin Pool
- Merge aaron's merge command
663
        assert(cset.entries["3"].is_boring())
974.1.24 by Aaron Bentley
Fixed spurious new_contents_conflict
664
        assert(cset.entries["4"].is_boring())
493 by Martin Pool
- Merge aaron's merge command
665
        builder.apply_changeset(cset)
666
        assert(file(builder.this.full_path("1"), "rb").read() == "text4" )
667
        assert(file(builder.this.full_path("2"), "rb").read() == "text2" )
668
        assert(os.stat(builder.this.full_path("1")).st_mode &0777 == 0755)
669
        assert(os.stat(builder.this.full_path("2")).st_mode &0777 == 0655)
670
        assert(os.stat(builder.this.full_path("3")).st_mode &0777 == 0744)
974.1.8 by Aaron Bentley
Added default backups for merge-revert
671
        return builder
493 by Martin Pool
- Merge aaron's merge command
672
974.1.8 by Aaron Bentley
Added default backups for merge-revert
673
    def contents_test_conflicts(self, merge_factory):
493 by Martin Pool
- Merge aaron's merge command
674
        builder = MergeBuilder()
675
        builder.add_file("1", "0", "name1", "text1", 0755)
676
        builder.change_contents("1", other="text4", this="text3")
974.1.7 by Aaron Bentley
Fixed handling of merge factory in test suite
677
        cset = builder.merge_changeset(merge_factory)
493 by Martin Pool
- Merge aaron's merge command
678
        self.assertRaises(changeset.MergeConflict, builder.apply_changeset,
679
                          cset)
680
        builder.cleanup()
681
909.1.4 by Aaron Bentley
Fixed conflict handling for missing merge targets
682
        builder = MergeBuilder()
683
        builder.add_file("1", "0", "name1", "text1", 0755)
684
        builder.change_contents("1", other="text4", base="text3")
685
        builder.remove_file("1", base=True)
686
        self.assertRaises(changeset.NewContentsConflict,
974.1.7 by Aaron Bentley
Fixed handling of merge factory in test suite
687
                          builder.merge_changeset, merge_factory)
909.1.4 by Aaron Bentley
Fixed conflict handling for missing merge targets
688
        builder.cleanup()
689
690
        builder = MergeBuilder()
691
        builder.add_file("1", "0", "name1", "text1", 0755)
692
        builder.change_contents("1", other="text4", base="text3")
693
        builder.remove_file("1", this=True)
974.1.7 by Aaron Bentley
Fixed handling of merge factory in test suite
694
        self.assertRaises(changeset.MissingForMerge, builder.merge_changeset, 
695
                          merge_factory)
909.1.4 by Aaron Bentley
Fixed conflict handling for missing merge targets
696
        builder.cleanup()
697
493 by Martin Pool
- Merge aaron's merge command
698
    def test_perms_merge(self):
699
        builder = MergeBuilder()
700
        builder.add_file("1", "0", "name1", "text1", 0755)
701
        builder.change_perms("1", other=0655)
702
        builder.add_file("2", "0", "name2", "text2", 0755)
703
        builder.change_perms("2", base=0655)
704
        builder.add_file("3", "0", "name3", "text3", 0755)
705
        builder.change_perms("3", this=0655)
974.1.7 by Aaron Bentley
Fixed handling of merge factory in test suite
706
        cset = builder.merge_changeset(ApplyMerge3)
493 by Martin Pool
- Merge aaron's merge command
707
        assert(cset.entries["1"].metadata_change is not None)
708
        assert(isinstance(cset.entries["1"].metadata_change,
709
                          PermissionsMerge))
710
        assert(isinstance(cset.entries["2"].metadata_change,
711
                          PermissionsMerge))
712
        assert(cset.entries["3"].is_boring())
713
        builder.apply_changeset(cset)
714
        assert(os.stat(builder.this.full_path("1")).st_mode &0777 == 0655)
715
        assert(os.stat(builder.this.full_path("2")).st_mode &0777 == 0755)
716
        assert(os.stat(builder.this.full_path("3")).st_mode &0777 == 0655)
717
        builder.cleanup();
718
        builder = MergeBuilder()
719
        builder.add_file("1", "0", "name1", "text1", 0755)
720
        builder.change_perms("1", other=0655, base=0555)
974.1.7 by Aaron Bentley
Fixed handling of merge factory in test suite
721
        cset = builder.merge_changeset(ApplyMerge3)
493 by Martin Pool
- Merge aaron's merge command
722
        self.assertRaises(changeset.MergePermissionConflict, 
723
                     builder.apply_changeset, cset)
724
        builder.cleanup()
725
726
def test():        
727
    changeset_suite = unittest.makeSuite(MergeTest, 'test_')
728
    runner = unittest.TextTestRunner()
729
    runner.run(changeset_suite)
730
        
731
if __name__ == "__main__":
732
    test()