~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_commit.py

Add source index to the index iteration API to allow mapping back to the origin of retrieved data.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005 by Canonical Ltd
2
 
 
 
1
# Copyright (C) 2005, 2006 Canonical Ltd
 
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
5
5
# the Free Software Foundation; either version 2 of the License, or
6
6
# (at your option) any later version.
7
 
 
 
7
#
8
8
# This program is distributed in the hope that it will be useful,
9
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
11
# GNU General Public License for more details.
12
 
 
 
12
#
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18
18
import os
19
19
 
20
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)
21
33
from bzrlib.tests import TestCaseWithTransport
22
 
from bzrlib.branch import Branch
23
34
from bzrlib.workingtree import WorkingTree
24
 
from bzrlib.commit import Commit
25
 
from bzrlib.config import BranchConfig
26
 
from bzrlib.errors import PointlessCommit, BzrError, SigningFailed
27
35
 
28
36
 
29
37
# TODO: Test commit with some added, and added-but-missing files
43
51
        return "bzrlib.ahook bzrlib.ahook"
44
52
 
45
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
 
46
74
class TestCommit(TestCaseWithTransport):
47
75
 
48
76
    def test_simple_commit(self):
195
223
        wt.move(['hello'], 'a')
196
224
        r2 = 'test@rev-2'
197
225
        wt.commit('two', rev_id=r2, allow_pointless=False)
198
 
        self.check_inventory_shape(wt.read_working_inventory(),
199
 
                                   ['a', 'a/hello', 'b'])
 
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()
200
232
 
201
233
        wt.move(['b'], 'a')
202
234
        r3 = 'test@rev-3'
203
235
        wt.commit('three', rev_id=r3, allow_pointless=False)
204
 
        self.check_inventory_shape(wt.read_working_inventory(),
205
 
                                   ['a', 'a/hello', 'a/b'])
206
 
        self.check_inventory_shape(b.repository.get_revision_inventory(r3),
207
 
                                   ['a', 'a/hello', 'a/b'])
 
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()
208
244
 
209
245
        wt.move(['a/hello'], 'a/b')
210
246
        r4 = 'test@rev-4'
211
247
        wt.commit('four', rev_id=r4, allow_pointless=False)
212
 
        self.check_inventory_shape(wt.read_working_inventory(),
213
 
                                   ['a', 'a/b/hello', 'a/b'])
 
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()
214
254
 
215
255
        inv = b.repository.get_revision_inventory(r4)
216
256
        eq(inv['hello-id'].revision, r4)
217
257
        eq(inv['a-id'].revision, r1)
218
258
        eq(inv['b-id'].revision, r3)
219
 
        
 
259
 
220
260
    def test_removed_commit(self):
221
261
        """Commit with a removed file"""
222
262
        wt = self.make_branch_and_tree('.')
308
348
        wt = self.make_branch_and_tree('.')
309
349
        branch = wt.branch
310
350
        wt.commit("base", allow_pointless=True, rev_id='A')
311
 
        self.failIf(branch.repository.revision_store.has_id('A', 'sig'))
 
351
        self.failIf(branch.repository.has_signature_for_revision_id('A'))
312
352
        try:
313
353
            from bzrlib.testament import Testament
314
354
            # monkey patch gpg signing mechanism
317
357
                                                      allow_pointless=True,
318
358
                                                      rev_id='B',
319
359
                                                      working_tree=wt)
320
 
            self.assertEqual(Testament.from_revision(branch.repository,
321
 
                             'B').as_short_text(),
322
 
                             branch.repository.revision_store.get('B', 
323
 
                                                               'sig').read())
 
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'))
324
365
        finally:
325
366
            bzrlib.gpg.GPGStrategy = oldstrategy
326
367
 
331
372
        wt = self.make_branch_and_tree('.')
332
373
        branch = wt.branch
333
374
        wt.commit("base", allow_pointless=True, rev_id='A')
334
 
        self.failIf(branch.repository.revision_store.has_id('A', 'sig'))
 
375
        self.failIf(branch.repository.has_signature_for_revision_id('A'))
335
376
        try:
336
377
            from bzrlib.testament import Testament
337
378
            # monkey patch gpg signing mechanism
345
386
                              working_tree=wt)
346
387
            branch = Branch.open(self.get_url('.'))
347
388
            self.assertEqual(branch.revision_history(), ['A'])
348
 
            self.failIf(branch.repository.revision_store.has_id('B'))
 
389
            self.failIf(branch.repository.has_revision('B'))
349
390
        finally:
350
391
            bzrlib.gpg.GPGStrategy = oldstrategy
351
392
 
366
407
            self.assertEqual(['called', 'called'], calls)
367
408
        finally:
368
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)
 
684
 
 
685
    def test_selected_file_merge_commit(self):
 
686
        """Ensure the correct error is raised"""
 
687
        tree = self.make_branch_and_tree('foo')
 
688
        # pending merge would turn into a left parent
 
689
        tree.commit('commit 1')
 
690
        tree.add_parent_tree_id('example')
 
691
        self.build_tree(['foo/bar', 'foo/baz'])
 
692
        tree.add(['bar', 'baz'])
 
693
        err = self.assertRaises(errors.CannotCommitSelectedFileMerge,
 
694
            tree.commit, 'commit 2', specific_files=['bar', 'baz'])
 
695
        self.assertEqual(['bar', 'baz'], err.files)
 
696
        self.assertEqual('Selected-file commit of merges is not supported'
 
697
                         ' yet: files bar, baz', str(err))