~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_commit.py

  • Committer: Martin Pool
  • Date: 2005-06-27 08:18:07 UTC
  • mto: This revision was merged to the branch mainline in revision 852.
  • Revision ID: mbp@sourcefrog.net-20050627081807-dc3ff5726c88b247
More tests for insertion of lines in new versions.

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
 
    )
25
 
from bzrlib.branch import Branch
26
 
from bzrlib.bzrdir import BzrDir, BzrDirMetaFormat1
27
 
from bzrlib.commit import Commit, NullCommitReporter
28
 
from bzrlib.config import BranchConfig
29
 
from bzrlib.errors import (PointlessCommit, BzrError, SigningFailed, 
30
 
                           LockContention)
31
 
from bzrlib.tests import TestCaseWithTransport
32
 
from bzrlib.workingtree import WorkingTree
33
 
 
34
 
 
35
 
# TODO: Test commit with some added, and added-but-missing files
36
 
 
37
 
class MustSignConfig(BranchConfig):
38
 
 
39
 
    def signature_needed(self):
40
 
        return True
41
 
 
42
 
    def gpg_signing_command(self):
43
 
        return ['cat', '-']
44
 
 
45
 
 
46
 
class BranchWithHooks(BranchConfig):
47
 
 
48
 
    def post_commit(self):
49
 
        return "bzrlib.ahook bzrlib.ahook"
50
 
 
51
 
 
52
 
class CapturingReporter(NullCommitReporter):
53
 
    """This reporter captures the calls made to it for evaluation later."""
54
 
 
55
 
    def __init__(self):
56
 
        # a list of the calls this received
57
 
        self.calls = []
58
 
 
59
 
    def snapshot_change(self, change, path):
60
 
        self.calls.append(('change', change, path))
61
 
 
62
 
    def deleted(self, file_id):
63
 
        self.calls.append(('deleted', file_id))
64
 
 
65
 
    def missing(self, path):
66
 
        self.calls.append(('missing', path))
67
 
 
68
 
    def renamed(self, change, old_path, new_path):
69
 
        self.calls.append(('renamed', change, old_path, new_path))
70
 
 
71
 
 
72
 
class TestCommit(TestCaseWithTransport):
73
 
 
74
 
    def test_simple_commit(self):
75
 
        """Commit and check two versions of a single file."""
76
 
        wt = self.make_branch_and_tree('.')
77
 
        b = wt.branch
78
 
        file('hello', 'w').write('hello world')
79
 
        wt.add('hello')
80
 
        wt.commit(message='add hello')
81
 
        file_id = wt.path2id('hello')
82
 
 
83
 
        file('hello', 'w').write('version 2')
84
 
        wt.commit(message='commit 2')
85
 
 
86
 
        eq = self.assertEquals
87
 
        eq(b.revno(), 2)
88
 
        rh = b.revision_history()
89
 
        rev = b.repository.get_revision(rh[0])
90
 
        eq(rev.message, 'add hello')
91
 
 
92
 
        tree1 = b.repository.revision_tree(rh[0])
93
 
        text = tree1.get_file_text(file_id)
94
 
        eq(text, 'hello world')
95
 
 
96
 
        tree2 = b.repository.revision_tree(rh[1])
97
 
        eq(tree2.get_file_text(file_id), 'version 2')
98
 
 
99
 
    def test_delete_commit(self):
100
 
        """Test a commit with a deleted file"""
101
 
        wt = self.make_branch_and_tree('.')
102
 
        b = wt.branch
103
 
        file('hello', 'w').write('hello world')
104
 
        wt.add(['hello'], ['hello-id'])
105
 
        wt.commit(message='add hello')
106
 
 
107
 
        os.remove('hello')
108
 
        wt.commit('removed hello', rev_id='rev2')
109
 
 
110
 
        tree = b.repository.revision_tree('rev2')
111
 
        self.assertFalse(tree.has_id('hello-id'))
112
 
 
113
 
    def test_pointless_commit(self):
114
 
        """Commit refuses unless there are changes or it's forced."""
115
 
        wt = self.make_branch_and_tree('.')
116
 
        b = wt.branch
117
 
        file('hello', 'w').write('hello')
