/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_commit.py

More work on roundtrip push support.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006 Canonical Ltd
2
 
#
3
 
# This program is free software; you can redistribute it and/or modify
4
 
# it under the terms of the GNU General Public License as published by
5
 
# the Free Software Foundation; either version 2 of the License, or
6
 
# (at your option) any later version.
7
 
#
8
 
# This program is distributed in the hope that it will be useful,
9
 
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
 
# GNU General Public License for more details.
12
 
#
13
 
# You should have received a copy of the GNU General Public License
14
 
# along with this program; if not, write to the Free Software
15
 
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
 
 
17
 
 
18
 
import os
19
 
 
20
 
import bzrlib
21
 
from bzrlib import (
22
 
    errors,
23
 
    lockdir,
24
 
    osutils,
25
 
    tests,
26
 
    )
27
 
from bzrlib.branch import Branch
28
 
from bzrlib.bzrdir import BzrDir, BzrDirMetaFormat1
29
 
from bzrlib.commit import Commit, NullCommitReporter
30
 
from bzrlib.config import BranchConfig
31
 
from bzrlib.errors import (PointlessCommit, BzrError, SigningFailed, 
32
 
                           LockContention)
33
 
from bzrlib.tests import SymlinkFeature, TestCaseWithTransport
34
 
from bzrlib.workingtree import WorkingTree
35
 
 
36
 
 
37
 
# TODO: Test commit with some added, and added-but-missing files
38
 
 
39
 
class MustSignConfig(BranchConfig):
40
 
 
41
 
    def signature_needed(self):
42
 
        return True
43
 
 
44
 
    def gpg_signing_command(self):
45
 
        return ['cat', '-']
46
 
 
47
 
 
48
 
class BranchWithHooks(BranchConfig):
49
 
 
50
 
    def post_commit(self):
51
 
        return "bzrlib.ahook bzrlib.ahook"
52
 
 
53
 
 
54
 
class CapturingReporter(NullCommitReporter):
55
 
    """This reporter captures the calls made to it for evaluation later."""
56
 
 
57
 
    def __init__(self):
58
 
        # a list of the calls this received
59
 
        self.calls = []
60
 
 
61
 
    def snapshot_change(self, change, path):
62
 
        self.calls.append(('change', change, path))
63
 
 
64
 
    def deleted(self, file_id):
65
 
        self.calls.append(('deleted', file_id))
66
 
 
67
 
    def missing(self, path):
68
 
        self.calls.append(('missing', path))
69
 
 
70
 
    def renamed(self, change, old_path, new_path):
71
 
        self.calls.append(('renamed', change, old_path, new_path))
72
 
 
73
 
    def is_verbose(self):
74
 
        return True
75
 
 
76
 
 
77
 
class TestCommit(TestCaseWithTransport):
78
 
 
79
 
    def test_simple_commit(self):
80
 
        """Commit and check two versions of a single file."""
81
 
        wt = self.make_branch_and_tree('.')
82
 
        b = wt.branch
83
 
        file('hello', 'w').write('hello world')
84
 
        wt.add('hello')
85
 
        wt.commit(message='add hello')
86
 
        file_id = wt.path2id('hello')
87
 
 
88
 
        file('hello', 'w').write('version 2')
89
 
        wt.commit(message='commit 2')
90
 
 
91
 
        eq = self.assertEquals
92
 
        eq(b.revno(), 2)
93
 
        rh = b.revision_history()
94
 
        rev = b.repository.get_revision(rh[0])
95
 
        eq(rev.message, 'add hello')
96
 
 
97
 
        tree1 = b.repository.revision_tree(rh[0])
98
 
        tree1.lock_read()
99
 
        text = tree1.get_file_text(file_id)
100
 
        tree1.unlock()
101
 
        self.assertEqual('hello world', text)
102
 
 
103
 
        tree2 = b.repository.revision_tree(rh[1])
104
 
        tree2.lock_read()
105
 
        text = tree2.get_file_text(file_id)
106
 
        tree2.unlock()
107
 
        self.assertEqual('version 2', text)
108
 
 
109
 
    def test_delete_commit(self):
110
 
        """Test a commit with a deleted file"""
111
 
        wt = self.make_branch_and_tree('.')
112
 
        b = wt.branch
113
 
        file('hello', 'w').write('hello world')
114
 
        wt.add(['hello'], ['hello-id'])
115
 
        wt.commit(message='add hello')
116
 
 
117
 
        os.remove('hello')
