~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/revisionspec.py

Merge in format-5 work - release bzr 0.1rc1.

Show diffs side-by-side

added added

removed removed

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