118
 
        wt.add(['hello'])
119
 
        wt.commit(message='add hello')
120
 
        self.assertEquals(b.revno(), 1)
121
 
        self.assertRaises(PointlessCommit,
122
 
                          wt.commit,
123
 
                          message='fails',
124
 
                          allow_pointless=False)
125
 
        self.assertEquals(b.revno(), 1)
126
 
        
127
 
    def test_commit_empty(self):
128
 
        """Commiting an empty tree works."""
129
 
        wt = self.make_branch_and_tree('.')
130
 
        b = wt.branch
131
 
        wt.commit(message='empty tree', allow_pointless=True)
132
 
        self.assertRaises(PointlessCommit,
133
 
                          wt.commit,
134
 
                          message='empty tree',
135
 
                          allow_pointless=False)
136
 
        wt.commit(message='empty tree', allow_pointless=True)
137
 
        self.assertEquals(b.revno(), 2)
138
 
 
139
 
    def test_selective_delete(self):
140
 
        """Selective commit in tree with deletions"""
141
 
        wt = self.make_branch_and_tree('.')
142
 
        b = wt.branch
143
 
        file('hello', 'w').write('hello')
144
 
        file('buongia', 'w').write('buongia')
145
 
        wt.add(['hello', 'buongia'],
146
 
              ['hello-id', 'buongia-id'])
147
 
        wt.commit(message='add files',
148
 
                 rev_id='test@rev-1')
149
 
        
150
 
        os.remove('hello')
151
 
        file('buongia', 'w').write('new text')
152
 
        wt.commit(message='update text',
153
 
                 specific_files=['buongia'],
154
 
                 allow_pointless=False,
155
 
                 rev_id='test@rev-2')
156
 
 
157
 
        wt.commit(message='remove hello',
158
 
                 specific_files=['hello'],
159
 
                 allow_pointless=False,
160
 
                 rev_id='test@rev-3')
161
 
 
162
 
        eq = self.assertEquals
163
 
        eq(b.revno(), 3)
164
 
 
165
 
        tree2 = b.repository.revision_tree('test@rev-2')
166
 
        self.assertTrue(tree2.has_filename('hello'))
167
 
        self.assertEquals(tree2.get_file_text('hello-id'), 'hello')
168
 
        self.assertEquals(tree2.get_file_text('buongia-id'), 'new text')
169
 
        
170
 
        tree3 = b.repository.revision_tree('test@rev-3')
171
 
        self.assertFalse(tree3.has_filename('hello'))
172
 
        self.assertEquals(tree3.get_file_text('buongia-id'), 'new text')
173
 
 
174
 
    def test_commit_rename(self):
175
 
        """Test commit of a revision where a file is renamed."""
176
 
        tree = self.make_branch_and_tree('.')
177
 
        b = tree.branch
178
 
        self.build_tree(['hello'], line_endings='binary')
179
 
        tree.add(['hello'], ['hello-id'])
180
 
        tree.commit(message='one', rev_id='test@rev-1', allow_pointless=False)
181
 
 
182
 
        tree.rename_one('hello', 'fruity')
183
 
        tree.commit(message='renamed', rev_id='test@rev-2', allow_pointless=False)
184
 
 
185
 
        eq = self.assertEquals
186
 
        tree1 = b.repository.revision_tree('test@rev-1')
187
 
        eq(tree1.id2path('hello-id'), 'hello')
188
 
        eq(tree1.get_file_text('hello-id'), 'contents of hello\n')
189
 
        self.assertFalse(tree1.has_filename('fruity'))
190
 
        self.check_inventory_shape(tree1.inventory, ['hello'])
191
 
        ie = tree1.inventory['hello-id']
192
 
        eq(ie.revision, 'test@rev-1')
193
 
 
194
 
        tree2 = b.repository.revision_tree('test@rev-2')
195
 
        eq(tree2.id2path('hello-id'), 'fruity')
196
 
        eq(tree2.get_file_text('hello-id'), 'contents of hello\n')
197
 
        self.check_inventory_shape(tree2.inventory, ['fruity'])
198
 
        ie = tree2.inventory['hello-id']
