~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-07-04 12:26:02 UTC
  • Revision ID: mbp@sourcefrog.net-20050704122602-69901910521e62c3
- check command checks that all inventory-ids are the same as in the revision.

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