~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/revisionspec.py

  • Committer: Lalo Martins
  • Date: 2005-09-09 18:59:17 UTC
  • mto: (1185.1.22)
  • mto: This revision was merged to the branch mainline in revision 1390.
  • Revision ID: lalo@exoweb.net-20050913094215-c9b1ceb71213cf51
polishing up the RevisionSpec api, as per suggestions from Robert

Show diffs side-by-side

added added

removed removed

Lines of Context:
19
19
import re
20
20
from bzrlib.errors import BzrError, NoSuchRevision
21
21
 
22
 
# Map some sort of prefix into a namespace
23
 
# stuff like "revno:10", "revid:", etc.
24
 
# This should match a prefix with a function which accepts it
25
 
REVISION_NAMESPACES = {}
26
 
 
27
 
class RevisionSpec(object):
28
 
    """Equivalent to the old get_revision_info().
 
22
_marker = []
 
23
 
 
24
class RevisionInfo(object):
 
25
    """The results of applying a revision specification to a branch.
 
26
 
29
27
    An instance has two useful attributes: revno, and rev_id.
30
28
 
31
29
    They can also be accessed as spec[0] and spec[1] respectively,
33
31
    revno, rev_id = RevisionSpec(branch, spec)
34
32
    although this is probably going to be deprecated later.
35
33
 
36
 
    Revision specs are an UI element, and they have been moved out
37
 
    of the branch class to leave "back-end" classes unaware of such
38
 
    details.  Code that gets a revno or rev_id from other code should
39
 
    not be using revision specs - revnos and revision ids are the
40
 
    accepted ways to refer to revisions internally.
 
34
    This class exists mostly to be the return value of a RevisionSpec,
 
35
    so that you can access the member you're interested in (number or id)
 
36
    or treat the result as a tuple.
41
37
    """
42
 
    def __init__(self, branch, spec):
43
 
        """Parse a revision specifier.
44
38
 
45
 
        spec can be an integer, in which case it is assumed to be revno
46
 
        (though this will translate negative values into positive ones)
47
 
        spec can also be a string, in which case it is parsed for something
48
 
        like 'date:' or 'revid:' etc.
49
 
        """
 
39
    def __init__(self, branch, revno, rev_id=_marker):
50
40
        self.branch = branch
51
 
 
52
 
        if spec is None:
53
 
            self.revno = 0
54
 
            self.rev_id = None
55
 
            return
56
 
        self.revno = None
57
 
        try:# Convert to int if possible
58
 
            spec = int(spec)
59
 
        except ValueError:
60
 
            pass
61
 
        revs = branch.revision_history()
62
 
        if isinstance(spec, int):
63
 
            if spec < 0:
64
 
                self.revno = len(revs) + spec + 1
65
 
            else:
66
 
                self.revno = spec
67
 
            self.rev_id = branch.get_rev_id(self.revno, revs)
68
 
        elif isinstance(spec, basestring):
69
 
            for prefix, func in REVISION_NAMESPACES.iteritems():
70
 
                if spec.startswith(prefix):
71
 
                    result = func(branch, revs, spec)
72
 
                    if len(result) > 1:
73
 
                        self.revno, self.rev_id = result
74
 
                    else:
75
 
                        self.revno = result[0]
76
 
                        self.rev_id = branch.get_rev_id(self.revno, revs)
77
 
                    break
78
 
            else:
79
 
                raise BzrError('No namespace registered for string: %r' %
80
 
                               spec)
 
41
        self.revno = revno
 
42
        if rev_id is _marker:
 
43
            # allow caller to be lazy
 
44
            if self.revno is None:
 
45
                self.rev_id = None
 
46
            else:
 
47
                self.rev_id = branch.get_rev_id(self.revno)
81
48
        else:
82
 
            raise TypeError('Unhandled revision type %s' % spec)
 
49
            self.rev_id = rev_id
83
50
 
84
 
        if self.revno is None or self.rev_id is None:
85
 
            raise NoSuchRevision(branch, spec)
 
51
    def __nonzero__(self):
 
52
        return not (self.revno is None or self.rev_id is None)
86
53
 
87
54
    def __len__(self):
88
55
        return 2
103
70
        return tuple(self) == tuple(other)
104
71
 
105
72
    def __repr__(self):
106
 
        return '<bzrlib.revisionspec.RevisionSpec object %s, %s for %r>' % (
 
73
        return '<bzrlib.revisionspec.RevisionInfo object %s, %s for %r>' % (
107
74
            self.revno, self.rev_id, self.branch)
108
75
 
 
76
# classes in this list should have a "prefix" attribute, against which
 
77
# string specs are matched
 
78
SPEC_TYPES = []
 
79
 
 
80
class RevisionSpec(object):
 
81
    """A parsed revision specification.
 
