/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar
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
4
558 by Martin Pool
- All top-level classes inherit from object
5
class ThreewayInventory(object):
493 by Martin Pool
- Merge aaron's merge command
6
    def __init__(self, this_inventory, base_inventory, other_inventory):
7
        self.this = this_inventory
8
        self.base = base_inventory
9
        self.other = other_inventory
10
def invert_invent(inventory):
11
    invert_invent = {}
12
    for key, value in inventory.iteritems():
13
        invert_invent[value.id] = key
14
    return invert_invent
15
16
def make_inv(inventory):
17
    return Inventory(invert_invent(inventory))
18
        
19
20
def merge_flex(this, base, other, changeset_function, inventory_function,
21
               conflict_handler):
22
    this_inventory = inventory_function(this)
23
    base_inventory = inventory_function(base)
24
    other_inventory = inventory_function(other)
25
    inventory = ThreewayInventory(make_inv(this_inventory),
26
                                  make_inv(base_inventory), 
27
                                  make_inv(other_inventory))
28
    cset = changeset_function(base, other, base_inventory, other_inventory)
29
    new_cset = make_merge_changeset(cset, inventory, this, base, other, 
30
                                    conflict_handler)
622 by Martin Pool
Updated merge patch from Aaron
31
    result = apply_changeset(new_cset, invert_invent(this_inventory),
32
                             this.root, conflict_handler, False)
33
    conflict_handler.finalize()
34
    return result
493 by Martin Pool
- Merge aaron's merge command
35
36
    
37
38
def make_merge_changeset(cset, inventory, this, base, other, 
39
                         conflict_handler=None):
40
    new_cset = changeset.Changeset()
41
    def get_this_contents(id):
42
        path = os.path.join(this.root, inventory.this.get_path(id))
43
        if os.path.isdir(path):
44
            return changeset.dir_create
45
        else:
46
            return changeset.FileCreate(file(path, "rb").read())
47
48
    for entry in cset.entries.itervalues():
49
        if entry.is_boring():
50
            new_cset.add_entry(entry)
51
        else:
850 by Martin Pool
- Merge merge updates from aaron
52
            new_entry = make_merged_entry(entry, inventory, conflict_handler)
909.1.4 by Aaron Bentley
Fixed conflict handling for missing merge targets
53
            new_contents = make_merged_contents(entry, this, base, other, 
54
                                                inventory, conflict_handler)
850 by Martin Pool
- Merge merge updates from aaron
55
            new_entry.contents_change = new_contents
56
            new_entry.metadata_change = make_merged_metadata(entry, base, other)
57
            new_cset.add_entry(new_entry)
58
493 by Martin Pool
- Merge aaron's merge command
59
    return new_cset
60
850 by Martin Pool
- Merge merge updates from aaron
61
def make_merged_entry(entry, inventory, conflict_handler):
909.1.2 by aaron.bentley at utoronto
Fixed rename handling in merge
62
    from bzrlib.trace import mutter
493 by Martin Pool
- Merge aaron's merge command
63
    this_name = inventory.this.get_name(entry.id)
64
    this_parent = inventory.this.get_parent(entry.id)
65
    this_dir = inventory.this.get_dir(entry.id)
66
    if this_dir is None:
67
        this_dir = ""
68
69
    base_name = inventory.base.get_name(entry.id)
70
    base_parent = inventory.base.get_parent(entry.id)
71
    base_dir = inventory.base.get_dir(entry.id)
72
    if base_dir is None:
73
        base_dir = ""
74
    other_name = inventory.other.get_name(entry.id)
75
    other_parent = inventory.other.get_parent(entry.id)
76
    other_dir = inventory.base.get_dir(entry.id)
77
    if other_dir is None:
78
        other_dir = ""
909.1.2 by aaron.bentley at utoronto
Fixed rename handling in merge
79
    mutter("Dirs: this, base, other %r %r %r" % (this_dir, base_dir, other_dir))
80
    mutter("Names: this, base, other %r %r %r" % (this_name, base_name, other_name))
493 by Martin Pool
- Merge aaron's merge command
81
    if base_name == other_name:
82
        old_name = this_name
83
        new_name = this_name
84
    else:
85
        if this_name != base_name and this_name != other_name:
86
            conflict_handler.rename_conflict(entry.id, this_name, base_name,
87
                                             other_name)
88
        else:
89
            old_name = this_name
90
            new_name = other_name
91
92
    if base_parent == other_parent:
