~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_commit.py

  • Committer: Aaron Bentley
  • Date: 2006-06-14 19:45:57 UTC
  • mto: This revision was merged to the branch mainline in revision 1777.
  • Revision ID: abentley@panoramicfeedback.com-20060614194557-6b499aa1cf03f7e6
Use create_signature for signing policy, deprecate check_signatures for this

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005, 2006 by 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.tests import TestCaseWithTransport
 
22
from bzrlib.branch import Branch
 
23
from bzrlib.bzrdir import BzrDir, BzrDirMetaFormat1
 
24
from bzrlib.workingtree import WorkingTree
 
25
from bzrlib.commit import Commit, NullCommitReporter
 
26
from bzrlib.config import BranchConfig
 
27
from bzrlib.errors import (PointlessCommit, BzrError, SigningFailed, 
 
28
                           LockContention)
 
29
 
 
30
 
 
31
# TODO: Test commit with some added, and added-but-missing files
 
32
 
 
33
class MustSignConfig(BranchConfig):
 
34
 
 
35
    def signature_needed(self):
 
36
        return True
 
37
 
 
38
    def gpg_signing_command(self):
 
39
        return ['cat', '-']
 
40
 
 
41
 
 
42
class BranchWithHooks(BranchConfig):
 
43
 
 
44
    def post_commit(self):
 
45
        return "bzrlib.ahook bzrlib.ahook"
 
46
 
 
47
 
 
48
class CapturingReporter(NullCommitReporter):
 
49
    """This reporter captures the calls made to it for evaluation later."""
 
50
 
 
51
    def __init__(self):
 
52
        # a list of the calls this received
 
53
        self.calls = []
 
54
 
 
55
    def snapshot_change(self, change, path):
 
56
        self.calls.append(('change', change, path))
 
57
 
 
58
    def deleted(self, file_id):
 
59
        self.calls.append(('deleted', file_id))
 
60
 
 
61
    def missing(self, path):
 
62
        self.calls.append(('missing', path))
 
63
 
 
64
    def renamed(self, change, old_path, new_path):
 
65
        self.calls.append(('renamed', change, old_path, new_path))
 
66
 
 
67
 
 
68
class TestCommit(TestCaseWithTransport):
 
69
 
 
70
    def test_simple_commit(self):
 
71
        """Commit and check two versions of a single file."""
 
72
        wt = self.make_branch_and_tree('.')
 
73
        b = wt.branch
 
74
        file('hello', 'w').write('hello world')
 
75
        wt.add('hello')
 
76
        wt.commit(message='add hello')
 
77
        file_id = wt.path2id('hello')
 
78
 
 
79
        file('hello', 'w').write('version 2')
 
80
        wt.commit(message='commit 2')
 
81
 
 
82
        eq = self.assertEquals
 
83
        eq(b.revno(), 2)
 
84
        rh = b.revision_history()
 
85
        rev = b.repository.get_revision(rh[0])
 
86
        eq(rev.message, 'add hello')
 
87
 
 
88
        tree1 = b.repository.revision_tree(rh[0])
 
89
        text = tree1.get_file_text(file_id)
 
90
        eq(text, 'hello world')
 
91
 
 
92
        tree2 = b.repository.revision_tree(rh[1])
 
93
        eq(tree2.get_file_text(file_id), 'version 2')
 
94
 
 
95
    def test_delete_commit(self):
 
96
        """Test a commit with a deleted file"""
 
97
        wt = self.make_branch_and_tree('.')
 
98
        b = wt.branch
 
99
        file('hello', 'w').write('hello world')
 
100
        wt.add(['hello'], ['hello-id'])
 
101
        wt.commit(message='add hello')
 
102
 
 
103
        os.remove('hello')
 
104
        wt.commit('removed hello', rev_id='rev2')
 
105
 
 
106
        tree = b.repository.revision_tree('rev2')
 
107
        self.assertFalse(tree.has_id('hello-id'))
 
108
 
 
109
    def test_pointless_commit(self):
 