199
 
        eq(ie.revision, 'test@rev-2')
200
 
 
201
 
    def test_reused_rev_id(self):
202
 
        """Test that a revision id cannot be reused in a branch"""
203
 
        wt = self.make_branch_and_tree('.')
204
 
        b = wt.branch
205
 
        wt.commit('initial', rev_id='test@rev-1', allow_pointless=True)
206
 
        self.assertRaises(Exception,
207
 
                          wt.commit,
208
 
                          message='reused id',
209
 
                          rev_id='test@rev-1',
210
 
                          allow_pointless=True)
211
 
 
212
 
    def test_commit_move(self):
213
 
        """Test commit of revisions with moved files and directories"""
214
 
        eq = self.assertEquals
215
 
        wt = self.make_branch_and_tree('.')
216
 
        b = wt.branch
217
 
        r1 = 'test@rev-1'
218
 
        self.build_tree(['hello', 'a/', 'b/'])
219
 
        wt.add(['hello', 'a', 'b'], ['hello-id', 'a-id', 'b-id'])
220
 
        wt.commit('initial', rev_id=r1, allow_pointless=False)
221
 
        wt.move(['hello'], 'a')
222
 
        r2 = 'test@rev-2'
223
 
        wt.commit('two', rev_id=r2, allow_pointless=False)
224
 
        self.check_inventory_shape(wt.read_working_inventory(),
225
 
                                   ['a', 'a/hello', 'b'])
226
 
 
227
 
        wt.move(['b'], 'a')
228
 
        r3 = 'test@rev-3'
229
 
        wt.commit('three', rev_id=r3, allow_pointless=False)
230
 
        self.check_inventory_shape(wt.read_working_inventory(),
231
 
                                   ['a', 'a/hello', 'a/b'])
232
 
        self.check_inventory_shape(b.repository.get_revision_inventory(r3),
233
 
                                   ['a', 'a/hello', 'a/b'])
234
 
 
235
 
        wt.move(['a/hello'], 'a/b')
236
 
        r4 = 'test@rev-4'
237
 
        wt.commit('four', rev_id=r4, allow_pointless=False)
238
 
        self.check_inventory_shape(wt.read_working_inventory(),
239
 
                                   ['a', 'a/b/hello', 'a/b'])
240
 
 
241
 
        inv = b.repository.get_revision_inventory(r4)
242
 
        eq(inv['hello-id'].revision, r4)
243
 
        eq(inv['a-id'].revision, r1)
244
 
        eq(inv['b-id'].revision, r3)
245
 
        
246
 
    def test_removed_commit(self):
247
 
        """Commit with a removed file"""
248
 
        wt = self.make_branch_and_tree('.')
249
 
        b = wt.branch
250
 
        file('hello', 'w').write('hello world')
251
 
        wt.add(['hello'], ['hello-id'])
252
 
        wt.commit(message='add hello')
253
 
        wt.remove('hello')
254
 
        wt.commit('removed hello', rev_id='rev2')
255
 
 
256
 
        tree = b.repository.revision_tree('rev2')
257
 
        self.assertFalse(tree.has_id('hello-id'))
258
 
 
259
 
    def test_committed_ancestry(self):
260
 
        """Test commit appends revisions to ancestry."""
261
 
        wt = self.make_branch_and_tree('.')
262
 
        b = wt.branch
263
 
        rev_ids = []
264
 
        for i in range(4):
265
 
            file('hello', 'w').write((str(i) * 4) + '\n')
266
 
            if i == 0:
267
 
                wt.add(['hello'], ['hello-id'])
268
 
            rev_id = 'test@rev-%d' % (i+1)
269
 
            rev_ids.append(rev_id)
270
 
            wt.commit(message='rev %d' % (i+1),
271
 
                     rev_id=rev_id)
272
 
        eq = self.assertEquals
273
 
        eq(b.revision_history(), rev_ids)
274
 
        for i in range(4):
275
 
            anc = b.repository.get_ancestry(rev_ids[i])
276
 
            eq(anc, [None] + rev_ids[:i+1])
277
 
 
278
 
    def test_commit_new_subdir_child_selective(self):
279
 
        wt = self.make_branch_and_tree('.')
