~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_commit.py

  • Committer: John Arbash Meinel
  • Date: 2007-03-15 22:35:35 UTC
  • mto: This revision was merged to the branch mainline in revision 2363.
  • Revision ID: john@arbash-meinel.com-20070315223535-d3d4964oe1hc8zhg
Add an overzealous test, for Unicode support of _iter_changes.
For both knowns and unknowns.
And include a basic, if suboptimal, fix.
I would rather defer the decoding until we've determined that we are going to return the tuple.
There is still something broken with added files, but I'll get to that.

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
            def sign(text):
 
361
                return bzrlib.gpg.LoopbackGPGStrategy(None).sign(text)
 
362
            self.assertEqual(sign(Testament.from_revision(branch.repository,
 
363
                             'B').as_short_text()),
 
364
                             branch.repository.get_signature_text('B'))
 
365
        finally:
 
366
            bzrlib.gpg.GPGStrategy = oldstrategy
 
367
 
 
368
    def test_commit_failed_signature(self):
 
369
        import bzrlib.gpg
 
370
        import bzrlib.commit as commit
 
371
        oldstrategy = bzrlib.gpg.GPGStrategy
 
372
        wt = self.make_branch_and_tree('.')
 
373
        branch = wt.branch
 
374
        wt.commit("base", allow_pointless=True, rev_id='A')
 
375
        self.failIf(branch.repository.has_signature_for_revision_id('A'))
 
376
        try:
 
377
            from bzrlib.testament import Testament
 
378
            # monkey patch gpg signing mechanism
 
379
            bzrlib.gpg.GPGStrategy = bzrlib.gpg.DisabledGPGStrategy
 
380
            config = MustSignConfig(branch)
 
381
            self.assertRaises(SigningFailed,
 
382
                              commit.Commit(config=config).commit,
 
383
                              message="base",
 
384
                              allow_pointless=True,
 
385
                              rev_id='B',
 
386
                              working_tree=wt)
 
387
            branch = Branch.open(self.get_url('.'))
 
388
            self.assertEqual(branch.revision_history(), ['A'])
 
389
            self.failIf(branch.repository.has_revision('B'))
 
390
        finally:
 
391
            bzrlib.gpg.GPGStrategy = oldstrategy
 
392
 
 
393
    def test_commit_invokes_hooks(self):
 
394
        import bzrlib.commit as commit
 
395
        wt = self.make_branch_and_tree('.')
 
396
        branch = wt.branch
 
397
        calls = []
 
398
        def called(branch, rev_id):
 
399
            calls.append('called')
 
400
        bzrlib.ahook = called
 
401
        try:
 
402
            config = BranchWithHooks(branch)
 
403
            commit.Commit(config=config).commit(
 
404
                            message = "base",
 
405
                            allow_pointless=True,
 
406
                            rev_id='A', working_tree = wt)
 
407
            self.assertEqual(['called', 'called'], calls)
 
408
        finally:
 
409
            del bzrlib.ahook
 
410
 
 
411
    def test_commit_object_doesnt_set_nick(self):
 
412
        # using the Commit object directly does not set the branch nick.
 
413
        wt = self.make_branch_and_tree('.')
 
414
        c = Commit()
 
415
        c.commit(working_tree=wt, message='empty tree', allow_pointless=True)
 
416
        self.assertEquals(wt.branch.revno(), 1)
 
417
        self.assertEqual({},
 
418
                         wt.branch.repository.get_revision(
 
419
                            wt.branch.last_revision()).properties)
 
420
 
 
421
    def test_safe_master_lock(self):
 
422
        os.mkdir('master')
 
423
        master = BzrDirMetaFormat1().initialize('master')
 
424
        master.create_repository()
 
425
        master_branch = master.create_branch()
 
426
        master.create_workingtree()
 
427
        bound = master.sprout('bound')
 
428
        wt = bound.open_workingtree()
 
429
        wt.branch.set_bound_location(os.path.realpath('master'))
 
430
 
 
431
        orig_default = lockdir._DEFAULT_TIMEOUT_SECONDS
 