93
        old_parent = this_parent
94
        new_parent = this_parent
95
        old_dir = this_dir
96
        new_dir = this_dir
97
    else:
98
        if this_parent != base_parent and this_parent != other_parent:
99
            conflict_handler.move_conflict(entry.id, inventory)
100
        else:
101
            old_parent = this_parent
102
            old_dir = this_dir
103
            new_parent = other_parent
104
            new_dir = other_dir
850 by Martin Pool
- Merge merge updates from aaron
105
    if old_name is not None and old_parent is not None:
106
        old_path = os.path.join(old_dir, old_name)
107
    else:
108
        old_path = None
909.1.2 by aaron.bentley at utoronto
Fixed rename handling in merge
109
    new_entry = changeset.ChangesetEntry(entry.id, old_parent, old_path)
850 by Martin Pool
- Merge merge updates from aaron
110
    if new_name is not None and new_parent is not None:
493 by Martin Pool
- Merge aaron's merge command
111
        new_entry.new_path = os.path.join(new_dir, new_name)
112
    else:
113
        new_entry.new_path = None
114
    new_entry.new_parent = new_parent
909.1.2 by aaron.bentley at utoronto
Fixed rename handling in merge
115
    mutter(repr(new_entry))
850 by Martin Pool
- Merge merge updates from aaron
116
    return new_entry
117
118
909.1.4 by Aaron Bentley
Fixed conflict handling for missing merge targets
119
def make_merged_contents(entry, this, base, other, inventory, conflict_handler):
850 by Martin Pool
- Merge merge updates from aaron
120
    contents = entry.contents_change
121
    if contents is None:
122
        return None
123
    this_path = this.readonly_path(entry.id)
124
    def make_diff3():
125
        if this_path is None:
126
            return conflict_handler.missing_for_merge(entry.id, inventory)
127
        base_path = base.readonly_path(entry.id)
128
        other_path = other.readonly_path(entry.id)    
129
        return changeset.Diff3Merge(base_path, other_path)
130
131
    if isinstance(contents, changeset.PatchApply):
132
        return make_diff3()
133
    if isinstance(contents, changeset.ReplaceContents):
134
        if contents.old_contents is None and contents.new_contents is None:
135
            return None
136
        if contents.new_contents is None:
137
            if this_path is not None and os.path.exists(this_path):
138
                return contents
139
            else:
140
                return None
141
        elif contents.old_contents is None:
142
            if this_path is None or not os.path.exists(this_path):
143
                return contents
144
            else:
145
                this_contents = file(this_path, "rb").read()
146
                if this_contents == contents.new_contents:
147
                    return None
148
                else:
149
                    other_path = other.readonly_path(entry.id)    
150
                    conflict_handler.new_contents_conflict(this_path, 
151
                                                           other_path)
152
        elif isinstance(contents.old_contents, changeset.FileCreate) and \
153
            isinstance(contents.new_contents, changeset.FileCreate):
154
            return make_diff3()
155
        else:
156
            raise Exception("Unhandled merge scenario")
157
158
def make_merged_metadata(entry, base, other):
159
    if entry.metadata_change is not None:
160
        base_path = base.readonly_path(entry.id)
161
        other_path = other.readonly_path(entry.id)    
162
        return PermissionsMerge(base_path, other_path)
493 by Martin Pool
- Merge aaron's merge command
163
    
850 by Martin Pool
- Merge merge updates from aaron
164
def get_merge_entry(entry, inventory, base, other, conflict_handler):
493 by Martin Pool
- Merge aaron's merge command
165
    if entry.contents_change is not None:
166
        new_entry.contents_change = changeset.Diff3Merge(base_path, other_path)
167
    if entry.metadata_change is not None:
168
        new_entry.metadata_change = PermissionsMerge(base_path, other_path)
169
170
    return new_entry
171
558 by Martin Pool
- All top-level classes inherit from object
172
class PermissionsMerge(object):
493 by Martin Pool
- Merge aaron's merge command
173
    def __init__(self, base_path, other_path):
174
        self.base_path = base_path
175
        self.other_path = other_path
176
177
    def apply(self, filename, conflict_handler, reverse=False):
178
        if not reverse:
179
            base = self.base_path
180
            other = self.other_path
181
        else:
182
            base = self.other_path
183
            other = self.base_path
184
        base_stat = os.stat(base).st_mode
185
        other_stat = os.stat(other).st_mode
186
        this_stat = os.stat(filename).st_mode
