~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: 2010-04-15 18:09:55 UTC
  • mfrom: (5159.1.1 integration)
  • Revision ID: pqm@pqm.ubuntu.com-20100415180955-v9jh960r07pva92c
(vila) Merge 2.1 into bzr.dev including fixes for #262450, #373898,
        #498409

Show diffs side-by-side

added added

removed removed

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