/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/revisionspec.py

  • Committer: Robert Collins
  • Date: 2006-11-08 00:36:30 UTC
  • mto: This revision was merged to the branch mainline in revision 2124.
  • Revision ID: robertc@robertcollins.net-20061108003630-feb31613c83f7096
(Robert Collins) Extend the problem reporting command line UI to use
apport to report more detailed diagnostics which should help in in getting
faults reported in Malone and provides the basis for capturing more
information such as detailed logging data from the current invocation of
bzr in the future (without cluttering 'bzr.log' unnecessarily).
apport is available from Ubuntu Edgy onwards.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005 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 bisect
 
19
import datetime
 
20
import re
 
21
 
 
22
from bzrlib import (
 
23
    errors,
 
24
    revision,
 
25
    symbol_versioning,
 
26
    trace,
 
27
    tsort,
 
28
    )
 
29
 
 
30
 
 
31
_marker = []
 
32
 
 
33
 
 
34
class RevisionInfo(object):
 
35
    """The results of applying a revision specification to a branch.
 
36
 
 
37
    An instance has two useful attributes: revno, and rev_id.
 
38
 
 
39
    They can also be accessed as spec[0] and spec[1] respectively,
 
40
    so that you can write code like:
 
41
    revno, rev_id = RevisionSpec(branch, spec)
 
42
    although this is probably going to be deprecated later.
 
43
 
 
44
    This class exists mostly to be the return value of a RevisionSpec,
 
45
    so that you can access the member you're interested in (number or id)
 
46
    or treat the result as a tuple.
 
47
    """
 
48
 
 
49
    def __init__(self, branch, revno, rev_id=_marker):
 
50
        self.branch = branch
 
51
        self.revno = revno
 
52
        if rev_id is _marker:
 
53
            # allow caller to be lazy
 
54
            if self.revno is None:
 
55
                self.rev_id = None
 
56
            else:
 
57
                self.rev_id = branch.get_rev_id(self.revno)
 
58
        else:
 
59
            self.rev_id = rev_id
 
60
 
 
61
    def __nonzero__(self):
 
62
        # first the easy ones...
 
63
        if self.rev_id is None:
 
64
            return False
 
65
        if self.revno is not None:
 
66
            return True
 
67
        # TODO: otherwise, it should depend on how I was built -
 
68
        # if it's in_history(branch), then check revision_history(),
 
69
        # if it's in_store(branch), do the check below
 
70
        return self.branch.repository.has_revision(self.rev_id)
 
71
 
 
72
    def __len__(self):
 
73
        return 2
 
74
 
 
75
    def __getitem__(self, index):
 
76
        if index == 0: return self.revno
 
77
        if index == 1: return self.rev_id
 
78
        raise IndexError(index)
 
79
 
 
80
    def get(self):
 
81
        return self.branch.repository.get_revision(self.rev_id)
 
82
 
 
83
    def __eq__(self, other):
 
84
        if type(other) not in (tuple, list, type(self)):
 
85
            return False
 
86
        if type(other) is type(self) and self.branch is not other.branch:
 
87
            return False
 
88
        return tuple(self) == tuple(other)
 
89
 
 
90
    def __repr__(self):
 
91
        return '<bzrlib.revisionspec.RevisionInfo object %s, %s for %r>' % (
 
92
            self.revno, self.rev_id, self.branch)
 
93
 
 
94
 
 
95
# classes in this list should have a "prefix" attribute, against which
 
96
# string specs are matched
 
97
SPEC_TYPES = []
 
98
_revno_regex = None
 
99
 
 
100
 
 
101
class RevisionSpec(object):
 
102
    """A parsed revision specification.
 
103
 
 
104
    A revision specification can be an integer, in which case it is
 
105
    assumed to be a revno (though this will translate negative values
 
106
    into positive ones); or it can be a string, in which case it is
 
107
    parsed for something like 'date:' or 'revid:' etc.
 
108
 
 
109
    Revision specs are an UI element, and they have been moved out
 
110
    of the branch class to leave "back-end" classes unaware of such
 
111
    details.  Code that gets a revno or rev_id from other code should
 
112
    not be using revision specs - revnos and revision ids are the
 
113
    accepted ways to refer to revisions internally.
 
114
 
 
115
    (Equivalent to the old Branch method get_revision_info())
 
116
    """
 
117
 
 
118
    prefix = None
 
119
 
 
120
    def __new__(cls, spec, _internal=False):
 
121
        if _internal:
 
122
            return object.__new__(cls, spec, _internal=_internal)
 