187
        if base_stat &0777 == other_stat &0777:
188
            return
189
        elif this_stat &0777 == other_stat &0777:
190
            return
191
        elif this_stat &0777 == base_stat &0777:
192
            os.chmod(filename, other_stat)
193
        else:
194
            conflict_handler.permission_conflict(filename, base, other)
195
196
197
import unittest
198
import tempfile
199
import shutil
558 by Martin Pool
- All top-level classes inherit from object
200
class MergeTree(object):
493 by Martin Pool
- Merge aaron's merge command
201
    def __init__(self, dir):
202
        self.dir = dir;
203
        os.mkdir(dir)
204
        self.inventory = {'0': ""}
205
    
206
    def child_path(self, parent, name):
207
        return os.path.join(self.inventory[parent], name)
208
209
    def add_file(self, id, parent, name, contents, mode):
210
        path = self.child_path(parent, name)
211
        full_path = self.abs_path(path)
212
        assert not os.path.exists(full_path)
213
        file(full_path, "wb").write(contents)
214
        os.chmod(self.abs_path(path), mode)
215
        self.inventory[id] = path
216
909.1.4 by Aaron Bentley
Fixed conflict handling for missing merge targets
217
    def remove_file(self, id):
218
        os.unlink(self.full_path(id))
219
        del self.inventory[id]
220
493 by Martin Pool
- Merge aaron's merge command
221
    def add_dir(self, id, parent, name, mode):
222
        path = self.child_path(parent, name)
223
        full_path = self.abs_path(path)
224
        assert not os.path.exists(full_path)
225
        os.mkdir(self.abs_path(path))
226
        os.chmod(self.abs_path(path), mode)
227
        self.inventory[id] = path
228
229
    def abs_path(self, path):
230
        return os.path.join(self.dir, path)
231
232
    def full_path(self, id):
909.1.4 by Aaron Bentley
Fixed conflict handling for missing merge targets
233
        try:
234
            tree_path = self.inventory[id]
235
        except KeyError:
236
            return None
237
        return self.abs_path(tree_path)
493 by Martin Pool
- Merge aaron's merge command
238
850 by Martin Pool
- Merge merge updates from aaron
239
    def readonly_path(self, id):
240
        return self.full_path(id)
241
493 by Martin Pool
- Merge aaron's merge command
242
    def change_path(self, id, path):
243
        new = os.path.join(self.dir, self.inventory[id])
244
        os.rename(self.abs_path(self.inventory[id]), self.abs_path(path))
245
        self.inventory[id] = path
246
558 by Martin Pool
- All top-level classes inherit from object
247
class MergeBuilder(object):
493 by Martin Pool
- Merge aaron's merge command
248
    def __init__(self):
249
        self.dir = tempfile.mkdtemp(prefix="BaZing")
250
        self.base = MergeTree(os.path.join(self.dir, "base"))
251
        self.this = MergeTree(os.path.join(self.dir, "this"))
252
        self.other = MergeTree(os.path.join(self.dir, "other"))
253
        
254
        self.cset = changeset.Changeset()
255
        self.cset.add_entry(changeset.ChangesetEntry("0", 
256
                                                     changeset.NULL_ID, "./."))
257
    def get_cset_path(self, parent, name):
258
        if name is None:
259
            assert (parent is None)
260
            return None
261
        return os.path.join(self.cset.entries[parent].path, name)
262
263
    def add_file(self, id, parent, name, contents, mode):
264
        self.base.add_file(id, parent, name, contents, mode)
265
        self.this.add_file(id, parent, name, contents, mode)
266
        self.other.add_file(id, parent, name, contents, mode)
267
        path = self.get_cset_path(parent, name)
268
        self.cset.add_entry(changeset.ChangesetEntry(id, parent, path))
269
909.1.4 by Aaron Bentley
Fixed conflict handling for missing merge targets
270
    def remove_file(self, id, base=False, this=False, other=False):
271
        for option, tree in ((base, self.base), (this, self.this), 
272
                             (other, self.other)):
273
            if option:
274
                tree.remove_file(id)
275
            if other or base:
276
                change = self.cset.entries[id].contents_change
277
                assert isinstance(change, changeset.ReplaceContents)
278
                if other:
279
                    change.new_contents=None
280
                if base:
281
                    change.old_contents=None
282
                if change.old_contents is None and change.new_contents is None:
283
                    change = None
