~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-05-27 01:50:28 UTC
  • Revision ID: mbp@sourcefrog.net-20050527015028-83638384380101a8
- still use internal diff by default

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
 
        wt.lock_read()
227
 
        try:
228
 
            self.check_inventory_shape(wt.read_working_inventory(),
229
 
                                       ['a', 'a/hello', 'b'])
230
 
        finally:
231
 
            wt.unlock()
232
 
 
233
 
        wt.move(['b'], 'a')
234
 
        r3 = 'test@rev-3'
235
 
        wt.commit('three', rev_id=r3, allow_pointless=False)
236
 
        wt.lock_read()
237
 
        try:
238
 
            self.check_inventory_shape(wt.read_working_inventory(),
239
 
                                       ['a', 'a/hello', 'a/b'])
240
 
            self.check_inventory_shape(b.repository.get_revision_inventory(r3),
241
 
                                       ['a', 'a/hello', 'a/b'])
242
 
        finally:
243
 
            wt.unlock()
244
 
 
245
 
        wt.move(['a/hello'], 'a/b')
246
 
        r4 = 'test@rev-4'
247
 
        wt.commit('four', rev_id=r4, allow_pointless=False)
248
 
        wt.lock_read()
249
 
        try:
250
 
            self.check_inventory_shape(wt.read_working_inventory(),
251
 
                                       ['a', 'a/b/hello', 'a/b'])
252
 
        finally:
253
 
            wt.unlock()
254
 
 
255
 
        inv = b.repository.get_revision_inventory(r4)
256
 
        eq(inv['hello-id'].revision, r4)
257
 
        eq(inv['a-id'].revision, r1)
258
 
        eq(inv['b-id'].revision, r3)
259
 
 
260
 
    def test_removed_commit(self):
261
 
        """Commit with a removed file"""
262
 
        wt = self.make_branch_and_tree('.')
263
 
        b = wt.branch
264
 
        file('hello', 'w').write('hello world')
265
 
        wt.add(['hello'], ['hello-id'])
266
 
        wt.commit(message='add hello')
267
 
        wt.remove('hello')
268
 
        wt.commit('removed hello', rev_id='rev2')
269
 
 
270
 
        tree = b.repository.revision_tree('rev2')
271
 
        self.assertFalse(tree.has_id('hello-id'))
272
 
 
273
 
    def test_committed_ancestry(self):
274
 
        """Test commit appends revisions to ancestry."""
275
 
        wt = self.make_branch_and_tree('.')
276
 
        b = wt.branch
277
 
        rev_ids = []
278
 
        for i in range(4):
279
 
            file('hello', 'w').write((str(i) * 4) + '\n')
280
 
            if i == 0:
281
 
                wt.add(['hello'], ['hello-id'])
282
 
            rev_id = 'test@rev-%d' % (i+1)
283
 
            rev_ids.append(rev_id)
284
 
            wt.commit(message='rev %d' % (i+1),
285
 
                     rev_id=rev_id)
286
 
        eq = self.assertEquals
287
 
        eq(b.revision_history(), rev_ids)
288
 
        for i in range(4):
289
 
            anc = b.repository.get_ancestry(rev_ids[i])
290
 
            eq(anc, [None] + rev_ids[:i+1])
291
 
 
292
 
    def test_commit_new_subdir_child_selective(self):
293
 
        wt = self.make_branch_and_tree('.')
294
 
        b = wt.branch
295
 
        self.build_tree(['dir/', 'dir/file1', 'dir/file2'])
296
 
        wt.add(['dir', 'dir/file1', 'dir/file2'],
297
 
              ['dirid', 'file1id', 'file2id'])
298
 
        wt.commit('dir/file1', specific_files=['dir/file1'], rev_id='1')
299
 
        inv = b.repository.get_inventory('1')
300
 
        self.assertEqual('1', inv['dirid'].revision)
301
 
        self.assertEqual('1', inv['file1id'].revision)
302
 
        # FIXME: This should raise a KeyError I think, rbc20051006
303
 
        self.assertRaises(BzrError, inv.__getitem__, 'file2id')
304
 
 
305
 
    def test_strict_commit(self):
306
 
        """Try and commit with unknown files and strict = True, should fail."""
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
 
        file('goodbye', 'w').write('goodbye cruel world!')
313
 
        self.assertRaises(StrictCommitFailed, wt.commit,
314
 
            message='add hello but not goodbye', strict=True)