118
 
        wt.commit('removed hello', rev_id='rev2')
119
 
 
120
 
        tree = b.repository.revision_tree('rev2')
121
 
        self.assertFalse(tree.has_id('hello-id'))
122
 
 
123
 
    def test_pointless_commit(self):
124
 
        """Commit refuses unless there are changes or it's forced."""
125
 
        wt = self.make_branch_and_tree('.')
126
 
        b = wt.branch
127
 
        file('hello', 'w').write('hello')
128
 
        wt.add(['hello'])
129
 
        wt.commit(message='add hello')
130
 
        self.assertEquals(b.revno(), 1)
131
 
        self.assertRaises(PointlessCommit,
132
 
                          wt.commit,
133
 
                          message='fails',
134
 
                          allow_pointless=False)
135
 
        self.assertEquals(b.revno(), 1)
136
 
        
137
 
    def test_commit_empty(self):
138
 
        """Commiting an empty tree works."""
139
 
        wt = self.make_branch_and_tree('.')
140
 
        b = wt.branch
141
 
        wt.commit(message='empty tree', allow_pointless=True)
142
 
        self.assertRaises(PointlessCommit,
143
 
                          wt.commit,
144
 
                          message='empty tree',
145
 
                          allow_pointless=False)
146
 
        wt.commit(message='empty tree', allow_pointless=True)
147
 
        self.assertEquals(b.revno(), 2)
148
 
 
149
 
    def test_selective_delete(self):
150
 
        """Selective commit in tree with deletions"""
151
 
        wt = self.make_branch_and_tree('.')
152
 
        b = wt.branch
153
 
        file('hello', 'w').write('hello')
154
 
        file('buongia', 'w').write('buongia')
155
 
        wt.add(['hello', 'buongia'],
156
 
              ['hello-id', 'buongia-id'])
157
 
        wt.commit(message='add files',
158
 
                 rev_id='test@rev-1')
159
 
        
160
 
        os.remove('hello')
161
 
        file('buongia', 'w').write('new text')
162
 
        wt.commit(message='update text',
163
 
                 specific_files=['buongia'],
164
 
                 allow_pointless=False,
165
 
                 rev_id='test@rev-2')
166
 
 
167
 
        wt.commit(message='remove hello',
168
 
                 specific_files=['hello'],
169
 
                 allow_pointless=False,
170
 
                 rev_id='test@rev-3')
171
 
 
172
 
        eq = self.assertEquals
173
 
        eq(b.revno(), 3)
174
 
 
175
 
        tree2 = b.repository.revision_tree('test@rev-2')
176
 
        tree2.lock_read()
177
 
        self.addCleanup(tree2.unlock)
178
 
        self.assertTrue(tree2.has_filename('hello'))
179
 
        self.assertEquals(tree2.get_file_text('hello-id'), 'hello')
180
 
        self.assertEquals(tree2.get_file_text('buongia-id'), 'new text')
181
 
        
182
 
        tree3 = b.repository.revision_tree('test@rev-3')
183
 
        tree3.lock_read()
184
 
        self.addCleanup(tree3.unlock)
185
 
        self.assertFalse(tree3.has_filename('hello'))
186
 
        self.assertEquals(tree3.get_file_text('buongia-id'), 'new text')
187
 
 
188
 
    def test_commit_rename(self):
189
 
        """Test commit of a revision where a file is renamed."""
190
 
        tree = self.make_branch_and_tree('.')
191
 
        b = tree.branch
192
 
        self.build_tree(['hello'], line_endings='binary')
193
 
        tree.add(['hello'], ['hello-id'])
194
 
        tree.commit(message='one', rev_id='test@rev-1', allow_pointless=False)
195
 
 
196
 
        tree.rename_one('hello', 'fruity')
197
 
        tree.commit(message='renamed', rev_id='test@rev-2', allow_pointless=False)
198
 
 
199
 
        eq = self.assertEquals
200
 
        tree1 = b.repository.revision_tree('test@rev-1')
201
 
        tree1.lock_read()
202
 
        self.addCleanup(tree1.unlock)
203
 
        eq(tree1.id2path('hello-id'), 'hello')
204
 
        eq(tree1.get_file_text('hello-id'), 'contents of hello\n')
205
 
        self.assertFalse(tree1.has_filename('fruity'))
206
 
        self.check_inventory_shape(tree1.inventory, ['hello'])
207
 
        ie = tree1.inventory['hello-id']