110
        """Commit refuses unless there are changes or it's forced."""
 
111
        wt = self.make_branch_and_tree('.')
 
112
        b = wt.branch
 
113
        file('hello', 'w').write('hello')
 
114
        wt.add(['hello'])
 
115
        wt.commit(message='add hello')
 
116
        self.assertEquals(b.revno(), 1)
 
117
        self.assertRaises(PointlessCommit,
 
118
                          wt.commit,
 
119
                          message='fails',
 
120
                          allow_pointless=False)
 
121
        self.assertEquals(b.revno(), 1)
 
122
        
 
123
    def test_commit_empty(self):
 
124
        """Commiting an empty tree works."""
 
125
        wt = self.make_branch_and_tree('.')
 
126
        b = wt.branch
 
127
        wt.commit(message='empty tree', allow_pointless=True)
 
128
        self.assertRaises(PointlessCommit,
 
129
                          wt.commit,
 
130
                          message='empty tree',
 
131
                          allow_pointless=False)
 
132
        wt.commit(message='empty tree', allow_pointless=True)
 
133
        self.assertEquals(b.revno(), 2)
 
134
 
 
135
    def test_selective_delete(self):
 
136
        """Selective commit in tree with deletions"""
 
137
        wt = self.make_branch_and_tree('.')
 
138
        b = wt.branch
 
139
        file('hello', 'w').write('hello')
 
140
        file('buongia', 'w').write('buongia')
 
141
        wt.add(['hello', 'buongia'],
 
142
              ['hello-id', 'buongia-id'])
 
143
        wt.commit(message='add files',
 
144
                 rev_id='test@rev-1')
 
145
        
 
146
        os.remove('hello')
 
147
        file('buongia', 'w').write('new text')
 
148
        wt.commit(message='update text',
 
149
                 specific_files=['buongia'],
 
150
                 allow_pointless=False,
 
151
                 rev_id='test@rev-2')
 
152
 
 
153
        wt.commit(message='remove hello',
 
154
                 specific_files=['hello'],
 
155
                 allow_pointless=False,
 
156
                 rev_id='test@rev-3')
 
157
 
 
158
        eq = self.assertEquals
 
159
        eq(b.revno(), 3)
 
160
 
 
161
        tree2 = b.repository.revision_tree('test@rev-2')
 
162
        self.assertTrue(tree2.has_filename('hello'))
 
163
        self.assertEquals(tree2.get_file_text('hello-id'), 'hello')
 
164
        self.assertEquals(tree2.get_file_text('buongia-id'), 'new text')
 
165
        
 
166
        tree3 = b.repository.revision_tree('test@rev-3')
 
167
        self.assertFalse(tree3.has_filename('hello'))
 
168
        self.assertEquals(tree3.get_file_text('buongia-id'), 'new text')
 
169
 
 
170
    def test_commit_rename(self):
 
171
        """Test commit of a revision where a file is renamed."""
 
172
        tree = self.make_branch_and_tree('.')
 
173
        b = tree.branch
 
174
        self.build_tree(['hello'], line_endings='binary')
 
175
        tree.add(['hello'], ['hello-id'])
 
176
        tree.commit(message='one', rev_id='test@rev-1', allow_pointless=False)
 
177
 
 
178
        tree.rename_one('hello', 'fruity')
 
179
        tree.commit(message='renamed', rev_id='test@rev-2', allow_pointless=False)
 
180
 
 
181
        eq = self.assertEquals
 
182
        tree1 = b.repository.revision_tree('test@rev-1')
 
183
        eq(tree1.id2path('hello-id'), 'hello')
 
184
        eq(tree1.get_file_text('hello-id'), 'contents of hello\n')
 
185
        self.assertFalse(tree1.has_filename('fruity'))
 
186
        self.check_inventory_shape(tree1.inventory, ['hello'])
 
187
        ie = tree1.inventory['hello-id']
 
188
        eq(ie.revision, 'test@rev-1')
 
189
 
 
190
        tree2 = b.repository.revision_tree('test@rev-2')
 