123
 
 
124
        symbol_versioning.warn('Creating a RevisionSpec directly has'
 
125
                               ' been deprecated in version 0.11. Use'
 
126
                               ' RevisionSpec.from_string()'
 
127
                               ' instead.',
 
128
                               DeprecationWarning, stacklevel=2)
 
129
        return RevisionSpec.from_string(spec)
 
130
 
 
131
    @staticmethod
 
132
    def from_string(spec):
 
133
        """Parse a revision spec string into a RevisionSpec object.
 
134
 
 
135
        :param spec: A string specified by the user
 
136
        :return: A RevisionSpec object that understands how to parse the
 
137
            supplied notation.
 
138
        """
 
139
        if not isinstance(spec, (type(None), basestring)):
 
140
            raise TypeError('error')
 
141
 
 
142
        if spec is None:
 
143
            return RevisionSpec(None, _internal=True)
 
144
 
 
145
        assert isinstance(spec, basestring), \
 
146
            "You should only supply strings not %s" % (type(spec),)
 
147
 
 
148
        for spectype in SPEC_TYPES:
 
149
            if spec.startswith(spectype.prefix):
 
150
                trace.mutter('Returning RevisionSpec %s for %s',
 
151
                             spectype.__name__, spec)
 
152
                return spectype(spec, _internal=True)
 
153
        else:
 
154
            # RevisionSpec_revno is special cased, because it is the only
 
155
            # one that directly handles plain integers
 
156
            # TODO: This should not be special cased rather it should be
 
157
            # a method invocation on spectype.canparse()
 
158
            global _revno_regex
 
159
            if _revno_regex is None:
 
160
                _revno_regex = re.compile(r'^(?:(\d+(\.\d+)*)|-\d+)(:.*)?$')
 
161
            if _revno_regex.match(spec) is not None:
 
162
                return RevisionSpec_revno(spec, _internal=True)
 
163
 
 
164
            raise errors.NoSuchRevisionSpec(spec)
 
165
 
 
166
    def __init__(self, spec, _internal=False):
 
167
        """Create a RevisionSpec referring to the Null revision.
 
168
 
 
169
        :param spec: The original spec supplied by the user
 
170
        :param _internal: Used to ensure that RevisionSpec is not being
 
171
            called directly. Only from RevisionSpec.from_string()
 
172
        """
 
173
        if not _internal:
 
174
            # XXX: Update this after 0.10 is released
 
175
            symbol_versioning.warn('Creating a RevisionSpec directly has'
 
176
                                   ' been deprecated in version 0.11. Use'
 
177
                                   ' RevisionSpec.from_string()'
 
178
                                   ' instead.',
 
179
                                   DeprecationWarning, stacklevel=2)
 
180
        self.user_spec = spec
 
181
        if self.prefix and spec.startswith(self.prefix):
 
182
            spec = spec[len(self.prefix):]
 
183
        self.spec = spec
 
184
 
 
185
    def _match_on(self, branch, revs):
 
186
        trace.mutter('Returning RevisionSpec._match_on: None')
 
187
        return RevisionInfo(branch, 0, None)
 
188
 
 
189
    def _match_on_and_check(self, branch, revs):
 
190
        info = self._match_on(branch, revs)
 
191
        if info:
 
192
            return info
 
193
        elif info == (0, None):
 
194
            # special case - the empty tree
 
195
            return info
 
196
        elif self.prefix:
 
197
            raise errors.InvalidRevisionSpec(self.user_spec, branch)
 
198
        else:
 
199
            raise errors.InvalidRevisionSpec(self.spec, branch)
 
200
 
 
201
    def in_history(self, branch):
 
202
        if branch:
 
203
            revs = branch.revision_history()
 
204
        else:
 
205
            # this should never trigger.
 
206
            # TODO: make it a deprecated code path. RBC 20060928
 
207
            revs = None
 
208
        return self._match_on_and_check(branch, revs)
 
209
 
 
210
        # FIXME: in_history is somewhat broken,
 
211
        # it will return non-history revisions in many
 
212
        # circumstances. The expected facility is that
 
213
        # in_history only returns revision-history revs,
 
214
        # in_store returns any rev. RBC 20051010
 
215
    # aliases for now, when we fix the core logic, then they
 
216
    # will do what you expect.
 
217
    in_store = in_history
 
218
    in_branch = in_store
 
219
        
 
220
    def __repr__(self):
 
221
        # this is mostly for helping with testing
 
222
        return '<%s %s>' % (self.__class__.__name__,
 
223
                              self.user_spec)
 