284
285
493 by Martin Pool
- Merge aaron's merge command
286
    def add_dir(self, id, parent, name, mode):
287
        path = self.get_cset_path(parent, name)
288
        self.base.add_dir(id, parent, name, mode)
289
        self.cset.add_entry(changeset.ChangesetEntry(id, parent, path))
290
        self.this.add_dir(id, parent, name, mode)
291
        self.other.add_dir(id, parent, name, mode)
292
293
294
    def change_name(self, id, base=None, this=None, other=None):
295
        if base is not None:
296
            self.change_name_tree(id, self.base, base)
297
            self.cset.entries[id].name = base
298
299
        if this is not None:
300
            self.change_name_tree(id, self.this, this)
301
302
        if other is not None:
303
            self.change_name_tree(id, self.other, other)
304
            self.cset.entries[id].new_name = other
305
306
    def change_parent(self, id, base=None, this=None, other=None):
307
        if base is not None:
308
            self.change_parent_tree(id, self.base, base)
309
            self.cset.entries[id].parent = base
310
            self.cset.entries[id].dir = self.cset.entries[base].path
311
312
        if this is not None:
313
            self.change_parent_tree(id, self.this, this)
314
315
        if other is not None:
316
            self.change_parent_tree(id, self.other, other)
317
            self.cset.entries[id].new_parent = other
318
            self.cset.entries[id].new_dir = \
319
                self.cset.entries[other].new_path
320
321
    def change_contents(self, id, base=None, this=None, other=None):
322
        if base is not None:
323
            self.change_contents_tree(id, self.base, base)
324
325
        if this is not None:
326
            self.change_contents_tree(id, self.this, this)
327
328
        if other is not None:
329
            self.change_contents_tree(id, self.other, other)
330
331
        if base is not None or other is not None:
332
            old_contents = file(self.base.full_path(id)).read()
333
            new_contents = file(self.other.full_path(id)).read()
334
            contents = changeset.ReplaceFileContents(old_contents, 
335
                                                     new_contents)
336
            self.cset.entries[id].contents_change = contents
337
338
    def change_perms(self, id, base=None, this=None, other=None):
339
        if base is not None:
340
            self.change_perms_tree(id, self.base, base)
341
342
        if this is not None:
343
            self.change_perms_tree(id, self.this, this)
344
345
        if other is not None:
346
            self.change_perms_tree(id, self.other, other)
347
348
        if base is not None or other is not None:
349
            old_perms = os.stat(self.base.full_path(id)).st_mode &077
350
            new_perms = os.stat(self.other.full_path(id)).st_mode &077
351
            contents = changeset.ChangeUnixPermissions(old_perms, 
352
                                                       new_perms)
353
            self.cset.entries[id].metadata_change = contents
354
355
    def change_name_tree(self, id, tree, name):
356
        new_path = tree.child_path(self.cset.entries[id].parent, name)
357
        tree.change_path(id, new_path)
358
359
    def change_parent_tree(self, id, tree, parent):
360
        new_path = tree.child_path(parent, self.cset.entries[id].name)
361
        tree.change_path(id, new_path)
362
363
    def change_contents_tree(self, id, tree, contents):
364
        path = tree.full_path(id)
365
        mode = os.stat(path).st_mode
366
        file(path, "w").write(contents)
367
        os.chmod(path, mode)
368
369
    def change_perms_tree(self, id, tree, mode):
370
        os.chmod(tree.full_path(id), mode)
371
372
    def merge_changeset(self):
373
        all_inventory = ThreewayInventory(Inventory(self.this.inventory),
374
                                          Inventory(self.base.inventory), 
375
                                          Inventory(self.other.inventory))
376
        conflict_handler = changeset.ExceptionConflictHandler(self.this.dir)
850 by Martin Pool
- Merge merge updates from aaron
377
        return make_merge_changeset(self.cset, all_inventory, self.this,
378
                                    self.base, self.other, conflict_handler)
379
380
    def apply_inv_change(self, inventory_change, orig_inventory):
381
        orig_inventory_by_path = {}
382
        for file_id, path in orig_inventory.iteritems():
383
            orig_inventory_by_path[path] = file_id
384
385
        def parent_id(file_id):
386
            try:
387
                parent_dir = os.path.dirname(orig_inventory[file_id])
388
            except:
389
                print file_id
390
                raise
391
            if parent_dir == "":
392
                return None
393
            return orig_inventory_by_path[parent_dir]
394
        
395
        def new_path(file_id):
