~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_commit.py

  • Committer: Wouter van Heyst
  • Date: 2007-01-18 18:37:21 UTC
  • mto: (2234.3.3 0.14)
  • mto: This revision was merged to the branch mainline in revision 2243.
  • Revision ID: larstiq@larstiq.dyndns.org-20070118183721-78uzxifyyyoqxja9
(Alexander Belchenko) add windows installer check for python2.5

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)