1
# Copyright (C) 2004, 2005 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),
59
self.assertRaises(NoSuchRevision,
60
RevisionSpec('date:tomorrow').in_history, b)
62
self.assertEquals(RevisionSpec('last:1').in_history(b),
64
self.assertEquals(RevisionSpec('-1').in_history(b), (3, 'a@r-0-3'))
65
# self.assertEquals(b.get_revision_info('last:1'), (3, 'a@r-0-3'))
66
# self.assertEquals(b.get_revision_info('-1'), (3, 'a@r-0-3'))
68
self.assertEquals(RevisionSpec('ancestor:.').in_history(b).rev_id,
72
wt2 = self.make_branch_and_tree('newbranch')
74
self.assertRaises(NoCommits, RevisionSpec('ancestor:.').in_history, b2)
76
d3 = b.bzrdir.sprout('copy')
78
wt3 = d3.open_workingtree()
79
wt3.commit('Commit four', rev_id='b@r-0-4')
80
self.assertEquals(RevisionSpec('ancestor:.').in_history(b3).rev_id,
82
merge(['copy', -1], [None, None])
83
wt.commit('Commit five', rev_id='a@r-0-4')
84
self.assertEquals(RevisionSpec('ancestor:copy').in_history(b).rev_id,
86
self.assertEquals(RevisionSpec('ancestor:.').in_history(b3).rev_id,
89
# This should be in the revision store, but not in revision-history
90
self.assertEquals((None, 'b@r-0-4'),
91
RevisionSpec('revid:b@r-0-4').in_history(b))
93
def test_branch_namespace(self):
94
"""Ensure that the branch namespace pulls in the requisite content."""
95
self.build_tree(['branch1/', 'branch1/file', 'branch2/'])
96
wt = self.make_branch_and_tree('branch1')
100
d2 = branch.bzrdir.sprout('branch2')
101
print >> open('branch2/file', 'w'), 'new content'
102
branch2 = d2.open_branch()
103
d2.open_workingtree().commit('update file', rev_id='A')
104
spec = RevisionSpec('branch:./branch2/.bzr/../')
105
rev_info = spec.in_history(branch)
106
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: %s returned wrong revno: %s != %s'
71
% (revision_spec, exp_revno, rev_info.revno))
72
self.assertEqual(exp_revision_id, rev_info.rev_id,
73
'Revision spec: %s 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')
281
class TestRevisionSpec_last(TestRevisionSpec):
283
def test_positive(self):
284
self.assertInHistoryIs(2, 'r2', 'last:1')
285
self.assertInHistoryIs(1, 'r1', 'last:2')
286
self.assertInHistoryIs(0, None, 'last:3')
288
def test_empty(self):
289
self.assertInHistoryIs(2, 'r2', 'last:')
291
def test_negative(self):
292
self.assertInvalid('last:-1',
293
extra='\nyou must supply a positive value')
295
def test_missing(self):
296
self.assertInvalid('last:4')
298
def test_no_history(self):
299
tree = self.make_branch_and_tree('tree3')
301
self.assertRaises(errors.NoCommits,
302
spec_in_history, 'last:', tree.branch)
304
def test_not_a_number(self):
307
except ValueError, e:
309
self.assertInvalid('last:Y', extra='\n' + str(e))
312
class TestRevisionSpec_before(TestRevisionSpec):
315
self.assertInHistoryIs(1, 'r1', 'before:2')
316
self.assertInHistoryIs(1, 'r1', 'before:-1')
318
def test_before_one(self):
319
self.assertInHistoryIs(0, None, 'before:1')
321
def test_before_none(self):
322
self.assertInvalid('before:0',
323
extra='\ncannot go before the null: revision')
325
def test_revid(self):
326
self.assertInHistoryIs(1, 'r1', 'before:revid:r2')
329
self.assertInHistoryIs(1, 'r1', 'before:last:1')
331
def test_alt_revid(self):
332
# This will grab the left-most ancestor for alternate histories
333
self.assertInHistoryIs(1, 'r1', 'before:revid:alt_r2')
335
def test_alt_no_parents(self):
336
new_tree = self.make_branch_and_tree('new_tree')
337
new_tree.commit('first', rev_id='new_r1')
338
self.tree.branch.repository.fetch(new_tree.branch.repository,
339
revision_id='new_r1')
340
self.assertInHistoryIs(0, None, 'before:revid:new_r1')
343
class TestRevisionSpec_tag(TestRevisionSpec):
345
def make_branch_and_tree(self, relpath):
346
# override format as the default one may not support tags
347
control = bzrdir.BzrDir.create(relpath)
348
control.create_repository()
349
branch.BzrBranchExperimental.initialize(control)
350
return control.create_workingtree()
352
def test_from_string_tag(self):
353
spec = RevisionSpec.from_string('tag:bzr-0.14')
354
self.assertIsInstance(spec, RevisionSpec_tag)
355
self.assertEqual(spec.spec, 'bzr-0.14')
357
def test_lookup_tag(self):
358
self.tree.branch.tags.set_tag('bzr-0.14', 'r1')
359
self.assertInHistoryIs(1, 'r1', 'tag:bzr-0.14')
361
def test_failed_lookup(self):
362
# tags that don't exist give a specific message: arguably we should
363
# just give InvalidRevisionSpec but I think this is more helpful
364
self.assertRaises(errors.NoSuchTag,
366
'tag:some-random-tag')
369
class TestRevisionSpec_date(TestRevisionSpec):
372
super(TestRevisionSpec, self).setUp()
374
new_tree = self.make_branch_and_tree('new_tree')
375
new_tree.commit('Commit one', rev_id='new_r1',
376
timestamp=time.time() - 60*60*24)
377
new_tree.commit('Commit two', rev_id='new_r2')
378
new_tree.commit('Commit three', rev_id='new_r3')
382
def test_tomorrow(self):
383
self.assertInvalid('date:tomorrow')
385
def test_today(self):
386
self.assertInHistoryIs(2, 'new_r2', 'date:today')
387
self.assertInHistoryIs(1, 'new_r1', 'before:date:today')
389
def test_yesterday(self):
390
self.assertInHistoryIs(1, 'new_r1', 'date:yesterday')
392
def test_invalid(self):
393
self.assertInvalid('date:foobar', extra='\ninvalid date')
394
# You must have '-' between year/month/day
395
self.assertInvalid('date:20040404', extra='\ninvalid date')
396
# Need 2 digits for each date piece
397
self.assertInvalid('date:2004-4-4', extra='\ninvalid date')
400
now = datetime.datetime.now()
401
self.assertInHistoryIs(2, 'new_r2',
402
'date:%04d-%02d-%02d' % (now.year, now.month, now.day))
405
class TestRevisionSpec_ancestor(TestRevisionSpec):
407
def test_non_exact_branch(self):
408
# It seems better to require an exact path to the branch
409
# Branch.open() rather than using Branch.open_containing()
410
self.assertRaises(errors.NotBranchError,
411
self.get_in_history, 'ancestor:tree2/a')
413
def test_simple(self):
414
# Common ancestor of trees is 'alt_r2'
415
self.assertInHistoryIs(None, 'alt_r2', 'ancestor:tree2')
417
# Going the other way, we get a valid revno
419
self.tree = self.tree2
421
self.assertInHistoryIs(2, 'alt_r2', 'ancestor:tree')
424
self.assertInHistoryIs(2, 'r2', 'ancestor:tree')
426
def test_unrelated(self):
427
new_tree = self.make_branch_and_tree('new_tree')
429
new_tree.commit('Commit one', rev_id='new_r1')
430
new_tree.commit('Commit two', rev_id='new_r2')
431
new_tree.commit('Commit three', rev_id='new_r3')
433
# With no common ancestor, we should raise another user error
434
self.assertRaises(errors.NoCommonAncestor,
435
self.get_in_history, 'ancestor:new_tree')
437
def test_no_commits(self):
438
new_tree = self.make_branch_and_tree('new_tree')
439
self.assertRaises(errors.NoCommits,
440
spec_in_history, 'ancestor:new_tree',
443
self.assertRaises(errors.NoCommits,
444
spec_in_history, 'ancestor:tree',
448
class TestRevisionSpec_branch(TestRevisionSpec):
450
def test_non_exact_branch(self):
451
# It seems better to require an exact path to the branch
452
# Branch.open() rather than using Branch.open_containing()
453
self.assertRaises(errors.NotBranchError,
454
self.get_in_history, 'branch:tree2/a')
456
def test_simple(self):
457
self.assertInHistoryIs(None, 'alt_r2', 'branch:tree2')
460
self.assertInHistoryIs(2, 'r2', 'branch:tree')
462
def test_unrelated(self):
463
new_tree = self.make_branch_and_tree('new_tree')
465
new_tree.commit('Commit one', rev_id='new_r1')
466
new_tree.commit('Commit two', rev_id='new_r2')
467
new_tree.commit('Commit three', rev_id='new_r3')
469
self.assertInHistoryIs(None, 'new_r3', 'branch:new_tree')
471
# XXX: Right now, we use fetch() to make sure the remote revisions
472
# have been pulled into the local branch. We may change that
473
# behavior in the future.
474
self.failUnless(self.tree.branch.repository.has_revision('new_r3'))
476
def test_no_commits(self):
477
new_tree = self.make_branch_and_tree('new_tree')
478
self.assertRaises(errors.NoCommits,
479
self.get_in_history, 'branch:new_tree')