/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_commit.py

  • Committer: Aaron Bentley
  • Date: 2007-12-20 20:19:22 UTC
  • mto: This revision was merged to the branch mainline in revision 3235.
  • Revision ID: abentley@panoramicfeedback.com-20071220201922-r6a2vsx612x87yf6
Implement hard-linking for build_tree

Show diffs side-by-side

added added

removed removed

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