~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, 2006, 2008 Canonical Ltd
2
 
#
3
 
# This program is free software; you can redistribute it and/or modify
4
 
# it under the terms of the GNU General Public License as published by
5
 
# the Free Software Foundation; either version 2 of the License, or
6
 
# (at your option) any later version.
7
 
#
8
 
# This program is distributed in the hope that it will be useful,
9
 
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
 
# GNU General Public License for more details.
12
 
#
13
 
# You should have received a copy of the GNU General Public License
14
 
# along with this program; if not, write to the Free Software
15
 
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
 
 
17
 
 
18
 
import os
19
 
 
20
 
import bzrlib
21
 
from bzrlib 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_delete_commit(self):
111
 
        """Test a commit with a deleted 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_revision_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_revision_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 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
 
        self.assertEqual([
554
 
            ('change', 'unchanged', ''),
555
 
            ('change', 'unchanged', 'dirtoleave'),
556
 
            ('change', 'unchanged', 'filetoleave'),
557
 
            ('change', 'modified', 'filetomodify'),
558
 
            ('change', 'added', 'newdir'),
559
 
            ('change', 'added', 'newfile'),
560
 
            ('renamed', 'renamed', 'dirtorename', 'renameddir'),
561
 
            ('renamed', 'renamed', 'filetorename', 'renamedfile'),
562
 
            ('renamed', 'renamed', 'dirtoreparent', 'renameddir/reparenteddir'),
563
 
            ('renamed', 'renamed', 'filetoreparent', 'renameddir/reparentedfile'),
564
 
            ('deleted', 'dirtoremove'),
565
 
            ('deleted', 'filetoremove'),
566
 
            ],
567
 
            reporter.calls)
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):
712
 
            raise errors.NoSuchFile('foo')
713
 
        repository.add_inventory = raise_
714
 
        self.assertRaises(errors.NoSuchFile, tree.commit, message_callback=cb)
715
 
        self.assertFalse(cb.called)
716
 
 
717
 
    def test_selected_file_merge_commit(self):
718
 
        """Ensure the correct error is raised"""
719
 
        tree = self.make_branch_and_tree('foo')
720
 
        # pending merge would turn into a left parent
721
 
        tree.commit('commit 1')
722
 
        tree.add_parent_tree_id('example')
723
 
        self.build_tree(['foo/bar', 'foo/baz'])
724
 
        tree.add(['bar', 'baz'])
725
 
        err = self.assertRaises(errors.CannotCommitSelectedFileMerge,
726
 
            tree.commit, 'commit 2', specific_files=['bar', 'baz'])
727
 
        self.assertEqual(['bar', 'baz'], err.files)
728
 
        self.assertEqual('Selected-file commit of merges is not supported'
729
 
                         ' yet: files bar, baz', str(err))
730
 
 
731
 
    def test_commit_ordering(self):
732
 
        """Test of corner-case commit ordering error"""
733
 
        tree = self.make_branch_and_tree('.')
734
 
        self.build_tree(['a/', 'a/z/', 'a/c/', 'a/z/x', 'a/z/y'])
735
 
        tree.add(['a/', 'a/z/', 'a/c/', 'a/z/x', 'a/z/y'])
736
 
        tree.commit('setup')
737
 
        self.build_tree(['a/c/d/'])
738
 
        tree.add('a/c/d')
739
 
        tree.rename_one('a/z/x', 'a/c/d/x')
740
 
        tree.commit('test', specific_files=['a/z/y'])
741
 
 
742
 
    def test_commit_no_author(self):
743
 
        """The default kwarg author in MutableTree.commit should not add
744
 
        the 'author' revision property.
745
 
        """
746
 
        tree = self.make_branch_and_tree('foo')
747
 
        rev_id = tree.commit('commit 1')
748
 
        rev = tree.branch.repository.get_revision(rev_id)
749
 
        self.assertFalse('author' in rev.properties)
750
 
 
751
 
    def test_commit_author(self):
752
 
        """Passing a non-empty author kwarg to MutableTree.commit should add
753
 
        the 'author' revision property.
754
 
        """
755
 
        tree = self.make_branch_and_tree('foo')
756
 
        rev_id = 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['author'])
760
 
 
761
 
    def test_commit_with_checkout_and_branch_sharing_repo(self):
762
 
        repo = self.make_repository('repo', shared=True)
763
 
        # make_branch_and_tree ignores shared repos
764
 
        branch = bzrdir.BzrDir.create_branch_convenience('repo/branch')
765
 
        tree2 = branch.create_checkout('repo/tree2')
766
 
        tree2.commit('message', rev_id='rev1')
767
 
        self.assertTrue(tree2.branch.repository.has_revision('rev1'))