191
        eq(tree2.id2path('hello-id'), 'fruity')
 
192
        eq(tree2.get_file_text('hello-id'), 'contents of hello\n')
 
193
        self.check_inventory_shape(tree2.inventory, ['fruity'])
 
194
        ie = tree2.inventory['hello-id']
 
195
        eq(ie.revision, 'test@rev-2')
 
196
 
 
197
    def test_reused_rev_id(self):
 
198
        """Test that a revision id cannot be reused in a branch"""
 
199
        wt = self.make_branch_and_tree('.')
 
200
        b = wt.branch
 
201
        wt.commit('initial', rev_id='test@rev-1', allow_pointless=True)
 
202
        self.assertRaises(Exception,
 
203
                          wt.commit,
 
204
                          message='reused id',
 
205
                          rev_id='test@rev-1',
 
206
                          allow_pointless=True)
 
207
 
 
208
    def test_commit_move(self):
 
209
        """Test commit of revisions with moved files and directories"""
 
210
        eq = self.assertEquals
 
211
        wt = self.make_branch_and_tree('.')
 
212
        b = wt.branch
 
213
        r1 = 'test@rev-1'
 
214
        self.build_tree(['hello', 'a/', 'b/'])
 
215
        wt.add(['hello', 'a', 'b'], ['hello-id', 'a-id', 'b-id'])
 
216
        wt.commit('initial', rev_id=r1, allow_pointless=False)
 
217
        wt.move(['hello'], 'a')
 
218
        r2 = 'test@rev-2'
 
219
        wt.commit('two', rev_id=r2, allow_pointless=False)
 
220
        self.check_inventory_shape(wt.read_working_inventory(),
 
221
                                   ['a', 'a/hello', 'b'])
 
222
 
 
223
        wt.move(['b'], 'a')
 
224
        r3 = 'test@rev-3'
 
225
        wt.commit('three', rev_id=r3, allow_pointless=False)
 
226
        self.check_inventory_shape(wt.read_working_inventory(),
 
227
                                   ['a', 'a/hello', 'a/b'])
 
228
        self.check_inventory_shape(b.repository.get_revision_inventory(r3),
 
229
                                   ['a', 'a/hello', 'a/b'])
 
230
 
 
231
        wt.move(['a/hello'], 'a/b')
 
232
        r4 = 'test@rev-4'
 
233
        wt.commit('four', rev_id=r4, allow_pointless=False)
 
234
        self.check_inventory_shape(wt.read_working_inventory(),
 
235
                                   ['a', 'a/b/hello', 'a/b'])
 
236
 
 
237
        inv = b.repository.get_revision_inventory(r4)
 
238
        eq(inv['hello-id'].revision, r4)
 
239
        eq(inv['a-id'].revision, r1)
 
240
        eq(inv['b-id'].revision, r3)
 
241
        
 
242
    def test_removed_commit(self):
 
243
        """Commit with a removed file"""
 
244
        wt = self.make_branch_and_tree('.')
 
245
        b = wt.branch
 
246
        file('hello', 'w').write('hello world')
 
247
        wt.add(['hello'], ['hello-id'])
 
248
        wt.commit(message='add hello')
 
249
        wt.remove('hello')
 
250
        wt.commit('removed hello', rev_id='rev2')
 
251
 
 
252
        tree = b.repository.revision_tree('rev2')
 
253
        self.assertFalse(tree.has_id('hello-id'))
 
254
 
 
255
    def test_committed_ancestry(self):
 
256
        """Test commit appends revisions to ancestry."""
 
257
        wt = self.make_branch_and_tree('.')
 
258
        b = wt.branch
 
259
        rev_ids = []
 
260
        for i in range(4):
 
261
            file('hello', 'w').write((str(i) * 4) + '\n')
 
262
            if i == 0:
 
263
                wt.add(['hello'], ['hello-id'])
 
264
            rev_id = 'test@rev-%d' % (i+1)
 
265
            rev_ids.append(rev_id)
 
266
            wt.commit(message='rev %d' % (i+1),
 
267
                     rev_id=rev_id)
 
