~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_commit.py

merge merge tweaks from aaron, which includes latest .dev

Show diffs side-by-side

added added

removed removed

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