208
 
        eq(ie.revision, 'test@rev-1')
209
 
 
210
 
        tree2 = b.repository.revision_tree('test@rev-2')
211
 
        tree2.lock_read()
212
 
        self.addCleanup(tree2.unlock)
213
 
        eq(tree2.id2path('hello-id'), 'fruity')
214
 
        eq(tree2.get_file_text('hello-id'), 'contents of hello\n')
215
 
        self.check_inventory_shape(tree2.inventory, ['fruity'])
216
 
        ie = tree2.inventory['hello-id']
217
 
        eq(ie.revision, 'test@rev-2')
218
 
 
219
 
    def test_reused_rev_id(self):
220
 
        """Test that a revision id cannot be reused in a branch"""
221
 
        wt = self.make_branch_and_tree('.')
222
 
        b = wt.branch
223
 
        wt.commit('initial', rev_id='test@rev-1', allow_pointless=True)
224
 
        self.assertRaises(Exception,
225
 
                          wt.commit,
226
 
                          message='reused id',
227
 
                          rev_id='test@rev-1',
228
 
                          allow_pointless=True)
229
 
 
230
 
    def test_commit_move(self):
231
 
        """Test commit of revisions with moved files and directories"""
232
 
        eq = self.assertEquals
233
 
        wt = self.make_branch_and_tree('.')
234
 
        b = wt.branch
235
 
        r1 = 'test@rev-1'
236
 
        self.build_tree(['hello', 'a/', 'b/'])
237
 
        wt.add(['hello', 'a', 'b'], ['hello-id', 'a-id', 'b-id'])
238
 
        wt.commit('initial', rev_id=r1, allow_pointless=False)
239
 
        wt.move(['hello'], 'a')
240
 
        r2 = 'test@rev-2'
241
 
        wt.commit('two', rev_id=r2, allow_pointless=False)
242
 
        wt.lock_read()
243
 
        try:
244
 
            self.check_inventory_shape(wt.read_working_inventory(),
245
 
                                       ['a/', 'a/hello', 'b/'])
246
 
        finally:
247
 
            wt.unlock()
248
 
 
249
 
        wt.move(['b'], 'a')
250
 
        r3 = 'test@rev-3'
251
 
        wt.commit('three', rev_id=r3, allow_pointless=False)
252
 
        wt.lock_read()
253
 
        try:
254
 
            self.check_inventory_shape(wt.read_working_inventory(),
255
 
                                       ['a/', 'a/hello', 'a/b/'])
256
 
            self.check_inventory_shape(b.repository.get_revision_inventory(r3),
257
 
                                       ['a/', 'a/hello', 'a/b/'])
258
 
        finally:
259
 
            wt.unlock()
260
 
 
261
 
        wt.move(['a/hello'], 'a/b')
262
 
        r4 = 'test@rev-4'
263
 
        wt.commit('four', rev_id=r4, allow_pointless=False)
264
 
        wt.lock_read()
265
 
        try:
266
 
            self.check_inventory_shape(wt.read_working_inventory(),
267
 
                                       ['a/', 'a/b/hello', 'a/b/'])
268
 
        finally:
269
 
            wt.unlock()
270
 
 
271
 
        inv = b.repository.get_revision_inventory(r4)
272
 
        eq(inv['hello-id'].revision, r4)
273
 
        eq(inv['a-id'].revision, r1)
274
 
        eq(inv['b-id'].revision, r3)
275
 
 
276
 
    def test_removed_commit(self):
277
 
        """Commit with a removed file"""
278
 
        wt = self.make_branch_and_tree('.')
279
 
        b = wt.branch
280
 
        file('hello', 'w').write('hello world')
281
 
        wt.add(['hello'], ['hello-id'])
282
 
        wt.commit(message='add hello')
283
 
        wt.remove('hello')
284
 
        wt.commit('removed hello', rev_id='rev2')
285
 
 
286
 
        tree = b.repository.revision_tree('rev2')
287
 
        self.assertFalse(tree.has_id('hello-id'))
288
 
 
289
 
    def test_committed_ancestry(self):
290
 
        """Test commit appends revisions to ancestry."""
291
 
        wt = self.make_branch_and_tree('.')
292
 
        b = wt.branch
293
 
        rev_ids = []
294
 
        for i in range(4):
295
 
            file('hello', 'w').write((str(i) * 4) + '\n')
296
 
            if i == 0:
297
 
                wt.add(['hello'], ['hello-id'])