268
        eq = self.assertEquals
 
269
        eq(b.revision_history(), rev_ids)
 
270
        for i in range(4):
 
271
            anc = b.repository.get_ancestry(rev_ids[i])
 
272
            eq(anc, [None] + rev_ids[:i+1])
 
273
 
 
274
    def test_commit_new_subdir_child_selective(self):
 
275
        wt = self.make_branch_and_tree('.')
 
276
        b = wt.branch
 
277
        self.build_tree(['dir/', 'dir/file1', 'dir/file2'])
 
278
        wt.add(['dir', 'dir/file1', 'dir/file2'],
 
279
              ['dirid', 'file1id', 'file2id'])
 
280
        wt.commit('dir/file1', specific_files=['dir/file1'], rev_id='1')
 
281
        inv = b.repository.get_inventory('1')
 
282
        self.assertEqual('1', inv['dirid'].revision)
 
283
        self.assertEqual('1', inv['file1id'].revision)
 
284
        # FIXME: This should raise a KeyError I think, rbc20051006
 
285
        self.assertRaises(BzrError, inv.__getitem__, 'file2id')
 
286
 
 
287
    def test_strict_commit(self):
 
288
        """Try and commit with unknown files and strict = True, should fail."""
 
289
        from bzrlib.errors import StrictCommitFailed
 
290
        wt = self.make_branch_and_tree('.')
 
291
        b = wt.branch
 
292
        file('hello', 'w').write('hello world')
 
293
        wt.add('hello')
 
294
        file('goodbye', 'w').write('goodbye cruel world!')
 
295
        self.assertRaises(StrictCommitFailed, wt.commit,
 
296
            message='add hello but not goodbye', strict=True)
 
297
 
 
298
    def test_strict_commit_without_unknowns(self):
 
299
        """Try and commit with no unknown files and strict = True,
 
300
        should work."""
 
301
        from bzrlib.errors import StrictCommitFailed
 
302
        wt = self.make_branch_and_tree('.')
 
303
        b = wt.branch
 
304
        file('hello', 'w').write('hello world')
 
305
        wt.add('hello')
 
306
        wt.commit(message='add hello', strict=True)
 
307
 
 
308
    def test_nonstrict_commit(self):
 
309
        """Try and commit with unknown files and strict = False, should work."""
 
310
        wt = self.make_branch_and_tree('.')
 
311
        b = wt.branch
 
312
        file('hello', 'w').write('hello world')
 
313
        wt.add('hello')
 
314
        file('goodbye', 'w').write('goodbye cruel world!')
 
315
        wt.commit(message='add hello but not goodbye', strict=False)
 
316
 
 
317
    def test_nonstrict_commit_without_unknowns(self):
 
318
        """Try and commit with no unknown files and strict = False,
 
319
        should work."""
 
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=False)
 
325
 
 
326
    def test_signed_commit(self):
 
327
        import bzrlib.gpg
 
328
        import bzrlib.commit as commit
 
329
        oldstrategy = bzrlib.gpg.GPGStrategy
 
330
        wt = self.make_branch_and_tree('.')
 
331
        branch = wt.branch
 
332
        wt.commit("base", allow_pointless=True, rev_id='A')
 
333
        self.failIf(branch.repository.has_signature_for_revision_id('A'))
 
334
        try:
 
335
            from bzrlib.testament import Testament
 
336
            # monkey patch gpg signing mechanism
 
337
            bzrlib.gpg.GPGStrategy = bzrlib.gpg.LoopbackGPGStrategy
 
338
            commit.Commit(config=MustSignConfig(branch)).commit(message="base",
 
339
                                                      allow_pointless=True,
 
340
                                                      rev_id='B',
 
341
                                                      working_tree=wt)
 
342
            self.assertEqual(Testament.from_revision(branch.repository,
 
343
                             'B').as_short_text(),
 
344
                             branch.repository.get_signature_text('B'))
 
345
        finally:
 
346
            bzrlib.gpg.GPGStrategy = oldstrategy
 