432
        master_branch.lock_write()
 
433
        try:
 
434
            lockdir._DEFAULT_TIMEOUT_SECONDS = 1
 
435
            self.assertRaises(LockContention, wt.commit, 'silly')
 
436
        finally:
 
437
            lockdir._DEFAULT_TIMEOUT_SECONDS = orig_default
 
438
            master_branch.unlock()
 
439
 
 
440
    def test_commit_bound_merge(self):
 
441
        # see bug #43959; commit of a merge in a bound branch fails to push
 
442
        # the new commit into the master
 
443
        master_branch = self.make_branch('master')
 
444
        bound_tree = self.make_branch_and_tree('bound')
 
445
        bound_tree.branch.bind(master_branch)
 
446
 
 
447
        self.build_tree_contents([('bound/content_file', 'initial contents\n')])
 
448
        bound_tree.add(['content_file'])
 
449
        bound_tree.commit(message='woo!')
 
450
 
 
451
        other_bzrdir = master_branch.bzrdir.sprout('other')
 
452
        other_tree = other_bzrdir.open_workingtree()
 
453
 
 
454
        # do a commit to the the other branch changing the content file so
 
455
        # that our commit after merging will have a merged revision in the
 
456
        # content file history.
 
457
        self.build_tree_contents([('other/content_file', 'change in other\n')])
 
458
        other_tree.commit('change in other')
 
459
 
 
460
        # do a merge into the bound branch from other, and then change the
 
461
        # content file locally to force a new revision (rather than using the
 
462
        # revision from other). This forces extra processing in commit.
 
463
        bound_tree.merge_from_branch(other_tree.branch)
 
464
        self.build_tree_contents([('bound/content_file', 'change in bound\n')])
 
465
 
 
466
        # before #34959 was fixed, this failed with 'revision not present in
 
467
        # weave' when trying to implicitly push from the bound branch to the master
 
468
        bound_tree.commit(message='commit of merge in bound tree')
 
469
 
 
470
    def test_commit_reporting_after_merge(self):
 
471
        # when doing a commit of a merge, the reporter needs to still 
 
472
        # be called for each item that is added/removed/deleted.
 
473
        this_tree = self.make_branch_and_tree('this')
 
474
        # we need a bunch of files and dirs, to perform one action on each.
 
475
        self.build_tree([
 
476
            'this/dirtorename/',
 
477
            'this/dirtoreparent/',
 
478
            'this/dirtoleave/',
 
479
            'this/dirtoremove/',
 
480
            'this/filetoreparent',
 
481
            'this/filetorename',
 
482
            'this/filetomodify',
 
483
            'this/filetoremove',
 
484
            'this/filetoleave']
 
485
            )
 
486
        this_tree.add([
 
487
            'dirtorename',
 
488
            'dirtoreparent',
 
489
            'dirtoleave',
 
490
            'dirtoremove',
 
491
            'filetoreparent',
 
492
            'filetorename',
 
493
            'filetomodify',
 
494
            'filetoremove',
 
495
            'filetoleave']
 
496
            )
 
497
        this_tree.commit('create_files')
 
498
        other_dir = this_tree.bzrdir.sprout('other')
 
499
        other_tree = other_dir.open_workingtree()
 
500
        other_tree.lock_write()
 
501
        # perform the needed actions on the files and dirs.
 
502
        try:
 
503
            other_tree.rename_one('dirtorename', 'renameddir')
 
504
            other_tree.rename_one('dirtoreparent', 'renameddir/reparenteddir')
 
505
            other_tree.rename_one('filetorename', 'renamedfile')
 
506
            other_tree.rename_one('filetoreparent', 'renameddir/reparentedfile')
 
507
            other_tree.remove(['dirtoremove', 'filetoremove'])
 
508
            self.build_tree_contents([
 
509
                ('other/newdir/', ),
 
510
                ('other/filetomodify', 'new content'),
 
511
                ('other/newfile', 'new file content')])
 
512
            other_tree.add('newfile')
 
513
            other_tree.add('newdir/')
 
514
            other_tree.commit('modify all sample files and dirs.')
 
