~bzr-pqm/bzr/bzr.dev

1185.2.5 by Lalo Martins
moving the 'revision spec' stuff out of the Branch class and into a new
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 datetime
19
import re
1185.2.18 by Lalo Martins
merging from integration again.
20
from bzrlib.errors import BzrError, NoSuchRevision, NoCommits
1185.2.5 by Lalo Martins
moving the 'revision spec' stuff out of the Branch class and into a new
21
1185.2.13 by Lalo Martins
polishing up the RevisionSpec api, as per suggestions from Robert
22
_marker = []
23
24
class RevisionInfo(object):
25
    """The results of applying a revision specification to a branch.
26
1185.2.6 by Lalo Martins
turned get_revision_info into a RevisionSpec class
27
    An instance has two useful attributes: revno, and rev_id.
28
29
    They can also be accessed as spec[0] and spec[1] respectively,
30
    so that you can write code like:
31
    revno, rev_id = RevisionSpec(branch, spec)
32
    although this is probably going to be deprecated later.
33
1185.2.13 by Lalo Martins
polishing up the RevisionSpec api, as per suggestions from Robert
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.
1185.2.5 by Lalo Martins
moving the 'revision spec' stuff out of the Branch class and into a new
37
    """
1185.2.6 by Lalo Martins
turned get_revision_info into a RevisionSpec class
38
1185.2.13 by Lalo Martins
polishing up the RevisionSpec api, as per suggestions from Robert
39
    def __init__(self, branch, revno, rev_id=_marker):
1185.2.6 by Lalo Martins
turned get_revision_info into a RevisionSpec class
40
        self.branch = branch
1185.2.13 by Lalo Martins
polishing up the RevisionSpec api, as per suggestions from Robert
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)
1185.2.6 by Lalo Martins
turned get_revision_info into a RevisionSpec class
48
        else:
1185.2.13 by Lalo Martins
polishing up the RevisionSpec api, as per suggestions from Robert
49
            self.rev_id = rev_id
1185.2.6 by Lalo Martins
turned get_revision_info into a RevisionSpec class
50
1185.2.13 by Lalo Martins
polishing up the RevisionSpec api, as per suggestions from Robert
51
    def __nonzero__(self):
1185.2.18 by Lalo Martins
merging from integration again.
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
1185.2.6 by Lalo Martins
turned get_revision_info into a RevisionSpec class
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):
1185.2.13 by Lalo Martins
polishing up the RevisionSpec api, as per suggestions from Robert
81
        return '<bzrlib.revisionspec.RevisionInfo object %s, %s for %r>' % (
1185.2.6 by Lalo Martins
turned get_revision_info into a RevisionSpec class
82
            self.revno, self.rev_id, self.branch)
1185.2.5 by Lalo Martins
moving the 'revision spec' stuff out of the Branch class and into a new
83
1185.2.13 by Lalo Martins
polishing up the RevisionSpec api, as per suggestions from Robert
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
96
    Revision specs are an UI element, and they have been moved out
97
    of the branch class to leave "back-end" classes unaware of such
98
    details.  Code that gets a revno or rev_id from other code should
99
    not be using revision specs - revnos and revision ids are the
100
    accepted ways to refer to revisions internally.
101
102
    (Equivalent to the old Branch method get_revision_info())
103
    """
104
105
    prefix = None
106
107
    def __new__(cls, spec, foo=_marker):
108
        """Parse a revision specifier.