82
 
 
83
    A revision specification can be an integer, in which case it is
 
84
    assumed to be a revno (though this will translate negative values
 
85
    into positive ones); or it can be a string, in which case it is
 
86
    parsed for something like 'date:' or 'revid:' etc.
 
87
 
 
88
    Revision specs are an UI element, and they have been moved out
 
89
    of the branch class to leave "back-end" classes unaware of such
 
90
    details.  Code that gets a revno or rev_id from other code should
 
91
    not be using revision specs - revnos and revision ids are the
 
92
    accepted ways to refer to revisions internally.
 
93
 
 
94
    (Equivalent to the old Branch method get_revision_info())
 
95
    """
 
96
 
 
97
    prefix = None
 
98
 
 
99
    def __new__(cls, spec, foo=_marker):
 
100
        """Parse a revision specifier.
 
101
        """
 
102
        if spec is None:
 
103
            return object.__new__(RevisionSpec, spec)
 
104
 
 
105
        try:
 
106
            spec = int(spec)
 
107
        except ValueError:
 
108
            pass
 
109
 
 
110
        if isinstance(spec, int):
 
111
            return object.__new__(RevisionSpec_int, spec)
 
112
        elif isinstance(spec, basestring):
 
113
            for spectype in SPEC_TYPES:
 
114
                if spec.startswith(spectype.prefix):
 
115
                    return object.__new__(spectype, spec)
 
116
            else:
 
117
                raise BzrError('No namespace registered for string: %r' %
 
118
                               spec)
 
119
        else:
 
120
            raise TypeError('Unhandled revision type %s' % spec)
 
121
 
 
122
    def __init__(self, spec):
 
123
        if self.prefix and spec.startswith(self.prefix):
 
124
            spec = spec[len(self.prefix):]
 
125
        self.spec = spec
 
126
 
 
127
    def _match_on(self, branch, revs):
 
128
        return RevisionInfo(branch, 0, None)
 
129
 
 
130
    def _match_on_and_check(self, branch, revs):
 
131
        info = self._match_on(branch, revs)
 
132
        if info:
 
133
            return info
 
134
        elif info == (0, None):
 
135
            # special case - the empty tree
 
136
            return info
 
137
        elif self.prefix:
 
138
            raise NoSuchRevision(branch, self.prefix + str(self.spec))
 
139
        else:
 
140
            raise NoSuchRevision(branch, str(self.spec))
 
141
 
 
142
    def in_history(self, branch):
 
143
        revs = branch.revision_history()
 
144
        return self._match_on_and_check(branch, revs)
 
145
 
109
146
 
110
147
# private API
111
148
 
112
 
def _namespace_revno(branch, revs, spec):
113
 
    """Lookup a revision by revision number"""
114
 
    assert spec.startswith('revno:')
115
 
    try:
116
 
        return (int(spec[len('revno:'):]),)
117
 
    except ValueError:
118
 
        return (None,)
119
 
REVISION_NAMESPACES['revno:'] = _namespace_revno
120
 
 
121
 
 
122
 
def _namespace_revid(branch, revs, spec):
123
 
    assert spec.startswith('revid:')
124
 
    rev_id = spec[len('revid:'):]
125
 
    try:
126
 
        return revs.index(rev_id) + 1, rev_id
127
 
    except ValueError:
128
 
        return (None,)
129
 
REVISION_NAMESPACES['revid:'] = _namespace_revid
130
 
 
131
 
 
132
 
def _namespace_last(branch, revs, spec):
133
 
    assert spec.startswith('last:')
134
 
    try:
135
 
        offset = int(spec[5:])
136
 
    except ValueError:
137
 
        return (None,)
138
 
    else:
139
 
        if offset <= 0:
140
 
            raise BzrError('You must supply a positive value for --revision last:XXX')
141
 
        return (len(revs) - offset + 1,)
142
 
REVISION_NAMESPACES['last:'] = _namespace_last
143
 
 
144
 
 
145
 
def _namespace_tag(branch, revs, spec):
146
 
    assert spec.startswith('tag:')
147
 
    raise BzrError('tag: namespace registered, but not implemented.')
148
 
REVISION_NAMESPACES['tag:'] = _namespace_tag
149
 
 
150
 
 
151
 
_date_re = re.compile(
152
 
        r'(?P<date>(?P<year>\d\d\d\d)-(?P<month>\d\d)-(?P<day>\d\d))?'
153
 
        r'(,|T)?\s*'
154
 
        r'(?P<time>(?P<hour>\d\d):(?P<minute>\d\d)(:(?P<second>\d\d))?)?'
155
 
    )
156
 
 
157
 
def _namespace_date(branch, revs, spec):
158
 
    """
