~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/revisionspec.py

  • Committer: Robert Collins
  • Date: 2005-10-16 00:22:17 UTC
  • mto: This revision was merged to the branch mainline in revision 1457.
  • Revision ID: robertc@lifelesslap.robertcollins.net-20051016002217-aa38f9c1eb13ee48
Plugins are now loaded under bzrlib.plugins, not bzrlib.plugin.

Plugins are also made available for other plugins to use by making them 
accessible via import bzrlib.plugins.NAME. You should not import other
plugins during the __init__ of your plugin though, as no ordering is
guaranteed, and the plugins directory is not on the python path.

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 datetime
 
19
import re
 
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
 
 
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
 
 
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
 
 
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
 
 
154
        # FIXME: in_history is somewhat broken,
 
155
        # it will return non-history revisions in many
 
156
        # circumstances. The expected facility is that
 
157
        # in_history only returns revision-history revs,
 
158
        # in_store returns any rev. RBC 20051010
 
159
    # aliases for now, when we fix the core logic, then they
 
160
    # will do what you expect.
 
161
    in_store = in_history
 
162
    in_branch = in_store
 
163
        
 
164
    def __repr__(self):
 
165
        # this is mostly for helping with testing
 
166
        return '<%s %s%s>' % (self.__class__.__name__,
 
167
                              self.prefix or '',
 
168
                              self.spec)
 
169
 
 
170
 
 
171
# private API
 
172
 
 
173
class RevisionSpec_int(RevisionSpec):
 
174
    """Spec is a number.  Special case."""
 
175
    def __init__(self, spec):
 
176
        self.spec = int(spec)
 
177
 
 
178
    def _match_on(self, branch, revs):
 
179
        if self.spec < 0:
 
180
            revno = len(revs) + self.spec + 1
 
181
        else:
 
182
            revno = self.spec
 
183
        rev_id = branch.get_rev_id(revno, revs)
 
184
        return RevisionInfo(branch, revno, rev_id)
 
185
 
 
186
 
 
187
class RevisionSpec_revno(RevisionSpec):
 
188
    prefix = 'revno:'
 
189
 
 
190
    def _match_on(self, branch, revs):
 
191
        """Lookup a revision by revision number"""
 
192
        try:
 
193
            return RevisionInfo(branch, int(self.spec))
 
194
        except ValueError:
 
195
            return RevisionInfo(branch, None)
 
196
 
 
197
SPEC_TYPES.append(RevisionSpec_revno)
 
198
 
 
199
 
 
200
class RevisionSpec_revid(RevisionSpec):
 
201
    prefix = 'revid:'
 
202
 
 
203
    def _match_on(self, branch, revs):
 
204
        try:
 
205
            return RevisionInfo(branch, revs.index(self.spec) + 1, self.spec)
 
206
        except ValueError:
 
207
            return RevisionInfo(branch, None)
 
208
 
 
209
SPEC_TYPES.append(RevisionSpec_revid)
 
210
 
 
211
 
 
212
class RevisionSpec_last(RevisionSpec):
 
213
 
 
214
    prefix = 'last:'
 
215
 
 
216
    def _match_on(self, branch, revs):
 
217
        try:
 
218
            offset = int(self.spec)
 
219
        except ValueError:
 
220
            return RevisionInfo(branch, None)
 
221
        else:
 
222
            if offset <= 0:
 
223
                raise BzrError('You must supply a positive value for --revision last:XXX')
 
224
            return RevisionInfo(branch, len(revs) - offset + 1)
 
225
 
 
226
SPEC_TYPES.append(RevisionSpec_last)
 
227
 
 
228
 
 
229
class RevisionSpec_before(RevisionSpec):
 
230
 
 
231
    prefix = 'before:'
 
232
    
 
233
    def _match_on(self, branch, revs):
 
234
        r = RevisionSpec(self.spec)._match_on(branch, revs)
 
235
        if (r.revno is None) or (r.revno == 0):
 
236
            return r
 
237
        return RevisionInfo(branch, r.revno - 1)
 
238
 
 
239
SPEC_TYPES.append(RevisionSpec_before)
 
240
 
 
241
 
 
242
class RevisionSpec_tag(RevisionSpec):
 
243
    prefix = 'tag:'
 
244
 
 
245
    def _match_on(self, branch, revs):
 
246
        raise BzrError('tag: namespace registered, but not implemented.')
 
247
 
 
248
SPEC_TYPES.append(RevisionSpec_tag)
 
249
 
 
250
 
 
251
class RevisionSpec_date(RevisionSpec):
 
252
    prefix = 'date:'
 