515
        finally:
 
516
            other_tree.unlock()
 
517
        this_tree.merge_from_branch(other_tree.branch)
 
518
        reporter = CapturingReporter()
 
519
        this_tree.commit('do the commit', reporter=reporter)
 
520
        self.assertEqual([
 
521
            ('change', 'unchanged', ''),
 
522
            ('change', 'unchanged', 'dirtoleave'),
 
523
            ('change', 'unchanged', 'filetoleave'),
 
524
            ('change', 'modified', 'filetomodify'),
 
525
            ('change', 'added', 'newdir'),
 
526
            ('change', 'added', 'newfile'),
 
527
            ('renamed', 'renamed', 'dirtorename', 'renameddir'),
 
528
            ('renamed', 'renamed', 'dirtoreparent', 'renameddir/reparenteddir'),
 
529
            ('renamed', 'renamed', 'filetoreparent', 'renameddir/reparentedfile'),
 
530
            ('renamed', 'renamed', 'filetorename', 'renamedfile'),
 
531
            ('deleted', 'dirtoremove'),
 
532
            ('deleted', 'filetoremove'),
 
533
            ],
 
534
            reporter.calls)
 
535
 
 
536
    def test_commit_removals_respects_filespec(self):
 
537
        """Commit respects the specified_files for removals."""
 
538
        tree = self.make_branch_and_tree('.')
 
539
        self.build_tree(['a', 'b'])
 
540
        tree.add(['a', 'b'])
 
541
        tree.commit('added a, b')
 
542
        tree.remove(['a', 'b'])
 
543
        tree.commit('removed a', specific_files='a')
 
544
        basis = tree.basis_tree()
 
545
        tree.lock_read()
 
546
        try:
 
547
            self.assertIs(None, basis.path2id('a'))
 
548
            self.assertFalse(basis.path2id('b') is None)
 
549
        finally:
 
550
            tree.unlock()
 
551
 
 
552
    def test_commit_saves_1ms_timestamp(self):
 
553
        """Passing in a timestamp is saved with 1ms resolution"""
 
554
        tree = self.make_branch_and_tree('.')
 
555
        self.build_tree(['a'])
 
556
        tree.add('a')
 
557
        tree.commit('added a', timestamp=1153248633.4186721, timezone=0,
 
558
                    rev_id='a1')
 
559
 
 
560
        rev = tree.branch.repository.get_revision('a1')
 
561
        self.assertEqual(1153248633.419, rev.timestamp)
 
562
 
 
563
    def test_commit_has_1ms_resolution(self):
 
564
        """Allowing commit to generate the timestamp also has 1ms resolution"""
 
565
        tree = self.make_branch_and_tree('.')
 
566
        self.build_tree(['a'])
 
567
        tree.add('a')
 
568
        tree.commit('added a', rev_id='a1')
 
569
 
 
570
        rev = tree.branch.repository.get_revision('a1')
 
571
        timestamp = rev.timestamp
 
572
        timestamp_1ms = round(timestamp, 3)
 
573
        self.assertEqual(timestamp_1ms, timestamp)
 
574
 
 
575
    def assertBasisTreeKind(self, kind, tree, file_id):
 
576
        basis = tree.basis_tree()
 
577
        basis.lock_read()
 
578
        try:
 
579
            self.assertEqual(kind, basis.kind(file_id))
 
580
        finally:
 
581
            basis.unlock()
 
582
 
 
583
    def test_commit_kind_changes(self):
 
584
        if not osutils.has_symlinks():
 
585
            raise tests.TestSkipped('Test requires symlink support')
 
586
        tree = self.make_branch_and_tree('.')
 
587
        os.symlink('target', 'name')
 
588
        tree.add('name', 'a-file-id')
 
589
        tree.commit('Added a symlink')
 
590
        self.assertBasisTreeKind('symlink', tree, 'a-file-id')
 
591
 
 
592
        os.unlink('name')
 
593
        self.build_tree(['name'])
 
594
        tree.commit('Changed symlink to file')
 