224
    
 
225
    def needs_branch(self):
 
226
        """Whether this revision spec needs a branch.
 
227
 
 
228
        Set this to False the branch argument of _match_on is not used.
 
229
        """
 
230
        return True
 
231
 
 
232
    def get_branch(self):
 
233
        """When the revision specifier contains a branch location, return it.
 
234
        
 
235
        Otherwise, return None.
 
236
        """
 
237
        return None
 
238
 
 
239
 
 
240
# private API
 
241
 
 
242
class RevisionSpec_revno(RevisionSpec):
 
243
    prefix = 'revno:'
 
244
 
 
245
    def _match_on(self, branch, revs):
 
246
        """Lookup a revision by revision number"""
 
247
        loc = self.spec.find(':')
 
248
        if loc == -1:
 
249
            revno_spec = self.spec
 
250
            branch_spec = None
 
251
        else:
 
252
            revno_spec = self.spec[:loc]
 
253
            branch_spec = self.spec[loc+1:]
 
254
 
 
255
        if revno_spec == '':
 
256
            if not branch_spec:
 
257
                raise errors.InvalidRevisionSpec(self.user_spec,
 
258
                        branch, 'cannot have an empty revno and no branch')
 
259
            revno = None
 
260
        else:
 
261
            try:
 
262
                revno = int(revno_spec)
 
263
                dotted = False
 
264
            except ValueError:
 
265
                # dotted decimal. This arguably should not be here
 
266
                # but the from_string method is a little primitive 
 
267
                # right now - RBC 20060928
 
268
                try:
 
269
                    match_revno = tuple((int(number) for number in revno_spec.split('.')))
 
270
                except ValueError, e:
 
271
                    raise errors.InvalidRevisionSpec(self.user_spec, branch, e)
 
272
 
 
273
                dotted = True
 
274
 
 
275
        if branch_spec:
 
276
            # the user has override the branch to look in.
 
277
            # we need to refresh the revision_history map and
 
278
            # the branch object.
 
279
            from bzrlib.branch import Branch
 
280
            branch = Branch.open(branch_spec)
 
281
            # Need to use a new revision history
 
282
            # because we are using a specific branch
 
283
            revs = branch.revision_history()
 
284
 
 
285
        if dotted:
 
286
            branch.lock_read()
 
287
            try:
 
288
                last_rev = branch.last_revision()
 
289
                merge_sorted_revisions = tsort.merge_sort(
 
290
                    branch.repository.get_revision_graph(last_rev),
 
291
                    last_rev,
 
292
                    generate_revno=True)
 
293
                def match(item):
 
294
                    return item[3] == match_revno
 
295
                revisions = filter(match, merge_sorted_revisions)
 
296
            finally:
 
297
                branch.unlock()
 
298
            if len(revisions) != 1:
 
299
                return RevisionInfo(branch, None, None)
 
300
            else:
 
301
                # there is no traditional 'revno' for dotted-decimal revnos.
 
302
                # so for  API compatability we return None.
 
303
                return RevisionInfo(branch, None, revisions[0][1])
 
304
        else:
 
305
            if revno < 0:
 
306
                if (-revno) >= len(revs):
 
307
                    revno = 1
 
308
                else:
 
309
                    revno = len(revs) + revno + 1
 
310
            try:
 
311
                revision_id = branch.get_rev_id(revno, revs)
 
312
            except errors.NoSuchRevision:
 
313
                raise errors.InvalidRevisionSpec(self.user_spec, branch)
 
314
        return RevisionInfo(branch, revno, revision_id)
 
315
        
 
316
    def needs_branch(self):
 
317
        return self.spec.find(':') == -1
 
318
 
 
319
    def get_branch(self):
 
320
        if self.spec.find(':') == -1:
 
321
            return None
 
322
        else:
 
323
            return self.spec[self.spec.find(':')+1:]
 
324
 
 
325
# Old compatibility 
 
326
RevisionSpec_int = RevisionSpec_revno
 
327
 
 
328
SPEC_TYPES.append(RevisionSpec_revno)
 
329
 
 
330
 
 
331
class RevisionSpec_revid(RevisionSpec):
 
332
    prefix = 'revid:'
 
333
 
 
334
    def _match_on(self, branch, revs):
 
335
        try:
 
336
            revno = revs.index(self.spec) + 1
 
337
        except ValueError:
 
338
            revno = None
 
339
        return RevisionInfo(branch, revno, self.spec)
 
340
 
 
341
SPEC_TYPES.append(RevisionSpec_revid)
 