315
 
 
316
 
    def test_strict_commit_without_unknowns(self):
317
 
        """Try and commit with no unknown files and strict = True,
318
 
        should work."""
319
 
        from bzrlib.errors import StrictCommitFailed
320
 
        wt = self.make_branch_and_tree('.')
321
 
        b = wt.branch
322
 
        file('hello', 'w').write('hello world')
323
 
        wt.add('hello')
324
 
        wt.commit(message='add hello', strict=True)
325
 
 
326
 
    def test_nonstrict_commit(self):
327
 
        """Try and commit with unknown files and strict = False, should work."""
328
 
        wt = self.make_branch_and_tree('.')
329
 
        b = wt.branch
330
 
        file('hello', 'w').write('hello world')
331
 
        wt.add('hello')
332
 
        file('goodbye', 'w').write('goodbye cruel world!')
333
 
        wt.commit(message='add hello but not goodbye', strict=False)
334
 
 
335
 
    def test_nonstrict_commit_without_unknowns(self):
336
 
        """Try and commit with no unknown files and strict = False,
337
 
        should work."""
338
 
        wt = self.make_branch_and_tree('.')
339
 
        b = wt.branch
340
 
        file('hello', 'w').write('hello world')
341
 
        wt.add('hello')
342
 
        wt.commit(message='add hello', strict=False)
343
 
 
344
 
    def test_signed_commit(self):
345
 
        import bzrlib.gpg
346
 
        import bzrlib.commit as commit
347
 
        oldstrategy = bzrlib.gpg.GPGStrategy
348
 
        wt = self.make_branch_and_tree('.')
349
 
        branch = wt.branch
350
 
        wt.commit("base", allow_pointless=True, rev_id='A')
351
 
        self.failIf(branch.repository.has_signature_for_revision_id('A'))
352
 
        try:
353
 
            from bzrlib.testament import Testament
354
 
            # monkey patch gpg signing mechanism
355
 
            bzrlib.gpg.GPGStrategy = bzrlib.gpg.LoopbackGPGStrategy
356
 
            commit.Commit(config=MustSignConfig(branch)).commit(message="base",
357
 
                                                      allow_pointless=True,
358
 
                                                      rev_id='B',
359
 
                                                      working_tree=wt)
360
 
            self.assertEqual(Testament.from_revision(branch.repository,
361
 
                             'B').as_short_text(),
362
 
                             branch.repository.get_signature_text('B'))
363
 
        finally:
364
 
            bzrlib.gpg.GPGStrategy = oldstrategy
365
 
 
366
 
    def test_commit_failed_signature(self):
367
 
        import bzrlib.gpg
368
 
        import bzrlib.commit as commit
369
 
        oldstrategy = bzrlib.gpg.GPGStrategy
370
 
        wt = self.make_branch_and_tree('.')
371
 
        branch = wt.branch
372
 
        wt.commit("base", allow_pointless=True, rev_id='A')
373
 
        self.failIf(branch.repository.has_signature_for_revision_id('A'))
374
 
        try:
375
 
            from bzrlib.testament import Testament
376
 
            # monkey patch gpg signing mechanism
377
 
            bzrlib.gpg.GPGStrategy = bzrlib.gpg.DisabledGPGStrategy
378
 
            config = MustSignConfig(branch)
379
 
            self.assertRaises(SigningFailed,
380
 
                              commit.Commit(config=config).commit,
381
 
                              message="base",
382
 
                              allow_pointless=True,
383
 
                              rev_id='B',
384
 
                              working_tree=wt)
385
 
            branch = Branch.open(self.get_url('.'))
386
 
            self.assertEqual(branch.revision_history(), ['A'])
387
 
            self.failIf(branch.repository.has_revision('B'))
388
 
        finally:
389
 
            bzrlib.gpg.GPGStrategy = oldstrategy
390
 
 
391
 
    def test_commit_invokes_hooks(self):
392
 
        import bzrlib.commit as commit
393
 
        wt = self.make_branch_and_tree('.')
394
 
        branch = wt.branch
395
 
        calls = []
396
 
        def called(branch, rev_id):
397
 
            calls.append('called')
398
 
        bzrlib.ahook = called
399
 
        try:
400
 
            config = BranchWithHooks(branch)
401
 
            commit.Commit(config=config).commit(
402
 
                            message = "base",
403
 
                            allow_pointless=True,
404
 
                            rev_id='A', working_tree = wt)
405
 
            self.assertEqual(['called', 'called'], calls)
