/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

  • Committer: Aaron Bentley
  • Date: 2007-03-02 20:27:06 UTC
  • mto: (2323.6.9 0.15-integration)
  • mto: This revision was merged to the branch mainline in revision 2330.
  • Revision ID: abentley@panoramicfeedback.com-20070302202706-mriia2sdekme12af
Add RegistryOption.from_swargs to simplify simple registry options

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