342
 
 
343
 
 
344
class RevisionSpec_last(RevisionSpec):
 
345
 
 
346
    prefix = 'last:'
 
347
 
 
348
    def _match_on(self, branch, revs):
 
349
        if self.spec == '':
 
350
            if not revs:
 
351
                raise errors.NoCommits(branch)
 
352
            return RevisionInfo(branch, len(revs), revs[-1])
 
353
 
 
354
        try:
 
355
            offset = int(self.spec)
 
356
        except ValueError, e:
 
357
            raise errors.InvalidRevisionSpec(self.user_spec, branch, e)
 
358
 
 
359
        if offset <= 0:
 
360
            raise errors.InvalidRevisionSpec(self.user_spec, branch,
 
361
                                             'you must supply a positive value')
 
362
        revno = len(revs) - offset + 1
 
363
        try:
 
364
            revision_id = branch.get_rev_id(revno, revs)
 
365
        except errors.NoSuchRevision:
 
366
            raise errors.InvalidRevisionSpec(self.user_spec, branch)
 
367
        return RevisionInfo(branch, revno, revision_id)
 
368
 
 
369
SPEC_TYPES.append(RevisionSpec_last)
 
370
 
 
371
 
 
372
class RevisionSpec_before(RevisionSpec):
 
373
 
 
374
    prefix = 'before:'
 
375
    
 
376
    def _match_on(self, branch, revs):
 
377
        r = RevisionSpec.from_string(self.spec)._match_on(branch, revs)
 
378
        if r.revno == 0:
 
379
            raise errors.InvalidRevisionSpec(self.user_spec, branch,
 
380
                                         'cannot go before the null: revision')
 
381
        if r.revno is None:
 
382
            # We need to use the repository history here
 
383
            rev = branch.repository.get_revision(r.rev_id)
 
384
            if not rev.parent_ids:
 
385
                revno = 0
 
386
                revision_id = None
 
387
            else:
 
388
                revision_id = rev.parent_ids[0]
 
389
                try:
 
390
                    revno = revs.index(revision_id) + 1
 
391
                except ValueError:
 
392
                    revno = None
 
393
        else:
 
394
            revno = r.revno - 1
 
395
            try:
 
396
                revision_id = branch.get_rev_id(revno, revs)
 
397
            except errors.NoSuchRevision:
 
398
                raise errors.InvalidRevisionSpec(self.user_spec,
 
399
                                                 branch)
 
400
        return RevisionInfo(branch, revno, revision_id)
 
401
 
 
402
SPEC_TYPES.append(RevisionSpec_before)
 
403
 
 
404
 
 
405
class RevisionSpec_tag(RevisionSpec):
 
406
    prefix = 'tag:'
 
407
 
 
408
    def _match_on(self, branch, revs):
 
409
        raise errors.InvalidRevisionSpec(self.user_spec, branch,
 
410
                                         'tag: namespace registered,'
 
411
                                         ' but not implemented')
 
412
 
 
413
SPEC_TYPES.append(RevisionSpec_tag)
 
414
 
 
415
 
 
416
class _RevListToTimestamps(object):
 
417
    """This takes a list of revisions, and allows you to bisect by date"""
 
418
 
 
419
    __slots__ = ['revs', 'branch']
 
420
 
 
421
    def __init__(self, revs, branch):
 
422
        self.revs = revs
 
423
        self.branch = branch
 
424
 
 
425
    def __getitem__(self, index):
 
426
        """Get the date of the index'd item"""
 
427
        r = self.branch.repository.get_revision(self.revs[index])
 
428
        # TODO: Handle timezone.
 
429
        return datetime.datetime.fromtimestamp(r.timestamp)
 
430
 
 
431
    def __len__(self):
 
432
        return len(self.revs)
 
433
 
 
434
 
 
435
class RevisionSpec_date(RevisionSpec):
 
436
    prefix = 'date:'
 
437
    _date_re = re.compile(
 
438
            r'(?P<date>(?P<year>\d\d\d\d)-(?P<month>\d\d)-(?P<day>\d\d))?'
 
439
            r'(,|T)?\s*'
 
440
            r'(?P<time>(?P<hour>\d\d):(?P<minute>\d\d)(:(?P<second>\d\d))?)?'
 
441
        )
 
442
 
 
443
    def _match_on(self, branch, revs):
 
444
        """
 
445
        Spec for date revisions:
 
446
          date:value
 
447
          value can be 'yesterday', 'today', 'tomorrow' or a YYYY-MM-DD string.
 
448
          matches the first entry after a given date (either at midnight or
 
449
          at a specified time).
 
450
 
 
451
          So the proper way of saying 'give me all entries for today' is:
 
452
              -r date:yesterday..date:today
 
453
        """
 