159
 
    Spec for date revisions:
160
 
      date:value
161
 
      value can be 'yesterday', 'today', 'tomorrow' or a YYYY-MM-DD string.
162
 
      it can also start with a '+/-/='. '+' says match the first
163
 
      entry after the given date. '-' is match the first entry before the date
164
 
      '=' is match the first entry after, but still on the given date.
165
 
    
166
 
      +2005-05-12 says find the first matching entry after May 12th, 2005 at 0:00
167
 
      -2005-05-12 says find the first matching entry before May 12th, 2005 at 0:00
168
 
      =2005-05-12 says find the first match after May 12th, 2005 at 0:00 but before
169
 
          May 13th, 2005 at 0:00
170
 
    
171
 
      So the proper way of saying 'give me all entries for today' is:
172
 
          -r {date:+today}:{date:-tomorrow}
173
 
      The default is '=' when not supplied
174
 
    """
175
 
    assert spec.startswith('date:')
176
 
    val = spec[5:]
177
 
    match_style = '='
178
 
    if val[:1] in ('+', '-', '='):
179
 
        match_style = val[:1]
180
 
        val = val[1:]
181
 
 
182
 
    # XXX: this should probably be using datetime.date instead
183
 
    today = datetime.datetime.today().replace(hour=0, minute=0, second=0,
184
 
                                              microsecond=0)
185
 
    if val.lower() == 'yesterday':
186
 
        dt = today - datetime.timedelta(days=1)
187
 
    elif val.lower() == 'today':
188
 
        dt = today
189
 
    elif val.lower() == 'tomorrow':
190
 
        dt = today + datetime.timedelta(days=1)
191
 
    else:
192
 
        m = _date_re.match(val)
193
 
        if not m or (not m.group('date') and not m.group('time')):
194
 
            raise BzrError('Invalid revision date %r' % spec)
195
 
 
196
 
        if m.group('date'):
197
 
            year, month, day = int(m.group('year')), int(m.group('month')), int(m.group('day'))
198
 
        else:
199
 
            year, month, day = today.year, today.month, today.day
200
 
        if m.group('time'):
201
 
            hour = int(m.group('hour'))
202
 
            minute = int(m.group('minute'))
203
 
            if m.group('second'):
204
 
                second = int(m.group('second'))
205
 
            else:
206
 
                second = 0
207
 
        else:
208
 
            hour, minute, second = 0,0,0
209
 
 
210
 
        dt = datetime.datetime(year=year, month=month, day=day,
211
 
                hour=hour, minute=minute, second=second)
212
 
    first = dt
213
 
    last = None
214
 
    reversed = False
215
 
    if match_style == '-':
216
 
        reversed = True
217
 
    elif match_style == '=':
218
 
        last = dt + datetime.timedelta(days=1)
219
 
 
220
 
    if reversed:
221
 
        for i in range(len(revs)-1, -1, -1):
222
 
            r = branch.get_revision(revs[i])
223
 
            # TODO: Handle timezone.
224
 
            dt = datetime.datetime.fromtimestamp(r.timestamp)
225
 
            if first >= dt and (last is None or dt >= last):
226
 
                return (i+1,)
227
 
    else:
228
 
        for i in range(len(revs)):
229
 
            r = branch.get_revision(revs[i])
230
 
            # TODO: Handle timezone.
231
 
            dt = datetime.datetime.fromtimestamp(r.timestamp)
232
 
            if first <= dt and (last is None or dt <= last):
233
 
                return (i+1,)
234
 
REVISION_NAMESPACES['date:'] = _namespace_date
 
149
class RevisionSpec_int(RevisionSpec):
 
150
    """Spec is a number.  Special case."""
 
151
    def __init__(self, spec):
 
152
        self.spec = int(spec)
 
153
 
 
154
    def _match_on(self, branch, revs):
 
155
        if self.spec < 0:
 
156
            revno = len(revs) + self.spec + 1
 
157
        else:
 
158
            revno = self.spec
 
159
        rev_id = branch.get_rev_id(revno, revs)
 
160
        return RevisionInfo(branch, revno, rev_id)
 
161
 
 
162
 
 
163
class RevisionSpec_revno(RevisionSpec):
 
164
    prefix = 'revno:'
 
165
 
 
166
    def _match_on(self, branch, revs):
 
167
        """Lookup a revision by revision number"""
 
168
        try:
 
169
            return RevisionInfo(branch, int(self.spec))
 
170
        except ValueError:
 
171
            return RevisionInfo(branch, None)
 
172
 
 
173
SPEC_TYPES.append(RevisionSpec_revno)
 
174
 
 
175
 
 
176
class RevisionSpec_revid(RevisionSpec):
 
177
    prefix = 'revid:'
 
178
 
 
179
    def _match_on(self, branch, revs):
 
180
        try:
 
181
            return RevisionInfo(branch, revs.index(self.spec) + 1, self.spec)
 
182
        except ValueError:
 
183
            return RevisionInfo(branch, None)
 
184
 
 
185
SPEC_TYPES.append(RevisionSpec_revid)
 
186
 
 
187
 
 
188
class RevisionSpec_last(RevisionSpec):
 
189
    prefix = 'last:'
 
190
 
 
191
    def _match_on(self, branch, revs):
 
192
        try:
 
193
            offset = int(self.spec)
 
194
        except ValueError:
 
195
            return RevisionInfo(branch, None)
 
196
        else:
 
197
            if offset <= 0:
 
198
                raise BzrError('You must supply a positive value for --revision last:XXX')
 
199
            return RevisionInfo(branch, len(revs) - offset + 1)
 
200
 
 
201
SPEC_TYPES.append(RevisionSpec_last)
 
202
 
 
203
 
 
204
class RevisionSpec_tag(RevisionSpec):
 
205
    prefix = 'tag:'
 
206
 
 
207
    def _match_on(self, branch, revs):
 
208
        raise BzrError('tag: namespace registered, but not implemented.')
 
209
 
 
210
SPEC_TYPES.append(RevisionSpec_tag)
 
211
 
 
212
 
 
213
class RevisionSpec_date(RevisionSpec):
 
214
    prefix = 'date:'
 
215
    _date_re = re.compile(
 
216
            r'(?P<date>(?P<year>\d\d\d\d)-(?P<month>\d\d)-(?P<day>\d\d))?'
 
217
            r'(,|T)?\s*'
 
218
            r'(?P<time>(?P<hour>\d\d):(?P<minute>\d\d)(:(?P<second>\d\d))?)?'
 
219
        )
 
220
 
 
221
    def _match_on(self, branch, revs):
 
222
        """
 
