1
# Copyright (C) 2004, 2005, 2006 by Canonical Ltd
1
# Copyright (C) 2004, 2005, 2006, 2007 Canonical Ltd
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
5
5
# the Free Software Foundation; either version 2 of the License, or
6
6
# (at your option) any later version.
8
8
# This program is distributed in the hope that it will be useful,
9
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
11
# GNU General Public License for more details.
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
20
27
from bzrlib.builtins import merge
21
from bzrlib.branch import Branch
22
from bzrlib.tests import TestCaseWithTransport
23
from bzrlib.errors import NoCommonAncestor, NoCommits
24
from bzrlib.errors import NoSuchRevision
25
from bzrlib.revisionspec import RevisionSpec
28
class TestRevisionNamespaces(TestCaseWithTransport):
30
def test_revision_namespaces(self):
31
"""Test revision specifiers.
33
These identify revisions by date, etc."""
34
wt = self.make_branch_and_tree('.')
37
wt.commit('Commit one', rev_id='a@r-0-1', timestamp=time.time() - 60*60*24)
38
wt.commit('Commit two', rev_id='a@r-0-2')
39
wt.commit('Commit three', rev_id='a@r-0-3')
41
self.assertEquals(RevisionSpec(None).in_history(b), (0, None))
42
self.assertEquals(RevisionSpec(1).in_history(b), (1, 'a@r-0-1'))
43
self.assertEquals(RevisionSpec('revno:1').in_history(b),
45
self.assertEquals(RevisionSpec('revid:a@r-0-1').in_history(b),
47
self.assertRaises(NoSuchRevision,
48
RevisionSpec('revid:a@r-0-0').in_history, b)
49
self.assertRaises(TypeError, RevisionSpec, object)
51
self.assertEquals(RevisionSpec('date:today').in_history(b),
53
self.assertRaises(NoSuchRevision,
54
RevisionSpec('date:tomorrow').in_history, b)
55
self.assertEquals(RevisionSpec('date:yesterday').in_history(b),
57
self.assertEquals(RevisionSpec('before:date:today').in_history(b),
60
self.assertEquals(RevisionSpec('last:1').in_history(b),
62
self.assertEquals(RevisionSpec('-1').in_history(b), (3, 'a@r-0-3'))
63
# self.assertEquals(b.get_revision_info('last:1'), (3, 'a@r-0-3'))
64
# self.assertEquals(b.get_revision_info('-1'), (3, 'a@r-0-3'))
66
self.assertEquals(RevisionSpec('ancestor:.').in_history(b).rev_id,
70
wt2 = self.make_branch_and_tree('newbranch')
72
self.assertRaises(NoCommits, RevisionSpec('ancestor:.').in_history, b2)
74
d3 = b.bzrdir.sprout('copy')
76
wt3 = d3.open_workingtree()
77
wt3.commit('Commit four', rev_id='b@r-0-4')
78
self.assertEquals(RevisionSpec('ancestor:.').in_history(b3).rev_id,
80
merge(['copy', -1], [None, None])
81
wt.commit('Commit five', rev_id='a@r-0-4')
82
self.assertEquals(RevisionSpec('ancestor:copy').in_history(b).rev_id,
84
self.assertEquals(RevisionSpec('ancestor:.').in_history(b3).rev_id,
87
# This should be in the revision store, but not in revision-history
88
self.assertEquals((None, 'b@r-0-4'),
89
RevisionSpec('revid:b@r-0-4').in_history(b))
91
def test_branch_namespace(self):
92
"""Ensure that the branch namespace pulls in the requisite content."""
93
self.build_tree(['branch1/', 'branch1/file', 'branch2/'])
94
wt = self.make_branch_and_tree('branch1')
98
d2 = branch.bzrdir.sprout('branch2')
99
print >> open('branch2/file', 'w'), 'new content'
100
branch2 = d2.open_branch()
101
d2.open_workingtree().commit('update file', rev_id='A')
102
spec = RevisionSpec('branch:./branch2/.bzr/../')
103
rev_info = spec.in_history(branch)
104
self.assertEqual(rev_info, (None, 'A'))
28
from bzrlib.tests import TestCase, TestCaseWithTransport
29
from bzrlib.revisionspec import (
36
def spec_in_history(spec, branch):
37
"""A simple helper to change a revision spec into a branch search"""
38
return RevisionSpec.from_string(spec).in_history(branch)
41
# Basic class, which just creates a really basic set of revisions
42
class TestRevisionSpec(TestCaseWithTransport):
45
super(TestRevisionSpec, self).setUp()
46
# this sets up a revision graph:
51
self.tree = self.make_branch_and_tree('tree')
52
self.build_tree(['tree/a'])
54
self.tree.commit('a', rev_id='r1')
56
self.tree2 = self.tree.bzrdir.sprout('tree2').open_workingtree()
57
self.tree2.commit('alt', rev_id='alt_r2')
59
self.tree.branch.repository.fetch(self.tree2.branch.repository,
61
self.tree.set_pending_merges(['alt_r2'])
62
self.tree.commit('second', rev_id='r2')
64
def get_in_history(self, revision_spec):
65
return spec_in_history(revision_spec, self.tree.branch)
67
def assertInHistoryIs(self, exp_revno, exp_revision_id, revision_spec):
68
rev_info = self.get_in_history(revision_spec)
69
self.assertEqual(exp_revno, rev_info.revno,
70
'Revision spec: %r returned wrong revno: %r != %r'
71
% (revision_spec, exp_revno, rev_info.revno))
72
self.assertEqual(exp_revision_id, rev_info.rev_id,
73
'Revision spec: %r returned wrong revision id:'
75
% (revision_spec, exp_revision_id, rev_info.rev_id))
77
def assertInvalid(self, revision_spec, extra=''):
79
self.get_in_history(revision_spec)
80
except errors.InvalidRevisionSpec, e:
81
self.assertEqual(revision_spec, e.spec)
82
self.assertEqual(extra, e.extra)
84
self.fail('Expected InvalidRevisionSpec to be raised for %s'
88
class TestOddRevisionSpec(TestRevisionSpec):
89
"""Test things that aren't normally thought of as revision specs"""
92
self.assertInHistoryIs(0, None, None)
94
def test_object(self):
95
self.assertRaises(TypeError, RevisionSpec.from_string, object())
97
def test_unregistered_spec(self):
98
self.assertRaises(errors.NoSuchRevisionSpec,
99
RevisionSpec.from_string, 'foo')
100
self.assertRaises(errors.NoSuchRevisionSpec,
101
RevisionSpec.from_string, '123a')
105
class TestRevnoFromString(TestCase):
107
def test_from_string_dotted_decimal(self):
108
self.assertRaises(errors.NoSuchRevisionSpec, RevisionSpec.from_string, '-1.1')
109
self.assertRaises(errors.NoSuchRevisionSpec, RevisionSpec.from_string, '.1')
110
self.assertRaises(errors.NoSuchRevisionSpec, RevisionSpec.from_string, '1..1')
111
self.assertRaises(errors.NoSuchRevisionSpec, RevisionSpec.from_string, '1.2..1')
112
self.assertRaises(errors.NoSuchRevisionSpec, RevisionSpec.from_string, '1.')
113
self.assertIsInstance(RevisionSpec.from_string('1.1'), RevisionSpec_revno)
114
self.assertIsInstance(RevisionSpec.from_string('1.1.3'), RevisionSpec_revno)
117
class TestRevisionSpec_revno(TestRevisionSpec):
119
def test_positive_int(self):
120
self.assertInHistoryIs(0, None, '0')
121
self.assertInHistoryIs(1, 'r1', '1')
122
self.assertInHistoryIs(2, 'r2', '2')
123
self.assertInvalid('3')
125
def test_dotted_decimal(self):
126
self.assertInHistoryIs(None, 'alt_r2', '1.1.1')
128
def test_negative_int(self):
129
self.assertInHistoryIs(2, 'r2', '-1')
130
self.assertInHistoryIs(1, 'r1', '-2')
132
self.assertInHistoryIs(1, 'r1', '-3')
133
self.assertInHistoryIs(1, 'r1', '-4')
134
self.assertInHistoryIs(1, 'r1', '-100')
136
def test_positive(self):
137
self.assertInHistoryIs(0, None, 'revno:0')
138
self.assertInHistoryIs(1, 'r1', 'revno:1')
139
self.assertInHistoryIs(2, 'r2', 'revno:2')
141
self.assertInvalid('revno:3')
143
def test_negative(self):
144
self.assertInHistoryIs(2, 'r2', 'revno:-1')
145
self.assertInHistoryIs(1, 'r1', 'revno:-2')
147
self.assertInHistoryIs(1, 'r1', 'revno:-3')
148
self.assertInHistoryIs(1, 'r1', 'revno:-4')
150
def test_invalid_number(self):
151
# Get the right exception text
154
except ValueError, e:
156
self.assertInvalid('revno:X', extra='\n' + str(e))
158
def test_missing_number_and_branch(self):
159
self.assertInvalid('revno::',
160
extra='\ncannot have an empty revno and no branch')
162
def test_invalid_number_with_branch(self):
165
except ValueError, e:
167
self.assertInvalid('revno:X:tree2', extra='\n' + str(e))
169
def test_non_exact_branch(self):
170
# It seems better to require an exact path to the branch
171
# Branch.open() rather than using Branch.open_containing()
172
spec = RevisionSpec.from_string('revno:2:tree2/a')
173
self.assertRaises(errors.NotBranchError,
174
spec.in_history, self.tree.branch)
176
def test_with_branch(self):
177
# Passing a URL overrides the supplied branch path
178
revinfo = self.get_in_history('revno:2:tree2')
179
self.assertNotEqual(self.tree.branch.base, revinfo.branch.base)
180
self.assertEqual(self.tree2.branch.base, revinfo.branch.base)
181
self.assertEqual(2, revinfo.revno)
182
self.assertEqual('alt_r2', revinfo.rev_id)
184
def test_int_with_branch(self):
185
revinfo = self.get_in_history('2:tree2')
186
self.assertNotEqual(self.tree.branch.base, revinfo.branch.base)
187
self.assertEqual(self.tree2.branch.base, revinfo.branch.base)
188
self.assertEqual(2, revinfo.revno)
189
self.assertEqual('alt_r2', revinfo.rev_id)
191
def test_with_url(self):
192
url = self.get_url() + '/tree2'
193
revinfo = self.get_in_history('revno:2:%s' % (url,))
194
self.assertNotEqual(self.tree.branch.base, revinfo.branch.base)
195
self.assertEqual(self.tree2.branch.base, revinfo.branch.base)
196
self.assertEqual(2, revinfo.revno)
197
self.assertEqual('alt_r2', revinfo.rev_id)
199
def test_negative_with_url(self):
200
url = self.get_url() + '/tree2'
201
revinfo = self.get_in_history('revno:-1:%s' % (url,))
202
self.assertNotEqual(self.tree.branch.base, revinfo.branch.base)
203
self.assertEqual(self.tree2.branch.base, revinfo.branch.base)
204
self.assertEqual(2, revinfo.revno)
205
self.assertEqual('alt_r2', revinfo.rev_id)
207
def test_different_history_lengths(self):
208
# Make sure we use the revisions and offsets in the supplied branch
209
# not the ones in the original branch.
210
self.tree2.commit('three', rev_id='r3')
211
self.assertInHistoryIs(3, 'r3', 'revno:3:tree2')
212
self.assertInHistoryIs(3, 'r3', 'revno:-1:tree2')
214
def test_invalid_branch(self):
215
self.assertRaises(errors.NotBranchError,
216
self.get_in_history, 'revno:-1:tree3')
218
def test_invalid_revno_in_branch(self):
219
self.tree.commit('three', rev_id='r3')
220
self.assertInvalid('revno:3:tree2')
222
def test_revno_n_path(self):
223
"""Old revno:N:path tests"""
224
wta = self.make_branch_and_tree('a')
227
wta.commit('Commit one', rev_id='a@r-0-1')
228
wta.commit('Commit two', rev_id='a@r-0-2')
229
wta.commit('Commit three', rev_id='a@r-0-3')
231
wtb = self.make_branch_and_tree('b')
234
wtb.commit('Commit one', rev_id='b@r-0-1')
235
wtb.commit('Commit two', rev_id='b@r-0-2')
236
wtb.commit('Commit three', rev_id='b@r-0-3')
239
self.assertEqual((1, 'a@r-0-1'),
240
spec_in_history('revno:1:a/', ba))
241
# The argument of in_history should be ignored since it is
242
# redundant with the path in the spec.
243
self.assertEqual((1, 'a@r-0-1'),
244
spec_in_history('revno:1:a/', None))
245
self.assertEqual((1, 'a@r-0-1'),
246
spec_in_history('revno:1:a/', bb))
247
self.assertEqual((2, 'b@r-0-2'),
248
spec_in_history('revno:2:b/', None))
252
class TestRevisionSpec_revid(TestRevisionSpec):
254
def test_in_history(self):
255
# We should be able to access revisions that are directly
257
self.assertInHistoryIs(1, 'r1', 'revid:r1')
258
self.assertInHistoryIs(2, 'r2', 'revid:r2')
260
def test_missing(self):
261
self.assertInvalid('revid:r3')
263
def test_merged(self):
264
"""We can reach revisions in the ancestry"""
265
self.assertInHistoryIs(None, 'alt_r2', 'revid:alt_r2')
267
def test_not_here(self):
268
self.tree2.commit('alt third', rev_id='alt_r3')
269
# It exists in tree2, but not in tree
270
self.assertInvalid('revid:alt_r3')
272
def test_in_repository(self):
273
"""We can get any revision id in the repository"""
274
# XXX: This may change in the future, but for now, it is true
275
self.tree2.commit('alt third', rev_id='alt_r3')
276
self.tree.branch.repository.fetch(self.tree2.branch.repository,
277
revision_id='alt_r3')
278
self.assertInHistoryIs(None, 'alt_r3', 'revid:alt_r3')
280
def test_unicode(self):
281
"""We correctly convert a unicode ui string to an encoded revid."""
282
revision_id = u'\N{SNOWMAN}'.encode('utf-8')
283
self.tree.commit('unicode', rev_id=revision_id)
284
self.assertInHistoryIs(3, revision_id, u'revid:\N{SNOWMAN}')
285
self.assertInHistoryIs(3, revision_id, 'revid:' + revision_id)
288
class TestRevisionSpec_last(TestRevisionSpec):
290
def test_positive(self):
291
self.assertInHistoryIs(2, 'r2', 'last:1')
292
self.assertInHistoryIs(1, 'r1', 'last:2')
293
self.assertInHistoryIs(0, None, 'last:3')
295
def test_empty(self):
296
self.assertInHistoryIs(2, 'r2', 'last:')
298
def test_negative(self):
299
self.assertInvalid('last:-1',
300
extra='\nyou must supply a positive value')
302
def test_missing(self):
303
self.assertInvalid('last:4')
305
def test_no_history(self):
306
tree = self.make_branch_and_tree('tree3')
308
self.assertRaises(errors.NoCommits,
309
spec_in_history, 'last:', tree.branch)
311
def test_not_a_number(self):
314
except ValueError, e:
316
self.assertInvalid('last:Y', extra='\n' + str(e))
319
class TestRevisionSpec_before(TestRevisionSpec):
322
self.assertInHistoryIs(1, 'r1', 'before:2')
323
self.assertInHistoryIs(1, 'r1', 'before:-1')
325
def test_before_one(self):
326
self.assertInHistoryIs(0, None, 'before:1')
328
def test_before_none(self):
329
self.assertInvalid('before:0',
330
extra='\ncannot go before the null: revision')
332
def test_revid(self):
333
self.assertInHistoryIs(1, 'r1', 'before:revid:r2')
336
self.assertInHistoryIs(1, 'r1', 'before:last:1')
338
def test_alt_revid(self):
339
# This will grab the left-most ancestor for alternate histories
340
self.assertInHistoryIs(1, 'r1', 'before:revid:alt_r2')
342
def test_alt_no_parents(self):
343
new_tree = self.make_branch_and_tree('new_tree')
344
new_tree.commit('first', rev_id='new_r1')
345
self.tree.branch.repository.fetch(new_tree.branch.repository,
346
revision_id='new_r1')
347
self.assertInHistoryIs(0, None, 'before:revid:new_r1')
350
class TestRevisionSpec_tag(TestRevisionSpec):
352
def make_branch_and_tree(self, relpath):
353
# override format as the default one may not support tags
354
control = bzrdir.BzrDir.create(relpath)
355
control.create_repository()
356
branch.BzrBranchExperimental.initialize(control)
357
return control.create_workingtree()
359
def test_from_string_tag(self):
360
spec = RevisionSpec.from_string('tag:bzr-0.14')
361
self.assertIsInstance(spec, RevisionSpec_tag)
362
self.assertEqual(spec.spec, 'bzr-0.14')
364
def test_lookup_tag(self):
365
self.tree.branch.tags.set_tag('bzr-0.14', 'r1')
366
self.assertInHistoryIs(1, 'r1', 'tag:bzr-0.14')
368
def test_failed_lookup(self):
369
# tags that don't exist give a specific message: arguably we should
370
# just give InvalidRevisionSpec but I think this is more helpful
371
self.assertRaises(errors.NoSuchTag,
373
'tag:some-random-tag')
376
class TestRevisionSpec_date(TestRevisionSpec):
379
super(TestRevisionSpec, self).setUp()
381
new_tree = self.make_branch_and_tree('new_tree')
382
new_tree.commit('Commit one', rev_id='new_r1',
383
timestamp=time.time() - 60*60*24)
384
new_tree.commit('Commit two', rev_id='new_r2')
385
new_tree.commit('Commit three', rev_id='new_r3')
389
def test_tomorrow(self):
390
self.assertInvalid('date:tomorrow')
392
def test_today(self):
393
self.assertInHistoryIs(2, 'new_r2', 'date:today')
394
self.assertInHistoryIs(1, 'new_r1', 'before:date:today')
396
def test_yesterday(self):
397
self.assertInHistoryIs(1, 'new_r1', 'date:yesterday')
399
def test_invalid(self):
400
self.assertInvalid('date:foobar', extra='\ninvalid date')
401
# You must have '-' between year/month/day
402
self.assertInvalid('date:20040404', extra='\ninvalid date')
403
# Need 2 digits for each date piece
404
self.assertInvalid('date:2004-4-4', extra='\ninvalid date')
407
now = datetime.datetime.now()
408
self.assertInHistoryIs(2, 'new_r2',
409
'date:%04d-%02d-%02d' % (now.year, now.month, now.day))
412
class TestRevisionSpec_ancestor(TestRevisionSpec):
414
def test_non_exact_branch(self):
415
# It seems better to require an exact path to the branch
416
# Branch.open() rather than using Branch.open_containing()
417
self.assertRaises(errors.NotBranchError,
418
self.get_in_history, 'ancestor:tree2/a')
420
def test_simple(self):
421
# Common ancestor of trees is 'alt_r2'
422
self.assertInHistoryIs(None, 'alt_r2', 'ancestor:tree2')
424
# Going the other way, we get a valid revno
426
self.tree = self.tree2
428
self.assertInHistoryIs(2, 'alt_r2', 'ancestor:tree')
431
self.assertInHistoryIs(2, 'r2', 'ancestor:tree')
433
def test_unrelated(self):
434
new_tree = self.make_branch_and_tree('new_tree')
436
new_tree.commit('Commit one', rev_id='new_r1')
437
new_tree.commit('Commit two', rev_id='new_r2')
438
new_tree.commit('Commit three', rev_id='new_r3')
440
# With no common ancestor, we should raise another user error
441
self.assertRaises(errors.NoCommonAncestor,
442
self.get_in_history, 'ancestor:new_tree')
444
def test_no_commits(self):
445
new_tree = self.make_branch_and_tree('new_tree')
446
self.assertRaises(errors.NoCommits,
447
spec_in_history, 'ancestor:new_tree',
450
self.assertRaises(errors.NoCommits,
451
spec_in_history, 'ancestor:tree',
455
class TestRevisionSpec_branch(TestRevisionSpec):
457
def test_non_exact_branch(self):
458
# It seems better to require an exact path to the branch
459
# Branch.open() rather than using Branch.open_containing()
460
self.assertRaises(errors.NotBranchError,
461
self.get_in_history, 'branch:tree2/a')
463
def test_simple(self):
464
self.assertInHistoryIs(None, 'alt_r2', 'branch:tree2')
467
self.assertInHistoryIs(2, 'r2', 'branch:tree')
469
def test_unrelated(self):
470
new_tree = self.make_branch_and_tree('new_tree')
472
new_tree.commit('Commit one', rev_id='new_r1')
473
new_tree.commit('Commit two', rev_id='new_r2')
474
new_tree.commit('Commit three', rev_id='new_r3')
476
self.assertInHistoryIs(None, 'new_r3', 'branch:new_tree')
478
# XXX: Right now, we use fetch() to make sure the remote revisions
479
# have been pulled into the local branch. We may change that
480
# behavior in the future.
481
self.failUnless(self.tree.branch.repository.has_revision('new_r3'))
483
def test_no_commits(self):
484
new_tree = self.make_branch_and_tree('new_tree')
485
self.assertRaises(errors.NoCommits,
486
self.get_in_history, 'branch:new_tree')
489
class TestRevisionSpec_submit(TestRevisionSpec):
491
def test_submit_branch(self):
492
# Common ancestor of trees is 'alt_r2'
493
self.assertRaises(errors.NoSubmitBranch, self.get_in_history,
495
self.tree.branch.set_parent('../tree2')
496
self.assertInHistoryIs(None, 'alt_r2', 'submit:')
497
self.tree.branch.set_parent('bogus')
498
self.assertRaises(errors.NotBranchError, self.get_in_history,
500
# submit branch overrides parent branch
501
self.tree.branch.set_submit_branch('tree2')
502
self.assertInHistoryIs(None, 'alt_r2', 'submit:')