109
        """
110
        if spec is None:
111
            return object.__new__(RevisionSpec, spec)
112
113
        try:
114
            spec = int(spec)
115
        except ValueError:
116
            pass
117
118
        if isinstance(spec, int):
119
            return object.__new__(RevisionSpec_int, spec)
120
        elif isinstance(spec, basestring):
121
            for spectype in SPEC_TYPES:
122
                if spec.startswith(spectype.prefix):
123
                    return object.__new__(spectype, spec)
124
            else:
125
                raise BzrError('No namespace registered for string: %r' %
126
                               spec)
127
        else:
128
            raise TypeError('Unhandled revision type %s' % spec)
129
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)
153
1185.2.14 by Lalo Martins
fixing up users of RevisionSpec, and moving it over to commands.py
154
    def __repr__(self):
155
        # this is mostly for helping with testing
156
        return '<%s %s%s>' % (self.__class__.__name__,
157
                              self.prefix or '',
158
                              self.spec)
159
1185.2.5 by Lalo Martins
moving the 'revision spec' stuff out of the Branch class and into a new
160
161
# private API
162
1185.2.13 by Lalo Martins
polishing up the RevisionSpec api, as per suggestions from Robert
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
    prefix = 'last:'
204
205
    def _match_on(self, branch, revs):
206
        try:
207
            offset = int(self.spec)
208
        except ValueError:
209
            return RevisionInfo(branch, None)
210
        else:
211
            if offset <= 0:
212
                raise BzrError('You must supply a positive value for --revision last:XXX')
213
            return RevisionInfo(branch, len(revs) - offset + 1)
214
215
SPEC_TYPES.append(RevisionSpec_last)
216
217
218
class RevisionSpec_tag(RevisionSpec):
219
    prefix = 'tag:'
220
221
    def _match_on(self, branch, revs):
222
        raise BzrError('tag: namespace registered, but not implemented.')
223
224
SPEC_TYPES.append(RevisionSpec_tag)
225
226
227
class RevisionSpec_date(RevisionSpec):
228
    prefix = 'date:'
229
    _date_re = re.compile(
230
            r'(?P<date>(?P<year>\d\d\d\d)-(?P<month>\d\d)-(?P<day>\d\d))?'
231
            r'(,|T)?\s*'
232
            r'(?P<time>(?P<hour>\d\d):(?P<minute>\d\d)(:(?P<second>\d\d))?)?'
233
        )
234
235
    def _match_on(self, branch, revs):
236
        """
237
        Spec for date revisions:
238
          date:value
239
          value can be 'yesterday', 'today', 'tomorrow' or a YYYY-MM-DD string.
240
          it can also start with a '+/-/='. '+' says match the first
241
          entry after the given date. '-' is match the first entry before the date
242
          '=' is match the first entry after, but still on the given date.
243
244
          +2005-05-12 says find the first matching entry after May 12th, 2005 at 0:00
245
          -2005-05-12 says find the first matching entry before May 12th, 2005 at 0:00
246
          =2005-05-12 says find the first match after May 12th, 2005 at 0:00 but before
247
              May 13th, 2005 at 0:00
248
249
          So the proper way of saying 'give me all entries for today' is:
250
              -r {date:+today}:{date:-tomorrow}
251
          The default is '=' when not supplied
252
        """
253
        match_style = '='
254
        if self.spec[:1] in ('+', '-', '='):
255
            match_style = self.spec[:1]
256
            self.spec = self.spec[1:]
257
258
        # XXX: this should probably be using datetime.date instead
259
        today = datetime.datetime.today().replace(hour=0, minute=0, second=0,
260
                                                  microsecond=0)
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
289
        last = None
290
        reversed = False
291
        if match_style == '-':
292
            reversed = True
293
        elif match_style == '=':
294
            last = dt + datetime.timedelta(days=1)
295
296
        if reversed:
297
            for i in range(len(revs)-1, -1, -1):
298
                r = branch.get_revision(revs[i])
299
                # TODO: Handle timezone.
300
                dt = datetime.datetime.fromtimestamp(r.timestamp)
301
                if first >= dt and (last is None or dt >= last):
302
                    return RevisionInfo(branch, i+1,)
303
        else:
304
            for i in range(len(revs)):
305
                r = branch.get_revision(revs[i])
306
                # TODO: Handle timezone.
307
                dt = datetime.datetime.fromtimestamp(r.timestamp)
308
                if first <= dt and (last is None or dt <= last):
309
                    return RevisionInfo(branch, i+1,)
310
311
SPEC_TYPES.append(RevisionSpec_date)
1185.2.18 by Lalo Martins
merging from integration again.
312
313
314
class RevisionSpec_ancestor(RevisionSpec):
315
    prefix = 'ancestor:'
316
317
    def _match_on(self, branch, revs):
318
        from branch import Branch
319
        from revision import common_ancestor, MultipleRevisionSources
320
        other_branch = Branch.open_containing(self.spec)
321
        revision_a = branch.last_patch()
322
        revision_b = other_branch.last_patch()
323
        for r, b in ((revision_a, branch), (revision_b, other_branch)):
324
            if r is None:
325
                raise NoCommits(b)
326
        revision_source = MultipleRevisionSources(branch, other_branch)
327
        rev_id = common_ancestor(revision_a, revision_b, revision_source)
328
        try:
329
            revno = branch.revision_id_to_revno(rev_id)
330
        except NoSuchRevision:
331
            revno = None
332
        return RevisionInfo(branch, revno, rev_id)
333
        
334
SPEC_TYPES.append(RevisionSpec_ancestor)