280
 
        b = wt.branch
281
 
        self.build_tree(['dir/', 'dir/file1', 'dir/file2'])
282
 
        wt.add(['dir', 'dir/file1', 'dir/file2'],
283
 
              ['dirid', 'file1id', 'file2id'])
284
 
        wt.commit('dir/file1', specific_files=['dir/file1'], rev_id='1')
285
 
        inv = b.repository.get_inventory('1')
286
 
        self.assertEqual('1', inv['dirid'].revision)
287
 
        self.assertEqual('1', inv['file1id'].revision)
288
 
        # FIXME: This should raise a KeyError I think, rbc20051006
289
 
        self.assertRaises(BzrError, inv.__getitem__, 'file2id')
290
 
 
291
 
    def test_strict_commit(self):
292
 
        """Try and commit with unknown files and strict = True, should fail."""
293
 
        from bzrlib.errors import StrictCommitFailed
294
 
        wt = self.make_branch_and_tree('.')
295
 
        b = wt.branch
296
 
        file('hello', 'w').write('hello world')
297
 
        wt.add('hello')
298
 
        file('goodbye', 'w').write('goodbye cruel world!')
299
 
        self.assertRaises(StrictCommitFailed, wt.commit,
300
 
            message='add hello but not goodbye', strict=True)
301
 
 
302
 
    def test_strict_commit_without_unknowns(self):
303
 
        """Try and commit with no unknown files and strict = True,
304
 
        should work."""
305
 
        from bzrlib.errors import StrictCommitFailed
306
 
        wt = self.make_branch_and_tree('.')
307
 
        b = wt.branch
308
 
        file('hello', 'w').write('hello world')
309
 
        wt.add('hello')
310
 
        wt.commit(message='add hello', strict=True)
311
 
 
312
 
    def test_nonstrict_commit(self):
313
 
        """Try and commit with unknown files and strict = False, should work."""
314
 
        wt = self.make_branch_and_tree('.')
315
 
        b = wt.branch
316
 
        file('hello', 'w').write('hello world')
317
 
        wt.add('hello')
318
 
        file('goodbye', 'w').write('goodbye cruel world!')
319
 
        wt.commit(message='add hello but not goodbye', strict=False)
320
 
 
321
 
    def test_nonstrict_commit_without_unknowns(self):
322
 
        """Try and commit with no unknown files and strict = False,
323
 
        should work."""
324
 
        wt = self.make_branch_and_tree('.')
325
 
        b = wt.branch
326
 
        file('hello', 'w').write('hello world')
327
 
        wt.add('hello')
328
 
        wt.commit(message='add hello', strict=False)
329
 
 
330
 
    def test_signed_commit(self):
331
 
        import bzrlib.gpg
332
 
        import bzrlib.commit as commit
333
 
        oldstrategy = bzrlib.gpg.GPGStrategy
334
 
        wt = self.make_branch_and_tree('.')
335
 
        branch = wt.branch
336
 
        wt.commit("base", allow_pointless=True, rev_id='A')
337
 
        self.failIf(branch.repository.has_signature_for_revision_id('A'))
338
 
        try:
339
 
            from bzrlib.testament import Testament
340
 
            # monkey patch gpg signing mechanism
341
 
            bzrlib.gpg.GPGStrategy = bzrlib.gpg.LoopbackGPGStrategy
342
 
            commit.Commit(config=MustSignConfig(branch)).commit(message="base",
343
 
                                                      allow_pointless=True,
344
 
                                                      rev_id='B',
345
 
                                                      working_tree=wt)
346
 
            self.assertEqual(Testament.from_revision(branch.repository,
347
 
                             'B').as_short_text(),
348
 
                             branch.repository.get_signature_text('B'))
349
 
        finally:
350
 
            bzrlib.gpg.GPGStrategy = oldstrategy
351
 
 
352
 
    def test_commit_failed_signature(self):
353
 
        import bzrlib.gpg
354
 
        import bzrlib.commit as commit
355
 
        oldstrategy = bzrlib.gpg.GPGStrategy
356
 
        wt = self.make_branch_and_tree('.')
357
 
        branch = wt.branch