595
        self.assertBasisTreeKind('file', tree, 'a-file-id')
 
596
 
 
597
        os.unlink('name')
 
598
        os.symlink('target', 'name')
 
599
        tree.commit('file to symlink')
 
600
        self.assertBasisTreeKind('symlink', tree, 'a-file-id')
 
601
 
 
602
        os.unlink('name')
 
603
        os.mkdir('name')
 
604
        tree.commit('symlink to directory')
 
605
        self.assertBasisTreeKind('directory', tree, 'a-file-id')
 
606
 
 
607
        os.rmdir('name')
 
608
        os.symlink('target', 'name')
 
609
        tree.commit('directory to symlink')
 
610
        self.assertBasisTreeKind('symlink', tree, 'a-file-id')
 
611
 
 
612
        # prepare for directory <-> file tests
 
613
        os.unlink('name')
 
614
        os.mkdir('name')
 
615
        tree.commit('symlink to directory')
 
616
        self.assertBasisTreeKind('directory', tree, 'a-file-id')
 
617
 
 
618
        os.rmdir('name')
 
619
        self.build_tree(['name'])
 
620
        tree.commit('Changed directory to file')
 
621
        self.assertBasisTreeKind('file', tree, 'a-file-id')
 
622
 
 
623
        os.unlink('name')
 
624
        os.mkdir('name')
 
625
        tree.commit('file to directory')
 
626
        self.assertBasisTreeKind('directory', tree, 'a-file-id')
 
627
 
 
628
    def test_commit_unversioned_specified(self):
 
629
        """Commit should raise if specified files isn't in basis or worktree"""
 
630
        tree = self.make_branch_and_tree('.')
 
631
        self.assertRaises(errors.PathsNotVersionedError, tree.commit, 
 
632
                          'message', specific_files=['bogus'])
 
633
 
 
634
    class Callback(object):
 
635
        
 
636
        def __init__(self, message, testcase):
 
637
            self.called = False
 
638
            self.message = message
 
639
            self.testcase = testcase
 
640
 
 
641
        def __call__(self, commit_obj):
 
642
            self.called = True
 
643
            self.testcase.assertTrue(isinstance(commit_obj, Commit))
 
644
            return self.message
 
645
 
 
646
    def test_commit_callback(self):
 
647
        """Commit should invoke a callback to get the message"""
 
648
 
 
649
        tree = self.make_branch_and_tree('.')
 
650
        try:
 
651
            tree.commit()
 
652
        except Exception, e:
 
653
            self.assertTrue(isinstance(e, BzrError))
 
654
            self.assertEqual('The message or message_callback keyword'
 
655
                             ' parameter is required for commit().', str(e))
 
656
        else:
 
657
            self.fail('exception not raised')
 
658
        cb = self.Callback(u'commit 1', self)
 
659
        tree.commit(message_callback=cb)
 
660
        self.assertTrue(cb.called)
 
661
        repository = tree.branch.repository
 
662
        message = repository.get_revision(tree.last_revision()).message
 
663
        self.assertEqual('commit 1', message)
 
664
 
 
665
    def test_no_callback_pointless(self):
 
666
        """Callback should not be invoked for pointless commit"""
 
667
        tree = self.make_branch_and_tree('.')
 
668
        cb = self.Callback(u'commit 2', self)
 
669
        self.assertRaises(PointlessCommit, tree.commit, message_callback=cb, 
 
670
                          allow_pointless=False)
 
671
        self.assertFalse(cb.called)
 
672
 
 
673
    def test_no_callback_netfailure(self):
 
674
        """Callback should not be invoked if connectivity fails"""
 
675
        tree = self.make_branch_and_tree('.')
 
676
        cb = self.Callback(u'commit 2', self)
 
677
        repository = tree.branch.repository
 
678
        # simulate network failure
 
679
        def raise_(self, arg, arg2):
 
680
            raise errors.NoSuchFile('foo')
 
681
        repository.add_inventory = raise_
 
682
        self.assertRaises(errors.NoSuchFile, tree.commit, message_callback=cb)
 
683
        self.assertFalse(cb.called)