298
 
            rev_id = 'test@rev-%d' % (i+1)
299
 
            rev_ids.append(rev_id)
300
 
            wt.commit(message='rev %d' % (i+1),
301
 
                     rev_id=rev_id)
302
 
        eq = self.assertEquals
303
 
        eq(b.revision_history(), rev_ids)
304
 
        for i in range(4):
305
 
            anc = b.repository.get_ancestry(rev_ids[i])
306
 
            eq(anc, [None] + rev_ids[:i+1])
307
 
 
308
 
    def test_commit_new_subdir_child_selective(self):
309
 
        wt = self.make_branch_and_tree('.')
310
 
        b = wt.branch
311
 
        self.build_tree(['dir/', 'dir/file1', 'dir/file2'])
312
 
        wt.add(['dir', 'dir/file1', 'dir/file2'],
313
 
              ['dirid', 'file1id', 'file2id'])
314
 
        wt.commit('dir/file1', specific_files=['dir/file1'], rev_id='1')
315
 
        inv = b.repository.get_inventory('1')
316
 
        self.assertEqual('1', inv['dirid'].revision)
317
 
        self.assertEqual('1', inv['file1id'].revision)
318
 
        # FIXME: This should raise a KeyError I think, rbc20051006
319
 
        self.assertRaises(BzrError, inv.__getitem__, 'file2id')
320
 
 
321
 
    def test_strict_commit(self):
322
 
        """Try and commit with unknown files and strict = True, should fail."""
323
 
        from bzrlib.errors import StrictCommitFailed
324
 
        wt = self.make_branch_and_tree('.')
325
 
        b = wt.branch
326
 
        file('hello', 'w').write('hello world')
327
 
        wt.add('hello')
328
 
        file('goodbye', 'w').write('goodbye cruel world!')
329
 
        self.assertRaises(StrictCommitFailed, wt.commit,
330
 
            message='add hello but not goodbye', strict=True)
331
 
 
332
 
    def test_strict_commit_without_unknowns(self):
333
 
        """Try and commit with no unknown files and strict = True,
334
 
        should work."""
335
 
        from bzrlib.errors import StrictCommitFailed
336
 
        wt = self.make_branch_and_tree('.')
337
 
        b = wt.branch
338
 
        file('hello', 'w').write('hello world')
339
 
        wt.add('hello')
340
 
        wt.commit(message='add hello', strict=True)
341
 
 
342
 
    def test_nonstrict_commit(self):
343
 
        """Try and commit with unknown files and strict = False, should work."""
344
 
        wt = self.make_branch_and_tree('.')
345
 
        b = wt.branch
346
 
        file('hello', 'w').write('hello world')
347
 
        wt.add('hello')
348
 
        file('goodbye', 'w').write('goodbye cruel world!')
349
 
        wt.commit(message='add hello but not goodbye', strict=False)
350
 
 
351
 
    def test_nonstrict_commit_without_unknowns(self):
352
 
        """Try and commit with no unknown files and strict = False,
353
 
        should work."""
354
 
        wt = self.make_branch_and_tree('.')
355
 
        b = wt.branch
356
 
        file('hello', 'w').write('hello world')
357
 
        wt.add('hello')
358
 
        wt.commit(message='add hello', strict=False)
359
 
 
360
 
    def test_signed_commit(self):
361
 
        import bzrlib.gpg
362
 
        import bzrlib.commit as commit
363
 
        oldstrategy = bzrlib.gpg.GPGStrategy
364
 
        wt = self.make_branch_and_tree('.')
365
 
        branch = wt.branch
366
 
        wt.commit("base", allow_pointless=True, rev_id='A')
367
 
        self.failIf(branch.repository.has_signature_for_revision_id('A'))
368
 
        try:
369
 
            from bzrlib.testament import Testament
370
 
            # monkey patch gpg signing mechanism
371
 
            bzrlib.gpg.GPGStrategy = bzrlib.gpg.LoopbackGPGStrategy
372
 
            commit.Commit(config=MustSignConfig(branch)).commit(message="base",
373
 
                                                      allow_pointless=True,
374
 
                                                      rev_id='B',
375
 
                                                      working_tree=wt)
376
 
            def sign(text):
377
 
                return bzrlib.gpg.LoopbackGPGStrategy(None).sign(text)
378
 
            self.assertEqual(sign(Testament.from_revision(branch.repository,
379
 
                             'B').as_short_text()),
380
 
                             branch.repository.get_signature_text('B'))
