~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_commit.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2007-11-03 23:02:16 UTC
  • mfrom: (2951.1.1 pack)
  • Revision ID: pqm@pqm.ubuntu.com-20071103230216-mnmwuxm413lyhjdv
(robertc) Fix data-refresh logic for packs not to refresh mid-transaction when a names write lock is held. (Robert Collins)

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 SymlinkFeature, 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
        self.requireFeature(SymlinkFeature)
 
588
        tree = self.make_branch_and_tree('.')
 
589
        os.symlink('target', 'name')
 
590
        tree.add('name', 'a-file-id')
 
591
        tree.commit('Added a symlink')
 
592
        self.assertBasisTreeKind('symlink', tree, 'a-file-id')
 
593
 
 
594
        os.unlink('name')
 
595
        self.build_tree(['name'])
 
596
        tree.commit('Changed symlink to file')
 
597
        self.assertBasisTreeKind('file', tree, 'a-file-id')
 
598
 
 
599
        os.unlink('name')
 
600
        os.symlink('target', 'name')
 
601
        tree.commit('file to symlink')
 
602
        self.assertBasisTreeKind('symlink', tree, 'a-file-id')
 
603
 
 
604
        os.unlink('name')
 
605
        os.mkdir('name')
 
606
        tree.commit('symlink to directory')
 
607
        self.assertBasisTreeKind('directory', tree, 'a-file-id')
 
608
 
 
609
        os.rmdir('name')
 
610
        os.symlink('target', 'name')
 
611
        tree.commit('directory to symlink')
 
612
        self.assertBasisTreeKind('symlink', tree, 'a-file-id')
 
613
 
 
614
        # prepare for directory <-> file tests
 
615
        os.unlink('name')
 
616
        os.mkdir('name')
 
617
        tree.commit('symlink to directory')
 
618
        self.assertBasisTreeKind('directory', tree, 'a-file-id')
 
619
 
 
620
        os.rmdir('name')
 
621
        self.build_tree(['name'])
 
622
        tree.commit('Changed directory to file')
 
623
        self.assertBasisTreeKind('file', tree, 'a-file-id')
 
624
 
 
625
        os.unlink('name')
 
626
        os.mkdir('name')
 
627
        tree.commit('file to directory')
 
628
        self.assertBasisTreeKind('directory', tree, 'a-file-id')
 
629
 
 
630
    def test_commit_unversioned_specified(self):
 
631
        """Commit should raise if specified files isn't in basis or worktree"""
 
632
        tree = self.make_branch_and_tree('.')
 
633
        self.assertRaises(errors.PathsNotVersionedError, tree.commit, 
 
634
                          'message', specific_files=['bogus'])
 
635
 
 
636
    class Callback(object):
 
637
        
 
638
        def __init__(self, message, testcase):
 
639
            self.called = False
 
640
            self.message = message
 
641
            self.testcase = testcase
 
642
 
 
643
        def __call__(self, commit_obj):
 
644
            self.called = True
 
645
            self.testcase.assertTrue(isinstance(commit_obj, Commit))
 
646
            return self.message
 
647
 
 
648
    def test_commit_callback(self):
 
649
        """Commit should invoke a callback to get the message"""
 
650
 
 
651
        tree = self.make_branch_and_tree('.')
 
652
        try:
 
653
            tree.commit()
 
654
        except Exception, e:
 
655
            self.assertTrue(isinstance(e, BzrError))
 
656
            self.assertEqual('The message or message_callback keyword'
 
657
                             ' parameter is required for commit().', str(e))
 
658
        else:
 
659
            self.fail('exception not raised')
 
660
        cb = self.Callback(u'commit 1', self)
 
661
        tree.commit(message_callback=cb)
 
662
        self.assertTrue(cb.called)
 
663
        repository = tree.branch.repository
 
664
        message = repository.get_revision(tree.last_revision()).message
 
665
        self.assertEqual('commit 1', message)
 
666
 
 
667
    def test_no_callback_pointless(self):
 
668
        """Callback should not be invoked for pointless commit"""
 
669
        tree = self.make_branch_and_tree('.')
 
670
        cb = self.Callback(u'commit 2', self)
 
671
        self.assertRaises(PointlessCommit, tree.commit, message_callback=cb, 
 
672
                          allow_pointless=False)
 
673
        self.assertFalse(cb.called)
 
674
 
 
675
    def test_no_callback_netfailure(self):
 
676
        """Callback should not be invoked if connectivity fails"""
 
677
        tree = self.make_branch_and_tree('.')
 
678
        cb = self.Callback(u'commit 2', self)
 
679
        repository = tree.branch.repository
 
680
        # simulate network failure
 
681
        def raise_(self, arg, arg2):
 
682
            raise errors.NoSuchFile('foo')
 
683
        repository.add_inventory = raise_
 
684
        self.assertRaises(errors.NoSuchFile, tree.commit, message_callback=cb)
 
685
        self.assertFalse(cb.called)
 
686
 
 
687
    def test_selected_file_merge_commit(self):
 
688
        """Ensure the correct error is raised"""
 
689
        tree = self.make_branch_and_tree('foo')
 
690
        # pending merge would turn into a left parent
 
691
        tree.commit('commit 1')
 
692
        tree.add_parent_tree_id('example')
 
693
        self.build_tree(['foo/bar', 'foo/baz'])
 
694
        tree.add(['bar', 'baz'])
 
695
        err = self.assertRaises(errors.CannotCommitSelectedFileMerge,
 
696
            tree.commit, 'commit 2', specific_files=['bar', 'baz'])
 
697
        self.assertEqual(['bar', 'baz'], err.files)
 
698
        self.assertEqual('Selected-file commit of merges is not supported'
 
699
                         ' yet: files bar, baz', str(err))
 
700
 
 
701
    def test_commit_ordering(self):
 
702
        """Test of corner-case commit ordering error"""
 
703
        tree = self.make_branch_and_tree('.')
 
704
        self.build_tree(['a/', 'a/z/', 'a/c/', 'a/z/x', 'a/z/y'])
 
705
        tree.add(['a/', 'a/z/', 'a/c/', 'a/z/x', 'a/z/y'])
 
706
        tree.commit('setup')
 
707
        self.build_tree(['a/c/d/'])
 
708
        tree.add('a/c/d')
 
709
        tree.rename_one('a/z/x', 'a/c/d/x')
 
710
        tree.commit('test', specific_files=['a/z/y'])
 
711
 
 
712
    def test_commit_no_author(self):
 
713
        """The default kwarg author in MutableTree.commit should not add
 
714
        the 'author' revision property.
 
715
        """
 
716
        tree = self.make_branch_and_tree('foo')
 
717
        rev_id = tree.commit('commit 1')
 
718
        rev = tree.branch.repository.get_revision(rev_id)
 
719
        self.assertFalse('author' in rev.properties)
 
720
 
 
721
    def test_commit_author(self):
 
722
        """Passing a non-empty author kwarg to MutableTree.commit should add
 
723
        the 'author' revision property.
 
724
        """
 
725
        tree = self.make_branch_and_tree('foo')
 
726
        rev_id = tree.commit('commit 1', author='John Doe <jdoe@example.com>')
 
727
        rev = tree.branch.repository.get_revision(rev_id)
 
728
        self.assertEqual('John Doe <jdoe@example.com>',
 
729
                         rev.properties['author'])