~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_commit.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2011-05-12 23:50:39 UTC
  • mfrom: (5844.2.2 bzr)
  • Revision ID: pqm@pqm.ubuntu.com-20110512235039-pj1gatuvy4jq415y
(jelmer) Move VersionedFiles-specific write group tests out of
 bzrlib.tests.per_repository into bzrlib.tests.per_repository_vf. (Jelmer
 Vernooij)

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