253
    _date_re = re.compile(
 
254
            r'(?P<date>(?P<year>\d\d\d\d)-(?P<month>\d\d)-(?P<day>\d\d))?'
 
255
            r'(,|T)?\s*'
 
256
            r'(?P<time>(?P<hour>\d\d):(?P<minute>\d\d)(:(?P<second>\d\d))?)?'
 
257
        )
 
258
 
 
259
    def _match_on(self, branch, revs):
 
260
        """
 
261
        Spec for date revisions:
 
262
          date:value
 
263
          value can be 'yesterday', 'today', 'tomorrow' or a YYYY-MM-DD string.
 
264
          matches the first entry after a given date (either at midnight or
 
265
          at a specified time).
 
266
 
 
267
          So the proper way of saying 'give me all entries for today' is:
 
268
              -r date:today..date:tomorrow
 
269
        """
 
270
        today = datetime.datetime.fromordinal(datetime.date.today().toordinal())
 
271
        if self.spec.lower() == 'yesterday':
 
272
            dt = today - datetime.timedelta(days=1)
 
273
        elif self.spec.lower() == 'today':
 
274
            dt = today
 
275
        elif self.spec.lower() == 'tomorrow':
 
276
            dt = today + datetime.timedelta(days=1)
 
277
        else:
 
278
            m = self._date_re.match(self.spec)
 
279
            if not m or (not m.group('date') and not m.group('time')):
 
280
                raise BzrError('Invalid revision date %r' % self.spec)
 
281
 
 
282
            if m.group('date'):
 
283
                year, month, day = int(m.group('year')), int(m.group('month')), int(m.group('day'))
 
284
            else:
 
285
                year, month, day = today.year, today.month, today.day
 
286
            if m.group('time'):
 
287
                hour = int(m.group('hour'))
 
288
                minute = int(m.group('minute'))
 
289
                if m.group('second'):
 
290
                    second = int(m.group('second'))
 
291
                else:
 
292
                    second = 0
 
293
            else:
 
294
                hour, minute, second = 0,0,0
 
295
 
 
296
            dt = datetime.datetime(year=year, month=month, day=day,
 
297
                    hour=hour, minute=minute, second=second)
 
298
        first = dt
 
299
        for i in range(len(revs)):
 
300
            r = branch.get_revision(revs[i])
 
301
            # TODO: Handle timezone.
 
302
            dt = datetime.datetime.fromtimestamp(r.timestamp)
 
303
            if first <= dt:
 
304
                return RevisionInfo(branch, i+1)
 
305
        return RevisionInfo(branch, None)
 
306
 
 
307
SPEC_TYPES.append(RevisionSpec_date)
 
308
 
 
309
 
 
310
class RevisionSpec_ancestor(RevisionSpec):
 
311
    prefix = 'ancestor:'
 
312
 
 
313
    def _match_on(self, branch, revs):
 
314
        from branch import Branch
 
315
        from revision import common_ancestor, MultipleRevisionSources
 
316
        other_branch = Branch.open_containing(self.spec)
 
317
        revision_a = branch.last_revision()
 
318
        revision_b = other_branch.last_revision()
 
319
        for r, b in ((revision_a, branch), (revision_b, other_branch)):
 
320
            if r is None:
 
321
                raise NoCommits(b)
 
322
        revision_source = MultipleRevisionSources(branch, other_branch)
 
323
        rev_id = common_ancestor(revision_a, revision_b, revision_source)
 
324
        try:
 
325
            revno = branch.revision_id_to_revno(rev_id)
 
326
        except NoSuchRevision:
 
327
            revno = None
 
328
        return RevisionInfo(branch, revno, rev_id)
 
329
        
 
330
SPEC_TYPES.append(RevisionSpec_ancestor)
 
331
 
 
332
class RevisionSpec_branch(RevisionSpec):
 
333
    """A branch: revision specifier.
 
334
 
 
335
    This takes the path to a branch and returns its tip revision id.
 
336
    """
 
337
    prefix = 'branch:'
 
338
 
 
339
    def _match_on(self, branch, revs):
 
340
        from branch import Branch
 
341
        from fetch import greedy_fetch
 
342
        other_branch = Branch.open_containing(self.spec)
 
343
        revision_b = other_branch.last_revision()
 
344
        if revision_b is None:
 
345
            raise NoCommits(other_branch)
 
346
        # pull in the remote revisions so we can diff
 
347
        greedy_fetch(branch, other_branch, revision=revision_b)
 
348
        try:
 
349
            revno = branch.revision_id_to_revno(revision_b)
 
350
        except NoSuchRevision:
 
351
            revno = None
 
352
        return RevisionInfo(branch, revno, revision_b)
 
353
        
 
354
SPEC_TYPES.append(RevisionSpec_branch)