381
 
        finally:
382
 
            bzrlib.gpg.GPGStrategy = oldstrategy
383
 
 
384
 
    def test_commit_failed_signature(self):
385
 
        import bzrlib.gpg
386
 
        import bzrlib.commit as commit
387
 
        oldstrategy = bzrlib.gpg.GPGStrategy
388
 
        wt = self.make_branch_and_tree('.')
389
 
        branch = wt.branch
390
 
        wt.commit("base", allow_pointless=True, rev_id='A')
391
 
        self.failIf(branch.repository.has_signature_for_revision_id('A'))
392
 
        try:
393
 
            from bzrlib.testament import Testament
394
 
            # monkey patch gpg signing mechanism
395
 
            bzrlib.gpg.GPGStrategy = bzrlib.gpg.DisabledGPGStrategy
396
 
            config = MustSignConfig(branch)
397
 
            self.assertRaises(SigningFailed,
398
 
                              commit.Commit(config=config).commit,
399
 
                              message="base",
400
 
                              allow_pointless=True,
401
 
                              rev_id='B',
402
 
                              working_tree=wt)
403
 
            branch = Branch.open(self.get_url('.'))
404
 
            self.assertEqual(branch.revision_history(), ['A'])
405
 
            self.failIf(branch.repository.has_revision('B'))
406
 
        finally:
407
 
            bzrlib.gpg.GPGStrategy = oldstrategy
408
 
 
409
 
    def test_commit_invokes_hooks(self):
410
 
        import bzrlib.commit as commit
411
 
        wt = self.make_branch_and_tree('.')
412
 
        branch = wt.branch
413
 
        calls = []
414
 
        def called(branch, rev_id):
415
 
            calls.append('called')
416
 
        bzrlib.ahook = called
417
 
        try:
418
 
            config = BranchWithHooks(branch)
419
 
            commit.Commit(config=config).commit(
420
 
                            message = "base",
421
 
                            allow_pointless=True,
422
 
                            rev_id='A', working_tree = wt)
423
 
            self.assertEqual(['called', 'called'], calls)
424
 
        finally:
425
 
            del bzrlib.ahook
426
 
 
427
 
    def test_commit_object_doesnt_set_nick(self):
428
 
        # using the Commit object directly does not set the branch nick.
429
 
        wt = self.make_branch_and_tree('.')
430
 
        c = Commit()
431
 
        c.commit(working_tree=wt, message='empty tree', allow_pointless=True)
432
 
        self.assertEquals(wt.branch.revno(), 1)
433
 
        self.assertEqual({},
434
 
                         wt.branch.repository.get_revision(
435
 
                            wt.branch.last_revision()).properties)
436
 
 
437
 
    def test_safe_master_lock(self):
438
 
        os.mkdir('master')
439
 
        master = BzrDirMetaFormat1().initialize('master')
440
 
        master.create_repository()
441
 
        master_branch = master.create_branch()
442
 
        master.create_workingtree()
443
 
        bound = master.sprout('bound')
444
 
        wt = bound.open_workingtree()
445
 
        wt.branch.set_bound_location(os.path.realpath('master'))
446
 
 
447
 
        orig_default = lockdir._DEFAULT_TIMEOUT_SECONDS
448
 
        master_branch.lock_write()
449
 
        try:
450
 
            lockdir._DEFAULT_TIMEOUT_SECONDS = 1
451
 
            self.assertRaises(LockContention, wt.commit, 'silly')
452
 
        finally:
453
 
            lockdir._DEFAULT_TIMEOUT_SECONDS = orig_default
454
 
            master_branch.unlock()
455
 
 
456
 
    def test_commit_bound_merge(self):
457
 
        # see bug #43959; commit of a merge in a bound branch fails to push
458
 
        # the new commit into the master
459
 
        master_branch = self.make_branch('master')
460
 
        bound_tree = self.make_branch_and_tree('bound')
461
 
        bound_tree.branch.bind(master_branch)
462
 
 
463
 
        self.build_tree_contents([('bound/content_file', 'initial contents\n')])
464
 
        bound_tree.add(['content_file'])
465
 
        bound_tree.commit(message='woo!')
466
 
 
467
 
        other_bzrdir = master_branch.bzrdir.sprout('other')
468
 
        other_tree = other_bzrdir.open_workingtree()
469
 
 
470
 
        # do a commit to the the other branch changing the content file so
471
 
        # that our commit after merging will have a merged revision in the
472
 
        # content file history.