358
 
        wt.commit("base", allow_pointless=True, rev_id='A')
359
 
        self.failIf(branch.repository.has_signature_for_revision_id('A'))
360
 
        try:
361
 
            from bzrlib.testament import Testament
362
 
            # monkey patch gpg signing mechanism
363
 
            bzrlib.gpg.GPGStrategy = bzrlib.gpg.DisabledGPGStrategy
364
 
            config = MustSignConfig(branch)
365
 
            self.assertRaises(SigningFailed,
366
 
                              commit.Commit(config=config).commit,
367
 
                              message="base",
368
 
                              allow_pointless=True,
369
 
                              rev_id='B',
370
 
                              working_tree=wt)
371
 
            branch = Branch.open(self.get_url('.'))
372
 
            self.assertEqual(branch.revision_history(), ['A'])
373
 
            self.failIf(branch.repository.has_revision('B'))
374
 
        finally:
375
 
            bzrlib.gpg.GPGStrategy = oldstrategy
376
 
 
377
 
    def test_commit_invokes_hooks(self):
378
 
        import bzrlib.commit as commit
379
 
        wt = self.make_branch_and_tree('.')
380
 
        branch = wt.branch
381
 
        calls = []
382
 
        def called(branch, rev_id):
383
 
            calls.append('called')
384
 
        bzrlib.ahook = called
385
 
        try:
386
 
            config = BranchWithHooks(branch)
387
 
            commit.Commit(config=config).commit(
388
 
                            message = "base",
389
 
                            allow_pointless=True,
390
 
                            rev_id='A', working_tree = wt)
391
 
            self.assertEqual(['called', 'called'], calls)
392
 
        finally:
393
 
            del bzrlib.ahook
394
 
 
395
 
    def test_commit_object_doesnt_set_nick(self):
396
 
        # using the Commit object directly does not set the branch nick.
397
 
        wt = self.make_branch_and_tree('.')
398
 
        c = Commit()
399
 
        c.commit(working_tree=wt, message='empty tree', allow_pointless=True)
400
 
        self.assertEquals(wt.branch.revno(), 1)
401
 
        self.assertEqual({},
402
 
                         wt.branch.repository.get_revision(
403
 
                            wt.branch.last_revision()).properties)
404
 
 
405
 
    def test_safe_master_lock(self):
406
 
        os.mkdir('master')
407
 
        master = BzrDirMetaFormat1().initialize('master')
408
 
        master.create_repository()
409
 
        master_branch = master.create_branch()
410
 
        master.create_workingtree()
411
 
        bound = master.sprout('bound')
412
 
        wt = bound.open_workingtree()
413
 
        wt.branch.set_bound_location(os.path.realpath('master'))
414
 
 
415
 
        orig_default = lockdir._DEFAULT_TIMEOUT_SECONDS
416
 
        master_branch.lock_write()
417
 
        try:
418
 
            lockdir._DEFAULT_TIMEOUT_SECONDS = 1
419
 
            self.assertRaises(LockContention, wt.commit, 'silly')
420
 
        finally:
421
 
            lockdir._DEFAULT_TIMEOUT_SECONDS = orig_default
422
 
            master_branch.unlock()
423
 
 
424
 
    def test_commit_bound_merge(self):
425
 
        # see bug #43959; commit of a merge in a bound branch fails to push
426
 
        # the new commit into the master
427
 
        master_branch = self.make_branch('master')
428
 
        bound_tree = self.make_branch_and_tree('bound')
429
 
        bound_tree.branch.bind(master_branch)
430
 
 
431
 
        self.build_tree_contents([('bound/content_file', 'initial contents\n')])
432
 
        bound_tree.add(['content_file'])
433
 
        bound_tree.commit(message='woo!')
434
 
 
435
 
        other_bzrdir = master_branch.bzrdir.sprout('other')
436
 
        other_tree = other_bzrdir.open_workingtree()
437
 
 
438
 
        # do a commit to the the other branch changing the content file so
439
 
        # that our commit after merging will have a merged revision in the
440
 
        # content file history.
441
 
        self.build_tree_contents([('other/content_file', 'change in other\n')])
442
 
        other_tree.commit('change in other')