406
 
        finally:
407
 
            del bzrlib.ahook
408
 
 
409
 
    def test_commit_object_doesnt_set_nick(self):
410
 
        # using the Commit object directly does not set the branch nick.
411
 
        wt = self.make_branch_and_tree('.')
412
 
        c = Commit()
413
 
        c.commit(working_tree=wt, message='empty tree', allow_pointless=True)
414
 
        self.assertEquals(wt.branch.revno(), 1)
415
 
        self.assertEqual({},
416
 
                         wt.branch.repository.get_revision(
417
 
                            wt.branch.last_revision()).properties)
418
 
 
419
 
    def test_safe_master_lock(self):
420
 
        os.mkdir('master')
421
 
        master = BzrDirMetaFormat1().initialize('master')
422
 
        master.create_repository()
423
 
        master_branch = master.create_branch()
424
 
        master.create_workingtree()
425
 
        bound = master.sprout('bound')
426
 
        wt = bound.open_workingtree()
427
 
        wt.branch.set_bound_location(os.path.realpath('master'))
428
 
 
429
 
        orig_default = lockdir._DEFAULT_TIMEOUT_SECONDS
430
 
        master_branch.lock_write()
431
 
        try:
432
 
            lockdir._DEFAULT_TIMEOUT_SECONDS = 1
433
 
            self.assertRaises(LockContention, wt.commit, 'silly')
434
 
        finally:
435
 
            lockdir._DEFAULT_TIMEOUT_SECONDS = orig_default
436
 
            master_branch.unlock()
437
 
 
438
 
    def test_commit_bound_merge(self):
439
 
        # see bug #43959; commit of a merge in a bound branch fails to push
440
 
        # the new commit into the master
441
 
        master_branch = self.make_branch('master')
442
 
        bound_tree = self.make_branch_and_tree('bound')
443
 
        bound_tree.branch.bind(master_branch)
444
 
 
445
 
        self.build_tree_contents([('bound/content_file', 'initial contents\n')])
446
 
        bound_tree.add(['content_file'])
447
 
        bound_tree.commit(message='woo!')
448
 
 
449
 
        other_bzrdir = master_branch.bzrdir.sprout('other')
450
 
        other_tree = other_bzrdir.open_workingtree()
451
 
 
452
 
        # do a commit to the the other branch changing the content file so
453
 
        # that our commit after merging will have a merged revision in the
454
 
        # content file history.
455
 
        self.build_tree_contents([('other/content_file', 'change in other\n')])
456
 
        other_tree.commit('change in other')
457
 
 
458
 
        # do a merge into the bound branch from other, and then change the
459
 
        # content file locally to force a new revision (rather than using the
460
 
        # revision from other). This forces extra processing in commit.
461
 
        bound_tree.merge_from_branch(other_tree.branch)
462
 
        self.build_tree_contents([('bound/content_file', 'change in bound\n')])
463
 
 
464
 
        # before #34959 was fixed, this failed with 'revision not present in
465
 
        # weave' when trying to implicitly push from the bound branch to the master
466
 
        bound_tree.commit(message='commit of merge in bound tree')
467
 
 
468
 
    def test_commit_reporting_after_merge(self):
469
 
        # when doing a commit of a merge, the reporter needs to still 
470
 
        # be called for each item that is added/removed/deleted.
471
 
        this_tree = self.make_branch_and_tree('this')
472
 
        # we need a bunch of files and dirs, to perform one action on each.
473
 
        self.build_tree([
474
 
            'this/dirtorename/',
475
 
            'this/dirtoreparent/',
476
 
            'this/dirtoleave/',
477
 
            'this/dirtoremove/',
478
 
            'this/filetoreparent',
479
 
            'this/filetorename',
480
 
            'this/filetomodify',
481
 
            'this/filetoremove',
482
 
            'this/filetoleave']
483
 
            )
484
 
        this_tree.add([
485
 
            'dirtorename',
486
 
            'dirtoreparent',
487
 
            'dirtoleave',
488
 
            'dirtoremove',
489
 
            'filetoreparent',
490
 
            'filetorename',
491
 
            'filetomodify',
492
 
            'filetoremove',
493
 
            'filetoleave']
494
 
            )
495
 
        this_tree.commit('create_files')
496
 
        other_dir = this_tree.bzrdir.sprout('other')
497
 
        other_tree = other_dir.open_workingtree()
498
 
        other_tree.lock_write()
499
 
        # perform the needed actions on the files and dirs.