473
 
        self.build_tree_contents([('other/content_file', 'change in other\n')])
474
 
        other_tree.commit('change in other')
475
 
 
476
 
        # do a merge into the bound branch from other, and then change the
477
 
        # content file locally to force a new revision (rather than using the
478
 
        # revision from other). This forces extra processing in commit.
479
 
        bound_tree.merge_from_branch(other_tree.branch)
480
 
        self.build_tree_contents([('bound/content_file', 'change in bound\n')])
481
 
 
482
 
        # before #34959 was fixed, this failed with 'revision not present in
483
 
        # weave' when trying to implicitly push from the bound branch to the master
484
 
        bound_tree.commit(message='commit of merge in bound tree')
485
 
 
486
 
    def test_commit_reporting_after_merge(self):
487
 
        # when doing a commit of a merge, the reporter needs to still 
488
 
        # be called for each item that is added/removed/deleted.
489
 
        this_tree = self.make_branch_and_tree('this')
490
 
        # we need a bunch of files and dirs, to perform one action on each.
491
 
        self.build_tree([
492
 
            'this/dirtorename/',
493
 
            'this/dirtoreparent/',
494
 
            'this/dirtoleave/',
495
 
            'this/dirtoremove/',
496
 
            'this/filetoreparent',
497
 
            'this/filetorename',
498
 
            'this/filetomodify',
499
 
            'this/filetoremove',
500
 
            'this/filetoleave']
501
 
            )
502
 
        this_tree.add([
503
 
            'dirtorename',
504
 
            'dirtoreparent',
505
 
            'dirtoleave',
506
 
            'dirtoremove',
507
 
            'filetoreparent',
508
 
            'filetorename',
509
 
            'filetomodify',
510
 
            'filetoremove',
511
 
            'filetoleave']
512
 
            )
513
 
        this_tree.commit('create_files')
514
 
        other_dir = this_tree.bzrdir.sprout('other')
515
 
        other_tree = other_dir.open_workingtree()
516
 
        other_tree.lock_write()
517
 
        # perform the needed actions on the files and dirs.
518
 
        try:
519
 
            other_tree.rename_one('dirtorename', 'renameddir')
520
 
            other_tree.rename_one('dirtoreparent', 'renameddir/reparenteddir')
521
 
            other_tree.rename_one('filetorename', 'renamedfile')
522
 
            other_tree.rename_one('filetoreparent', 'renameddir/reparentedfile')
523
 
            other_tree.remove(['dirtoremove', 'filetoremove'])
524
 
            self.build_tree_contents([
525
 
                ('other/newdir/', ),
526
 
                ('other/filetomodify', 'new content'),
527
 
                ('other/newfile', 'new file content')])
528
 
            other_tree.add('newfile')
529
 
            other_tree.add('newdir/')
530
 
            other_tree.commit('modify all sample files and dirs.')
531
 
        finally:
532
 
            other_tree.unlock()
533
 
        this_tree.merge_from_branch(other_tree.branch)
534
 
        reporter = CapturingReporter()
535
 
        this_tree.commit('do the commit', reporter=reporter)
536
 
        self.assertEqual([
537
 
            ('change', 'unchanged', ''),
538
 
            ('change', 'unchanged', 'dirtoleave'),
539
 
            ('change', 'unchanged', 'filetoleave'),
540
 
            ('change', 'modified', 'filetomodify'),
541
 
            ('change', 'added', 'newdir'),
542
 
            ('change', 'added', 'newfile'),
543
 
            ('renamed', 'renamed', 'dirtorename', 'renameddir'),
544
 
            ('renamed', 'renamed', 'filetorename', 'renamedfile'),
545
 
            ('renamed', 'renamed', 'dirtoreparent', 'renameddir/reparenteddir'),
546
 
            ('renamed', 'renamed', 'filetoreparent', 'renameddir/reparentedfile'),
547
 
            ('deleted', 'dirtoremove'),
548
 
            ('deleted', 'filetoremove'),
549
 
            ],
550
 
            reporter.calls)
551
 
 
552
 
    def test_commit_removals_respects_filespec(self):
553
 
        """Commit respects the specified_files for removals."""
554
 
        tree = self.make_branch_and_tree('.')
555
 
        self.build_tree(['a', 'b'])
556
 
        tree.add(['a', 'b'])
557
 
        tree.commit('added a, b')
558
 
        tree.remove(['a', 'b'])
559
 
        tree.commit('removed a', specific_files='a')