443
 
 
444
 
        # do a merge into the bound branch from other, and then change the
445
 
        # content file locally to force a new revision (rather than using the
446
 
        # revision from other). This forces extra processing in commit.
447
 
        bound_tree.merge_from_branch(other_tree.branch)
448
 
        self.build_tree_contents([('bound/content_file', 'change in bound\n')])
449
 
 
450
 
        # before #34959 was fixed, this failed with 'revision not present in
451
 
        # weave' when trying to implicitly push from the bound branch to the master
452
 
        bound_tree.commit(message='commit of merge in bound tree')
453
 
 
454
 
    def test_commit_reporting_after_merge(self):
455
 
        # when doing a commit of a merge, the reporter needs to still 
456
 
        # be called for each item that is added/removed/deleted.
457
 
        this_tree = self.make_branch_and_tree('this')
458
 
        # we need a bunch of files and dirs, to perform one action on each.
459
 
        self.build_tree([
460
 
            'this/dirtorename/',
461
 
            'this/dirtoreparent/',
462
 
            'this/dirtoleave/',
463
 
            'this/dirtoremove/',
464
 
            'this/filetoreparent',
465
 
            'this/filetorename',
466
 
            'this/filetomodify',
467
 
            'this/filetoremove',
468
 
            'this/filetoleave']
469
 
            )
470
 
        this_tree.add([
471
 
            'dirtorename',
472
 
            'dirtoreparent',
473
 
            'dirtoleave',
474
 
            'dirtoremove',
475
 
            'filetoreparent',
476
 
            'filetorename',
477
 
            'filetomodify',
478
 
            'filetoremove',
479
 
            'filetoleave']
480
 
            )
481
 
        this_tree.commit('create_files')
482
 
        other_dir = this_tree.bzrdir.sprout('other')
483
 
        other_tree = other_dir.open_workingtree()
484
 
        other_tree.lock_write()
485
 
        # perform the needed actions on the files and dirs.
486
 
        try:
487
 
            other_tree.rename_one('dirtorename', 'renameddir')
488
 
            other_tree.rename_one('dirtoreparent', 'renameddir/reparenteddir')
489
 
            other_tree.rename_one('filetorename', 'renamedfile')
490
 
            other_tree.rename_one('filetoreparent', 'renameddir/reparentedfile')
491
 
            other_tree.remove(['dirtoremove', 'filetoremove'])
492
 
            self.build_tree_contents([
493
 
                ('other/newdir/', ),
494
 
                ('other/filetomodify', 'new content'),
495
 
                ('other/newfile', 'new file content')])
496
 
            other_tree.add('newfile')
497
 
            other_tree.add('newdir/')
498
 
            other_tree.commit('modify all sample files and dirs.')
499
 
        finally:
500
 
            other_tree.unlock()
501
 
        this_tree.merge_from_branch(other_tree.branch)
502
 
        reporter = CapturingReporter()
503
 
        this_tree.commit('do the commit', reporter=reporter)
504
 
        self.assertEqual([
505
 
            ('change', 'unchanged', ''),
506
 
            ('change', 'unchanged', 'dirtoleave'),
507
 
            ('change', 'unchanged', 'filetoleave'),
508
 
            ('change', 'modified', 'filetomodify'),
509
 
            ('change', 'added', 'newdir'),
510
 
            ('change', 'added', 'newfile'),
511
 
            ('renamed', 'renamed', 'dirtorename', 'renameddir'),
512
 
            ('renamed', 'renamed', 'dirtoreparent', 'renameddir/reparenteddir'),
513
 
            ('renamed', 'renamed', 'filetoreparent', 'renameddir/reparentedfile'),
514
 
            ('renamed', 'renamed', 'filetorename', 'renamedfile'),
515
 
            ('deleted', 'dirtoremove'),
516
 
            ('deleted', 'filetoremove'),
517
 
            ],
518
 
            reporter.calls)
519
 
 
520
 
    def test_commit_removals_respects_filespec(self):
521
 
        """Commit respects the specified_files for removals."""
522
 
        tree = self.make_branch_and_tree('.')
523
 
        self.build_tree(['a', 'b'])
524
 
        tree.add(['a', 'b'])