500
 
        try:
501
 
            other_tree.rename_one('dirtorename', 'renameddir')
502
 
            other_tree.rename_one('dirtoreparent', 'renameddir/reparenteddir')
503
 
            other_tree.rename_one('filetorename', 'renamedfile')
504
 
            other_tree.rename_one('filetoreparent', 'renameddir/reparentedfile')
505
 
            other_tree.remove(['dirtoremove', 'filetoremove'])
506
 
            self.build_tree_contents([
507
 
                ('other/newdir/', ),
508
 
                ('other/filetomodify', 'new content'),
509
 
                ('other/newfile', 'new file content')])
510
 
            other_tree.add('newfile')
511
 
            other_tree.add('newdir/')
512
 
            other_tree.commit('modify all sample files and dirs.')
513
 
        finally:
514
 
            other_tree.unlock()
515
 
        this_tree.merge_from_branch(other_tree.branch)
516
 
        reporter = CapturingReporter()
517
 
        this_tree.commit('do the commit', reporter=reporter)
518
 
        self.assertEqual([
519
 
            ('change', 'unchanged', ''),
520
 
            ('change', 'unchanged', 'dirtoleave'),
521
 
            ('change', 'unchanged', 'filetoleave'),
522
 
            ('change', 'modified', 'filetomodify'),
523
 
            ('change', 'added', 'newdir'),
524
 
            ('change', 'added', 'newfile'),
525
 
            ('renamed', 'renamed', 'dirtorename', 'renameddir'),
526
 
            ('renamed', 'renamed', 'dirtoreparent', 'renameddir/reparenteddir'),
527
 
            ('renamed', 'renamed', 'filetoreparent', 'renameddir/reparentedfile'),
528
 
            ('renamed', 'renamed', 'filetorename', 'renamedfile'),
529
 
            ('deleted', 'dirtoremove'),
530
 
            ('deleted', 'filetoremove'),
531
 
            ],
532
 
            reporter.calls)
533
 
 
534
 
    def test_commit_removals_respects_filespec(self):
535
 
        """Commit respects the specified_files for removals."""
536
 
        tree = self.make_branch_and_tree('.')
537
 
        self.build_tree(['a', 'b'])
538
 
        tree.add(['a', 'b'])
539
 
        tree.commit('added a, b')
540
 
        tree.remove(['a', 'b'])
541
 
        tree.commit('removed a', specific_files='a')
542
 
        basis = tree.basis_tree()
543
 
        tree.lock_read()
544
 
        try:
545
 
            self.assertIs(None, basis.path2id('a'))
546
 
            self.assertFalse(basis.path2id('b') is None)
547
 
        finally:
548
 
            tree.unlock()
549
 
 
550
 
    def test_commit_saves_1ms_timestamp(self):
551
 
        """Passing in a timestamp is saved with 1ms resolution"""
552
 
        tree = self.make_branch_and_tree('.')
553
 
        self.build_tree(['a'])
554
 
        tree.add('a')
555
 
        tree.commit('added a', timestamp=1153248633.4186721, timezone=0,
556
 
                    rev_id='a1')
557
 
 
558
 
        rev = tree.branch.repository.get_revision('a1')
559
 
        self.assertEqual(1153248633.419, rev.timestamp)
560
 
 
561
 
    def test_commit_has_1ms_resolution(self):
562
 
        """Allowing commit to generate the timestamp also has 1ms resolution"""
563
 
        tree = self.make_branch_and_tree('.')
564
 
        self.build_tree(['a'])
565
 
        tree.add('a')
566
 
        tree.commit('added a', rev_id='a1')
567
 
 
568
 
        rev = tree.branch.repository.get_revision('a1')
569
 
        timestamp = rev.timestamp
570
 
        timestamp_1ms = round(timestamp, 3)
571
 
        self.assertEqual(timestamp_1ms, timestamp)
572
 
 
573
 
    def assertBasisTreeKind(self, kind, tree, file_id):
574
 
        basis = tree.basis_tree()
575
 
        basis.lock_read()
576
 
        try:
577
 
            self.assertEqual(kind, basis.kind(file_id))
578
 
        finally:
579
 
            basis.unlock()
580
 
 
581
 
    def test_commit_kind_changes(self):
582
 
        if not osutils.has_symlinks():
583
 
            raise tests.TestSkipped('Test requires symlink support')
584
 
        tree = self.make_branch_and_tree('.')
585
 
        os.symlink('target', 'name')