560
 
        basis = tree.basis_tree()
561
 
        tree.lock_read()
562
 
        try:
563
 
            self.assertIs(None, basis.path2id('a'))
564
 
            self.assertFalse(basis.path2id('b') is None)
565
 
        finally:
566
 
            tree.unlock()
567
 
 
568
 
    def test_commit_saves_1ms_timestamp(self):
569
 
        """Passing in a timestamp is saved with 1ms resolution"""
570
 
        tree = self.make_branch_and_tree('.')
571
 
        self.build_tree(['a'])
572
 
        tree.add('a')
573
 
        tree.commit('added a', timestamp=1153248633.4186721, timezone=0,
574
 
                    rev_id='a1')
575
 
 
576
 
        rev = tree.branch.repository.get_revision('a1')
577
 
        self.assertEqual(1153248633.419, rev.timestamp)
578
 
 
579
 
    def test_commit_has_1ms_resolution(self):
580
 
        """Allowing commit to generate the timestamp also has 1ms resolution"""
581
 
        tree = self.make_branch_and_tree('.')
582
 
        self.build_tree(['a'])
583
 
        tree.add('a')
584
 
        tree.commit('added a', rev_id='a1')
585
 
 
586
 
        rev = tree.branch.repository.get_revision('a1')
587
 
        timestamp = rev.timestamp
588
 
        timestamp_1ms = round(timestamp, 3)
589
 
        self.assertEqual(timestamp_1ms, timestamp)
590
 
 
591
 
    def assertBasisTreeKind(self, kind, tree, file_id):
592
 
        basis = tree.basis_tree()
593
 
        basis.lock_read()
594
 
        try:
595
 
            self.assertEqual(kind, basis.kind(file_id))
596
 
        finally:
597
 
            basis.unlock()
598
 
 
599
 
    def test_commit_kind_changes(self):
600
 
        self.requireFeature(SymlinkFeature)
601
 
        tree = self.make_branch_and_tree('.')
602
 
        os.symlink('target', 'name')
603
 
        tree.add('name', 'a-file-id')
604
 
        tree.commit('Added a symlink')
605
 
        self.assertBasisTreeKind('symlink', tree, 'a-file-id')
606
 
 
607
 
        os.unlink('name')
608
 
        self.build_tree(['name'])
609
 
        tree.commit('Changed symlink to file')
610
 
        self.assertBasisTreeKind('file', tree, 'a-file-id')
611
 
 
612
 
        os.unlink('name')
613
 
        os.symlink('target', 'name')
614
 
        tree.commit('file to symlink')
615
 
        self.assertBasisTreeKind('symlink', tree, 'a-file-id')
616
 
 
617
 
        os.unlink('name')
618
 
        os.mkdir('name')
619
 
        tree.commit('symlink to directory')
620
 
        self.assertBasisTreeKind('directory', tree, 'a-file-id')
621
 
 
622
 
        os.rmdir('name')
623
 
        os.symlink('target', 'name')
624
 
        tree.commit('directory to symlink')
625
 
        self.assertBasisTreeKind('symlink', tree, 'a-file-id')
626
 
 
627
 
        # prepare for directory <-> file tests
628
 
        os.unlink('name')
629
 
        os.mkdir('name')
630
 
        tree.commit('symlink to directory')
631
 
        self.assertBasisTreeKind('directory', tree, 'a-file-id')
632
 
 
633
 
        os.rmdir('name')
634
 
        self.build_tree(['name'])
635
 
        tree.commit('Changed directory to file')
636
 
        self.assertBasisTreeKind('file', tree, 'a-file-id')
637
 
 
638
 
        os.unlink('name')
639
 
        os.mkdir('name')
640
 
        tree.commit('file to directory')
641
 
        self.assertBasisTreeKind('directory', tree, 'a-file-id')
642
 
 
643
 
    def test_commit_unversioned_specified(self):
644
 
        """Commit should raise if specified files isn't in basis or worktree"""
645
 
        tree = self.make_branch_and_tree('.')
646
 
        self.assertRaises(errors.PathsNotVersionedError, tree.commit, 
647
 
                          'message', specific_files=['bogus'])
648
 
 
649
 
    class Callback(object):
650
 
        
651
 
        def __init__(self, message, testcase):
652
 
            self.called = False
653
 
            self.message = message
654
 
            self.testcase = testcase
655
 
 
656
 
        def __call__(self, commit_obj):
657
 
            self.called = True