525
 
        tree.commit('added a, b')
526
 
        tree.remove(['a', 'b'])
527
 
        tree.commit('removed a', specific_files='a')
528
 
        basis = tree.basis_tree().inventory
529
 
        self.assertIs(None, basis.path2id('a'))
530
 
        self.assertFalse(basis.path2id('b') is None)
531
 
 
532
 
    def test_commit_saves_1ms_timestamp(self):
533
 
        """Passing in a timestamp is saved with 1ms resolution"""
534
 
        tree = self.make_branch_and_tree('.')
535
 
        self.build_tree(['a'])
536
 
        tree.add('a')
537
 
        tree.commit('added a', timestamp=1153248633.4186721, timezone=0,
538
 
                    rev_id='a1')
539
 
 
540
 
        rev = tree.branch.repository.get_revision('a1')
541
 
        self.assertEqual(1153248633.419, rev.timestamp)
542
 
 
543
 
    def test_commit_has_1ms_resolution(self):
544
 
        """Allowing commit to generate the timestamp also has 1ms resolution"""
545
 
        tree = self.make_branch_and_tree('.')
546
 
        self.build_tree(['a'])
547
 
        tree.add('a')
548
 
        tree.commit('added a', rev_id='a1')
549
 
 
550
 
        rev = tree.branch.repository.get_revision('a1')
551
 
        timestamp = rev.timestamp
552
 
        timestamp_1ms = round(timestamp, 3)
553
 
        self.assertEqual(timestamp_1ms, timestamp)
554
 
 
555
 
    def test_commit_unversioned_specified(self):
556
 
        """Commit should raise if specified files isn't in basis or worktree"""
557
 
        tree = self.make_branch_and_tree('.')
558
 
        self.assertRaises(errors.PathsNotVersionedError, tree.commit, 
559
 
                          'message', specific_files=['bogus'])
560
 
 
561
 
    class Callback(object):
562
 
        
563
 
        def __init__(self, message, testcase):
564
 
            self.called = False
565
 
            self.message = message
566
 
            self.testcase = testcase
567
 
 
568
 
        def __call__(self, commit_obj):
569
 
            self.called = True
570
 
            self.testcase.assertTrue(isinstance(commit_obj, Commit))
571
 
            return self.message
572
 
 
573
 
    def test_commit_callback(self):
574
 
        """Commit should invoke a callback to get the message"""
575
 
 
576
 
        tree = self.make_branch_and_tree('.')
577
 
        try:
578
 
            tree.commit()
579
 
        except Exception, e:
580
 
            self.assertTrue(isinstance(e, BzrError))
581
 
            self.assertEqual('The message or message_callback keyword'
582
 
                             ' parameter is required for commit().', str(e))
583
 
        else:
584
 
            self.fail('exception not raised')
585
 
        cb = self.Callback(u'commit 1', self)
586
 
        tree.commit(message_callback=cb)
587
 
        self.assertTrue(cb.called)
588
 
        repository = tree.branch.repository
589
 
        message = repository.get_revision(tree.last_revision()).message
590
 
        self.assertEqual('commit 1', message)
591
 
 
592
 
    def test_no_callback_pointless(self):
593
 
        """Callback should not be invoked for pointless commit"""
594
 
        tree = self.make_branch_and_tree('.')
595
 
        cb = self.Callback(u'commit 2', self)
596
 
        self.assertRaises(PointlessCommit, tree.commit, message_callback=cb, 
597
 
                          allow_pointless=False)
598
 
        self.assertFalse(cb.called)
599
 
 
600
 
    def test_no_callback_netfailure(self):
601
 
        """Callback should not be invoked if connectivity fails"""
602
 
        tree = self.make_branch_and_tree('.')
603
 
        cb = self.Callback(u'commit 2', self)
604
 
        repository = tree.branch.repository
605
 
        # simulate network failure
606
 
        def raise_(self, arg, arg2):
607
 
            raise errors.NoSuchFile('foo')
608
 
        repository.add_inventory = raise_
609
 
        self.assertRaises(errors.NoSuchFile, tree.commit, message_callback=cb)
610
 
        self.assertFalse(cb.called)