396
            if inventory_change.has_key(file_id):
397
                return inventory_change[file_id]
398
            else:
399
                parent = parent_id(file_id)
400
                if parent is None:
401
                    return orig_inventory[file_id]
402
                dirname = new_path(parent)
403
                return os.path.join(dirname, orig_inventory[file_id])
404
405
        new_inventory = {}
406
        for file_id in orig_inventory.iterkeys():
407
            path = new_path(file_id)
408
            if path is None:
409
                continue
410
            new_inventory[file_id] = path
411
412
        for file_id, path in inventory_change.iteritems():
413
            if orig_inventory.has_key(file_id):
414
                continue
415
            new_inventory[file_id] = path
416
        return new_inventory
417
418
        
419
493 by Martin Pool
- Merge aaron's merge command
420
    def apply_changeset(self, cset, conflict_handler=None, reverse=False):
850 by Martin Pool
- Merge merge updates from aaron
421
        inventory_change = changeset.apply_changeset(cset,
422
                                                     self.this.inventory,
423
                                                     self.this.dir,
424
                                                     conflict_handler, reverse)
425
        self.this.inventory =  self.apply_inv_change(inventory_change, 
426
                                                     self.this.inventory)
427
428
                    
429
        
430
493 by Martin Pool
- Merge aaron's merge command
431
        
432
    def cleanup(self):
433
        shutil.rmtree(self.dir)
434
435
436
class MergeTest(unittest.TestCase):
437
    def test_change_name(self):
438
        """Test renames"""
439
        builder = MergeBuilder()
440
        builder.add_file("1", "0", "name1", "hello1", 0755)
441
        builder.change_name("1", other="name2")
442
        builder.add_file("2", "0", "name3", "hello2", 0755)
443
        builder.change_name("2", base="name4")
444
        builder.add_file("3", "0", "name5", "hello3", 0755)
445
        builder.change_name("3", this="name6")
446
        cset = builder.merge_changeset()
447
        assert(cset.entries["2"].is_boring())
448
        assert(cset.entries["1"].name == "name1")
449
        assert(cset.entries["1"].new_name == "name2")
450
        assert(cset.entries["3"].is_boring())
451
        for tree in (builder.this, builder.other, builder.base):
452
            assert(tree.dir != builder.dir and 
453
                   tree.dir.startswith(builder.dir))
454
            for path in tree.inventory.itervalues():
455
                fullpath = tree.abs_path(path)
456
                assert(fullpath.startswith(tree.dir))
457
                assert(not path.startswith(tree.dir))
458
                assert os.path.exists(fullpath)
459
        builder.apply_changeset(cset)
460
        builder.cleanup()
461
        builder = MergeBuilder()
462
        builder.add_file("1", "0", "name1", "hello1", 0644)
463
        builder.change_name("1", other="name2", this="name3")
464
        self.assertRaises(changeset.RenameConflict, 
465
                          builder.merge_changeset)
466
        builder.cleanup()
467
        
468
    def test_file_moves(self):
469
        """Test moves"""
470
        builder = MergeBuilder()
471
        builder.add_dir("1", "0", "dir1", 0755)
472
        builder.add_dir("2", "0", "dir2", 0755)
473
        builder.add_file("3", "1", "file1", "hello1", 0644)
474
        builder.add_file("4", "1", "file2", "hello2", 0644)
475
        builder.add_file("5", "1", "file3", "hello3", 0644)
476
        builder.change_parent("3", other="2")
477
        assert(Inventory(builder.other.inventory).get_parent("3") == "2")
478
        builder.change_parent("4", this="2")
479
        assert(Inventory(builder.this.inventory).get_parent("4") == "2")
480
        builder.change_parent("5", base="2")
481
        assert(Inventory(builder.base.inventory).get_parent("5") == "2")
482
        cset = builder.merge_changeset()
483
        for id in ("1", "2", "4", "5"):
484
            assert(cset.entries[id].is_boring())
485
        assert(cset.entries["3"].parent == "1")
486
        assert(cset.entries["3"].new_parent == "2")
487
        builder.apply_changeset(cset)
488
        builder.cleanup()
489
490
        builder = MergeBuilder()
491
        builder.add_dir("1", "0", "dir1", 0755)
492
        builder.add_dir("2", "0", "dir2", 0755)
493
        builder.add_dir("3", "0", "dir3", 0755)
494
        builder.add_file("4", "1", "file1", "hello1", 0644)
495
        builder.change_parent("4", other="2", this="3")