347
 
 
348
    def test_commit_failed_signature(self):
 
349
        import bzrlib.gpg
 
350
        import bzrlib.commit as commit
 
351
        oldstrategy = bzrlib.gpg.GPGStrategy
 
352
        wt = self.make_branch_and_tree('.')
 
353
        branch = wt.branch
 
354
        wt.commit("base", allow_pointless=True, rev_id='A')
 
355
        self.failIf(branch.repository.has_signature_for_revision_id('A'))
 
356
        try:
 
357
            from bzrlib.testament import Testament
 
358
            # monkey patch gpg signing mechanism
 
359
            bzrlib.gpg.GPGStrategy = bzrlib.gpg.DisabledGPGStrategy
 
360
            config = MustSignConfig(branch)
 
361
            self.assertRaises(SigningFailed,
 
362
                              commit.Commit(config=config).commit,
 
363
                              message="base",
 
364
                              allow_pointless=True,
 
365
                              rev_id='B',
 
366
                              working_tree=wt)
 
367
            branch = Branch.open(self.get_url('.'))
 
368
            self.assertEqual(branch.revision_history(), ['A'])
 
369
            self.failIf(branch.repository.has_revision('B'))
 
370
        finally:
 
371
            bzrlib.gpg.GPGStrategy = oldstrategy
 
372
 
 
373
    def test_commit_invokes_hooks(self):
 
374
        import bzrlib.commit as commit
 
375
        wt = self.make_branch_and_tree('.')
 
376
        branch = wt.branch
 
377
        calls = []
 
378
        def called(branch, rev_id):
 
379
            calls.append('called')
 
380
        bzrlib.ahook = called
 
381
        try:
 
382
            config = BranchWithHooks(branch)
 
383
            commit.Commit(config=config).commit(
 
384
                            message = "base",
 
385
                            allow_pointless=True,
 
386
                            rev_id='A', working_tree = wt)
 
387
            self.assertEqual(['called', 'called'], calls)
 
388
        finally:
 
389
            del bzrlib.ahook
 
390
 
 
391
    def test_commit_object_doesnt_set_nick(self):
 
392
        # using the Commit object directly does not set the branch nick.
 
393
        wt = self.make_branch_and_tree('.')
 
394
        c = Commit()
 
395
        c.commit(working_tree=wt, message='empty tree', allow_pointless=True)
 
396
        self.assertEquals(wt.branch.revno(), 1)
 
397
        self.assertEqual({},
 
398
                         wt.branch.repository.get_revision(
 
399
                            wt.branch.last_revision()).properties)
 
400
 
 
401
    def test_safe_master_lock(self):
 
402
        os.mkdir('master')
 
403
        master = BzrDirMetaFormat1().initialize('master')
 
404
        master.create_repository()
 
405
        master_branch = master.create_branch()
 
406
        master.create_workingtree()
 
407
        bound = master.sprout('bound')
 
408
        wt = bound.open_workingtree()
 
409
        wt.branch.set_bound_location(os.path.realpath('master'))
 
410
        master_branch.lock_write()
 
411
        try:
 
412
            self.assertRaises(LockContention, wt.commit, 'silly')
 
413
        finally:
 
414
            master_branch.unlock()
 
415
 
 
416
    def test_commit_bound_merge(self):
 
417
        # see bug #43959; commit of a merge in a bound branch fails to push
 
418
        # the new commit into the master
 
419
        master_branch = self.make_branch('master')
 
420
        bound_tree = self.make_branch_and_tree('bound')
 
421
        bound_tree.branch.bind(master_branch)
 
422
 
 
423
        self.build_tree_contents([('bound/content_file', 'initial contents\n')])
 
424
        bound_tree.add(['content_file'])
 
425
        bound_tree.commit(message='woo!')
 
426
 
 
427
        other_bzrdir = master_branch.bzrdir.sprout('other')
 
428
        other_tree = other_bzrdir.open_workingtree()
 
429
 
 
430
        # do a commit to the the other branch changing the content file so
 