658
 
            self.testcase.assertTrue(isinstance(commit_obj, Commit))
659
 
            return self.message
660
 
 
661
 
    def test_commit_callback(self):
662
 
        """Commit should invoke a callback to get the message"""
663
 
 
664
 
        tree = self.make_branch_and_tree('.')
665
 
        try:
666
 
            tree.commit()
667
 
        except Exception, e:
668
 
            self.assertTrue(isinstance(e, BzrError))
669
 
            self.assertEqual('The message or message_callback keyword'
670
 
                             ' parameter is required for commit().', str(e))
671
 
        else:
672
 
            self.fail('exception not raised')
673
 
        cb = self.Callback(u'commit 1', self)
674
 
        tree.commit(message_callback=cb)
675
 
        self.assertTrue(cb.called)
676
 
        repository = tree.branch.repository
677
 
        message = repository.get_revision(tree.last_revision()).message
678
 
        self.assertEqual('commit 1', message)
679
 
 
680
 
    def test_no_callback_pointless(self):
681
 
        """Callback should not be invoked for pointless commit"""
682
 
        tree = self.make_branch_and_tree('.')
683
 
        cb = self.Callback(u'commit 2', self)
684
 
        self.assertRaises(PointlessCommit, tree.commit, message_callback=cb, 
685
 
                          allow_pointless=False)
686
 
        self.assertFalse(cb.called)
687
 
 
688
 
    def test_no_callback_netfailure(self):
689
 
        """Callback should not be invoked if connectivity fails"""
690
 
        tree = self.make_branch_and_tree('.')
691
 
        cb = self.Callback(u'commit 2', self)
692
 
        repository = tree.branch.repository
693
 
        # simulate network failure
694
 
        def raise_(self, arg, arg2):
695
 
            raise errors.NoSuchFile('foo')
696
 
        repository.add_inventory = raise_
697
 
        self.assertRaises(errors.NoSuchFile, tree.commit, message_callback=cb)
698
 
        self.assertFalse(cb.called)
699
 
 
700
 
    def test_selected_file_merge_commit(self):
701
 
        """Ensure the correct error is raised"""
702
 
        tree = self.make_branch_and_tree('foo')
703
 
        # pending merge would turn into a left parent
704
 
        tree.commit('commit 1')
705
 
        tree.add_parent_tree_id('example')
706
 
        self.build_tree(['foo/bar', 'foo/baz'])
707
 
        tree.add(['bar', 'baz'])
708
 
        err = self.assertRaises(errors.CannotCommitSelectedFileMerge,
709
 
            tree.commit, 'commit 2', specific_files=['bar', 'baz'])
710
 
        self.assertEqual(['bar', 'baz'], err.files)
711
 
        self.assertEqual('Selected-file commit of merges is not supported'
712
 
                         ' yet: files bar, baz', str(err))
713
 
 
714
 
    def test_commit_ordering(self):
715
 
        """Test of corner-case commit ordering error"""
716
 
        tree = self.make_branch_and_tree('.')
717
 
        self.build_tree(['a/', 'a/z/', 'a/c/', 'a/z/x', 'a/z/y'])
718
 
        tree.add(['a/', 'a/z/', 'a/c/', 'a/z/x', 'a/z/y'])
719
 
        tree.commit('setup')
720
 
        self.build_tree(['a/c/d/'])
721
 
        tree.add('a/c/d')
722
 
        tree.rename_one('a/z/x', 'a/c/d/x')
723
 
        tree.commit('test', specific_files=['a/z/y'])
724
 
 
725
 
    def test_commit_no_author(self):
726
 
        """The default kwarg author in MutableTree.commit should not add
727
 
        the 'author' revision property.
728
 
        """
729
 
        tree = self.make_branch_and_tree('foo')
730
 
        rev_id = tree.commit('commit 1')
731
 
        rev = tree.branch.repository.get_revision(rev_id)
732
 
        self.assertFalse('author' in rev.properties)
733
 
 
734
 
    def test_commit_author(self):
735
 
        """Passing a non-empty author kwarg to MutableTree.commit should add
736
 
        the 'author' revision property.
737
 
        """
738
 
        tree = self.make_branch_and_tree('foo')
739
 
        rev_id = tree.commit('commit 1', author='John Doe <jdoe@example.com>')
740
 
        rev = tree.branch.repository.get_revision(rev_id)
741
 
        self.assertEqual('John Doe <jdoe@example.com>',
742
 
                         rev.properties['author'])