496
        self.assertRaises(changeset.MoveConflict, 
497
                          builder.merge_changeset)
498
        builder.cleanup()
499
500
    def test_contents_merge(self):
501
        """Test diff3 merging"""
502
        builder = MergeBuilder()
503
        builder.add_file("1", "0", "name1", "text1", 0755)
504
        builder.change_contents("1", other="text4")
505
        builder.add_file("2", "0", "name3", "text2", 0655)
506
        builder.change_contents("2", base="text5")
507
        builder.add_file("3", "0", "name5", "text3", 0744)
508
        builder.change_contents("3", this="text6")
509
        cset = builder.merge_changeset()
510
        assert(cset.entries["1"].contents_change is not None)
511
        assert(isinstance(cset.entries["1"].contents_change,
512
                          changeset.Diff3Merge))
513
        assert(isinstance(cset.entries["2"].contents_change,
514
                          changeset.Diff3Merge))
515
        assert(cset.entries["3"].is_boring())
516
        builder.apply_changeset(cset)
517
        assert(file(builder.this.full_path("1"), "rb").read() == "text4" )
518
        assert(file(builder.this.full_path("2"), "rb").read() == "text2" )
519
        assert(os.stat(builder.this.full_path("1")).st_mode &0777 == 0755)
520
        assert(os.stat(builder.this.full_path("2")).st_mode &0777 == 0655)
521
        assert(os.stat(builder.this.full_path("3")).st_mode &0777 == 0744)
522
        builder.cleanup()
523
524
        builder = MergeBuilder()
525
        builder.add_file("1", "0", "name1", "text1", 0755)
526
        builder.change_contents("1", other="text4", this="text3")
527
        cset = builder.merge_changeset()
528
        self.assertRaises(changeset.MergeConflict, builder.apply_changeset,
529
                          cset)
530
        builder.cleanup()
531
909.1.4 by Aaron Bentley
Fixed conflict handling for missing merge targets
532
        builder = MergeBuilder()
533
        builder.add_file("1", "0", "name1", "text1", 0755)
534
        builder.change_contents("1", other="text4", base="text3")
535
        builder.remove_file("1", base=True)
536
        self.assertRaises(changeset.NewContentsConflict,
537
                          builder.merge_changeset)
538
        builder.cleanup()
539
540
        builder = MergeBuilder()
541
        builder.add_file("1", "0", "name1", "text1", 0755)
542
        builder.change_contents("1", other="text4", base="text3")
543
        builder.remove_file("1", this=True)
544
        self.assertRaises(changeset.MissingForMerge, builder.merge_changeset)
545
        builder.cleanup()
546
493 by Martin Pool
- Merge aaron's merge command
547
    def test_perms_merge(self):
548
        builder = MergeBuilder()
549
        builder.add_file("1", "0", "name1", "text1", 0755)
550
        builder.change_perms("1", other=0655)
551
        builder.add_file("2", "0", "name2", "text2", 0755)
552
        builder.change_perms("2", base=0655)
553
        builder.add_file("3", "0", "name3", "text3", 0755)
554
        builder.change_perms("3", this=0655)
555
        cset = builder.merge_changeset()
556
        assert(cset.entries["1"].metadata_change is not None)
557
        assert(isinstance(cset.entries["1"].metadata_change,
558
                          PermissionsMerge))
559
        assert(isinstance(cset.entries["2"].metadata_change,
560
                          PermissionsMerge))
561
        assert(cset.entries["3"].is_boring())
562
        builder.apply_changeset(cset)
563
        assert(os.stat(builder.this.full_path("1")).st_mode &0777 == 0655)
564
        assert(os.stat(builder.this.full_path("2")).st_mode &0777 == 0755)
565
        assert(os.stat(builder.this.full_path("3")).st_mode &0777 == 0655)
566
        builder.cleanup();
567
        builder = MergeBuilder()
568
        builder.add_file("1", "0", "name1", "text1", 0755)
569
        builder.change_perms("1", other=0655, base=0555)
570
        cset = builder.merge_changeset()
571
        self.assertRaises(changeset.MergePermissionConflict, 
572
                     builder.apply_changeset, cset)
573
        builder.cleanup()
574
575
def test():        
576
    changeset_suite = unittest.makeSuite(MergeTest, 'test_')
577
    runner = unittest.TextTestRunner()
578
    runner.run(changeset_suite)
579
        
580
if __name__ == "__main__":
581
    test()