431
        # that our commit after merging will have a merged revision in the
 
432
        # content file history.
 
433
        self.build_tree_contents([('other/content_file', 'change in other\n')])
 
434
        other_tree.commit('change in other')
 
435
 
 
436
        # do a merge into the bound branch from other, and then change the
 
437
        # content file locally to force a new revision (rather than using the
 
438
        # revision from other). This forces extra processing in commit.
 
439
        self.merge(other_tree.branch, bound_tree)
 
440
        self.build_tree_contents([('bound/content_file', 'change in bound\n')])
 
441
 
 
442
        # before #34959 was fixed, this failed with 'revision not present in
 
443
        # weave' when trying to implicitly push from the bound branch to the master
 
444
        bound_tree.commit(message='commit of merge in bound tree')
 
445
 
 
446
    def test_commit_reporting_after_merge(self):
 
447
        # when doing a commit of a merge, the reporter needs to still 
 
448
        # be called for each item that is added/removed/deleted.
 
449
        this_tree = self.make_branch_and_tree('this')
 
450
        # we need a bunch of files and dirs, to perform one action on each.
 
451
        self.build_tree([
 
452
            'this/dirtorename/',
 
453
            'this/dirtoreparent/',
 
454
            'this/dirtoleave/',
 
455
            'this/dirtoremove/',
 
456
            'this/filetoreparent',
 
457
            'this/filetorename',
 
458
            'this/filetomodify',
 
459
            'this/filetoremove',
 
460
            'this/filetoleave']
 
461
            )
 
462
        this_tree.add([
 
463
            'dirtorename',
 
464
            'dirtoreparent',
 
465
            'dirtoleave',
 
466
            'dirtoremove',
 
467
            'filetoreparent',
 
468
            'filetorename',
 
469
            'filetomodify',
 
470
            'filetoremove',
 
471
            'filetoleave']
 
472
            )
 
473
        this_tree.commit('create_files')
 
474
        other_dir = this_tree.bzrdir.sprout('other')
 
475
        other_tree = other_dir.open_workingtree()
 
476
        other_tree.lock_write()
 
477
        # perform the needed actions on the files and dirs.
 
478
        try:
 
479
            other_tree.rename_one('dirtorename', 'renameddir')
 
480
            other_tree.rename_one('dirtoreparent', 'renameddir/reparenteddir')
 
481
            other_tree.rename_one('filetorename', 'renamedfile')
 
482
            other_tree.rename_one('filetoreparent', 'renameddir/reparentedfile')
 
483
            other_tree.remove(['dirtoremove', 'filetoremove'])
 
484
            self.build_tree_contents([
 
485
                ('other/newdir/', ),
 
486
                ('other/filetomodify', 'new content'),
 
487
                ('other/newfile', 'new file content')])
 
488
            other_tree.add('newfile')
 
489
            other_tree.add('newdir/')
 
490
            other_tree.commit('modify all sample files and dirs.')
 
491
        finally:
 
492
            other_tree.unlock()
 
493
        self.merge(other_tree.branch, this_tree)
 
494
        reporter = CapturingReporter()
 
495
        this_tree.commit('do the commit', reporter=reporter)
 
496
        self.assertEqual([
 
497
            ('change', 'unchanged', 'dirtoleave'),
 
498
            ('change', 'unchanged', 'filetoleave'),
 
499
            ('change', 'modified', 'filetomodify'),
 
500
            ('change', 'added', 'newdir'),
 
501
            ('change', 'added', 'newfile'),
 
502
            ('renamed', 'renamed', 'dirtorename', 'renameddir'),
 
503
            ('renamed', 'renamed', 'dirtoreparent', 'renameddir/reparenteddir'),
 
504
            ('renamed', 'renamed', 'filetoreparent', 'renameddir/reparentedfile'),
 
505
            ('renamed', 'renamed', 'filetorename', 'renamedfile'),
 
506
            ('deleted', 'dirtoremove'),
 
507
            ('deleted', 'filetoremove'),
 
508
            ],
 
509
            reporter.calls)