223
        Spec for date revisions:
 
224
          date:value
 
225
          value can be 'yesterday', 'today', 'tomorrow' or a YYYY-MM-DD string.
 
226
          it can also start with a '+/-/='. '+' says match the first
 
227
          entry after the given date. '-' is match the first entry before the date
 
228
          '=' is match the first entry after, but still on the given date.
 
229
 
 
230
          +2005-05-12 says find the first matching entry after May 12th, 2005 at 0:00
 
231
          -2005-05-12 says find the first matching entry before May 12th, 2005 at 0:00
 
232
          =2005-05-12 says find the first match after May 12th, 2005 at 0:00 but before
 
233
              May 13th, 2005 at 0:00
 
234
 
 
235
          So the proper way of saying 'give me all entries for today' is:
 
236
              -r {date:+today}:{date:-tomorrow}
 
237
          The default is '=' when not supplied
 
238
        """
 
239
        match_style = '='
 
240
        if self.spec[:1] in ('+', '-', '='):
 
241
            match_style = self.spec[:1]
 
242
            self.spec = self.spec[1:]
 
243
 
 
244
        # XXX: this should probably be using datetime.date instead
 
245
        today = datetime.datetime.today().replace(hour=0, minute=0, second=0,
 
246
                                                  microsecond=0)
 
247
        if self.spec.lower() == 'yesterday':
 
248
            dt = today - datetime.timedelta(days=1)
 
249
        elif self.spec.lower() == 'today':
 
250
            dt = today
 
251
        elif self.spec.lower() == 'tomorrow':
 
252
            dt = today + datetime.timedelta(days=1)
 
253
        else:
 
254
            m = self._date_re.match(self.spec)
 
255
            if not m or (not m.group('date') and not m.group('time')):
 
256
                raise BzrError('Invalid revision date %r' % self.spec)
 
257
 
 
258
            if m.group('date'):
 
259
                year, month, day = int(m.group('year')), int(m.group('month')), int(m.group('day'))
 
260
            else:
 
261
                year, month, day = today.year, today.month, today.day
 
262
            if m.group('time'):
 
263
                hour = int(m.group('hour'))
 
264
                minute = int(m.group('minute'))
 
265
                if m.group('second'):
 
266
                    second = int(m.group('second'))
 
267
                else:
 
268
                    second = 0
 
269
            else:
 
270
                hour, minute, second = 0,0,0
 
271
 
 
272
            dt = datetime.datetime(year=year, month=month, day=day,
 
273
                    hour=hour, minute=minute, second=second)
 
274
        first = dt
 
275
        last = None
 
276
        reversed = False
 
277
        if match_style == '-':
 
278
            reversed = True
 
279
        elif match_style == '=':
 
280
            last = dt + datetime.timedelta(days=1)
 
281
 
 
282
        if reversed:
 
283
            for i in range(len(revs)-1, -1, -1):
 
284
                r = branch.get_revision(revs[i])
 
285
                # TODO: Handle timezone.
 
286
                dt = datetime.datetime.fromtimestamp(r.timestamp)
 
287
                if first >= dt and (last is None or dt >= last):
 
288
                    return RevisionInfo(branch, i+1,)
 
289
        else:
 
290
            for i in range(len(revs)):
 
291
                r = branch.get_revision(revs[i])
 
292
                # TODO: Handle timezone.
 
293
                dt = datetime.datetime.fromtimestamp(r.timestamp)
 
294
                if first <= dt and (last is None or dt <= last):
 
295
                    return RevisionInfo(branch, i+1,)
 
296
 
 
297
SPEC_TYPES.append(RevisionSpec_date)