454
        today = datetime.datetime.fromordinal(datetime.date.today().toordinal())
 
455
        if self.spec.lower() == 'yesterday':
 
456
            dt = today - datetime.timedelta(days=1)
 
457
        elif self.spec.lower() == 'today':
 
458
            dt = today
 
459
        elif self.spec.lower() == 'tomorrow':
 
460
            dt = today + datetime.timedelta(days=1)
 
461
        else:
 
462
            m = self._date_re.match(self.spec)
 
463
            if not m or (not m.group('date') and not m.group('time')):
 
464
                raise errors.InvalidRevisionSpec(self.user_spec,
 
465
                                                 branch, 'invalid date')
 
466
 
 
467
            try:
 
468
                if m.group('date'):
 
469
                    year = int(m.group('year'))
 
470
                    month = int(m.group('month'))
 
471
                    day = int(m.group('day'))
 
472
                else:
 
473
                    year = today.year
 
474
                    month = today.month
 
475
                    day = today.day
 
476
 
 
477
                if m.group('time'):
 
478
                    hour = int(m.group('hour'))
 
479
                    minute = int(m.group('minute'))
 
480
                    if m.group('second'):
 
481
                        second = int(m.group('second'))
 
482
                    else:
 
483
                        second = 0
 
484
                else:
 
485
                    hour, minute, second = 0,0,0
 
486
            except ValueError:
 
487
                raise errors.InvalidRevisionSpec(self.user_spec,
 
488
                                                 branch, 'invalid date')
 
489
 
 
490
            dt = datetime.datetime(year=year, month=month, day=day,
 
491
                    hour=hour, minute=minute, second=second)
 
492
        branch.lock_read()
 
493
        try:
 
494
            rev = bisect.bisect(_RevListToTimestamps(revs, branch), dt)
 
495
        finally:
 
496
            branch.unlock()
 
497
        if rev == len(revs):
 
498
            return RevisionInfo(branch, None)
 
499
        else:
 
500
            return RevisionInfo(branch, rev + 1)
 
501
 
 
502
SPEC_TYPES.append(RevisionSpec_date)
 
503
 
 
504
 
 
505
class RevisionSpec_ancestor(RevisionSpec):
 
506
    prefix = 'ancestor:'
 
507
 
 
508
    def _match_on(self, branch, revs):
 
509
        from bzrlib.branch import Branch
 
510
 
 
511
        trace.mutter('matching ancestor: on: %s, %s', self.spec, branch)
 
512
        other_branch = Branch.open(self.spec)
 
513
        revision_a = branch.last_revision()
 
514
        revision_b = other_branch.last_revision()
 
515
        for r, b in ((revision_a, branch), (revision_b, other_branch)):
 
516
            if r in (None, revision.NULL_REVISION):
 
517
                raise errors.NoCommits(b)
 
518
        revision_source = revision.MultipleRevisionSources(
 
519
                branch.repository, other_branch.repository)
 
520
        rev_id = revision.common_ancestor(revision_a, revision_b,
 
521
                                          revision_source)
 
522
        try:
 
523
            revno = branch.revision_id_to_revno(rev_id)
 
524
        except errors.NoSuchRevision:
 
525
            revno = None
 
526
        return RevisionInfo(branch, revno, rev_id)
 
527
        
 
528
SPEC_TYPES.append(RevisionSpec_ancestor)
 
529
 
 
530
 
 
531
class RevisionSpec_branch(RevisionSpec):
 
532
    """A branch: revision specifier.
 
533
 
 
534
    This takes the path to a branch and returns its tip revision id.
 
535
    """
 
536
    prefix = 'branch:'
 
537
 
 
538
    def _match_on(self, branch, revs):
 
539
        from bzrlib.branch import Branch
 
540
        other_branch = Branch.open(self.spec)
 
541
        revision_b = other_branch.last_revision()
 
542
        if revision_b in (None, revision.NULL_REVISION):
 
543
            raise errors.NoCommits(other_branch)
 
544
        # pull in the remote revisions so we can diff
 
545
        branch.fetch(other_branch, revision_b)
 
546
        try:
 
547
            revno = branch.revision_id_to_revno(revision_b)
 
548
        except errors.NoSuchRevision:
 
549
            revno = None
 
550
        return RevisionInfo(branch, revno, revision_b)
 
551
        
 
552
SPEC_TYPES.append(RevisionSpec_branch)