586
 
        tree.add('name', 'a-file-id')
587
 
        tree.commit('Added a symlink')
588
 
        self.assertBasisTreeKind('symlink', tree, 'a-file-id')
589
 
 
590
 
        os.unlink('name')
591
 
        self.build_tree(['name'])
592
 
        tree.commit('Changed symlink to file')
593
 
        self.assertBasisTreeKind('file', tree, 'a-file-id')
594
 
 
595
 
        os.unlink('name')
596
 
        os.symlink('target', 'name')
597
 
        tree.commit('file to symlink')
598
 
        self.assertBasisTreeKind('symlink', tree, 'a-file-id')
599
 
 
600
 
        os.unlink('name')
601
 
        os.mkdir('name')
602
 
        tree.commit('symlink to directory')
603
 
        self.assertBasisTreeKind('directory', tree, 'a-file-id')
604
 
 
605
 
        os.rmdir('name')
606
 
        os.symlink('target', 'name')
607
 
        tree.commit('directory to symlink')
608
 
        self.assertBasisTreeKind('symlink', tree, 'a-file-id')
609
 
 
610
 
        # prepare for directory <-> file tests
611
 
        os.unlink('name')
612
 
        os.mkdir('name')
613
 
        tree.commit('symlink to directory')
614
 
        self.assertBasisTreeKind('directory', tree, 'a-file-id')
615
 
 
616
 
        os.rmdir('name')
617
 
        self.build_tree(['name'])
618
 
        tree.commit('Changed directory to file')
619
 
        self.assertBasisTreeKind('file', tree, 'a-file-id')
620
 
 
621
 
        os.unlink('name')
622
 
        os.mkdir('name')
623
 
        tree.commit('file to directory')
624
 
        self.assertBasisTreeKind('directory', tree, 'a-file-id')
625
 
 
626
 
    def test_commit_unversioned_specified(self):
627
 
        """Commit should raise if specified files isn't in basis or worktree"""
628
 
        tree = self.make_branch_and_tree('.')
629
 
        self.assertRaises(errors.PathsNotVersionedError, tree.commit, 
630
 
                          'message', specific_files=['bogus'])
631
 
 
632
 
    class Callback(object):
633
 
        
634
 
        def __init__(self, message, testcase):
635
 
            self.called = False
636
 
            self.message = message
637
 
            self.testcase = testcase
638
 
 
639
 
        def __call__(self, commit_obj):
640
 
            self.called = True
641
 
            self.testcase.assertTrue(isinstance(commit_obj, Commit))
642
 
            return self.message
643
 
 
644
 
    def test_commit_callback(self):
645
 
        """Commit should invoke a callback to get the message"""
646
 
 
647
 
        tree = self.make_branch_and_tree('.')
648
 
        try:
649
 
            tree.commit()
650
 
        except Exception, e:
651
 
            self.assertTrue(isinstance(e, BzrError))
652
 
            self.assertEqual('The message or message_callback keyword'
653
 
                             ' parameter is required for commit().', str(e))
654
 
        else:
655
 
            self.fail('exception not raised')
656
 
        cb = self.Callback(u'commit 1', self)
657
 
        tree.commit(message_callback=cb)
658
 
        self.assertTrue(cb.called)
659
 
        repository = tree.branch.repository
660
 
        message = repository.get_revision(tree.last_revision()).message
661
 
        self.assertEqual('commit 1', message)
662
 
 
663
 
    def test_no_callback_pointless(self):
664
 
        """Callback should not be invoked for pointless commit"""
665
 
        tree = self.make_branch_and_tree('.')
666
 
        cb = self.Callback(u'commit 2', self)
667
 
        self.assertRaises(PointlessCommit, tree.commit, message_callback=cb, 
668
 
                          allow_pointless=False)
669
 
        self.assertFalse(cb.called)
670
 
 
671
 
    def test_no_callback_netfailure(self):
672
 
        """Callback should not be invoked if connectivity fails"""
673
 
        tree = self.make_branch_and_tree('.')
674
 
        cb = self.Callback(u'commit 2', self)
675
 
        repository = tree.branch.repository
676
 
        # simulate network failure
677
 
        def raise_(self, arg, arg2):
678
 
            raise errors.NoSuchFile('foo')
679
 
        repository.add_inventory = raise_
680
 
        self.assertRaises(errors.NoSuchFile, tree.commit, message_callback=cb)
681
 
        self.assertFalse(cb.called)