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

Remove more cases of getting transport via control_files

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# (C) 2005 Canonical
2
 
 
 
1
# Copyright (C) 2005, 2006 Canonical Ltd
 
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
5
5
# the Free Software Foundation; either version 2 of the License, or
6
6
# (at your option) any later version.
7
 
 
 
7
#
8
8
# This program is distributed in the hope that it will be useful,
9
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
11
# GNU General Public License for more details.
12
 
 
 
12
#
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
16
 
17
 
 
18
 
 
19
 
 
20
 
class RevisionReference(object):
21
 
    """
22
 
    Reference to a stored revision.
23
 
 
24
 
    Includes the revision_id and revision_sha1.
25
 
    """
26
 
    revision_id = None
27
 
    revision_sha1 = None
28
 
    def __init__(self, revision_id, revision_sha1=None):
29
 
        if revision_id == None \
30
 
           or isinstance(revision_id, basestring):
31
 
            self.revision_id = revision_id
32
 
        else:
33
 
            raise ValueError('bad revision_id %r' % revision_id)
34
 
 
35
 
        if revision_sha1 != None:
36
 
            if isinstance(revision_sha1, basestring) \
37
 
               and len(revision_sha1) == 40:
38
 
                self.revision_sha1 = revision_sha1
39
 
            else:
40
 
                raise ValueError('bad revision_sha1 %r' % revision_sha1)
41
 
                
 
17
# TODO: Some kind of command-line display of revision properties: 
 
18
# perhaps show them in log -v and allow them as options to the commit command.
 
19
 
 
20
 
 
21
from bzrlib import (
 
22
    errors,
 
23
    symbol_versioning,
 
24
    )
 
25
from bzrlib.deprecated_graph import (
 
26
    all_descendants,
 
27
    Graph,
 
28
    node_distances,
 
29
    select_farthest,
 
30
    )
 
31
from bzrlib.osutils import contains_whitespace
 
32
from bzrlib.progress import DummyProgress
 
33
from bzrlib.symbol_versioning import (deprecated_function,
 
34
        )
 
35
 
 
36
NULL_REVISION="null:"
 
37
CURRENT_REVISION="current:"
42
38
 
43
39
 
44
40
class Revision(object):
50
46
 
51
47
    After bzr 0.0.5 revisions are allowed to have multiple parents.
52
48
 
53
 
    parents
54
 
        List of parent revisions, each is a RevisionReference.
 
49
    parent_ids
 
50
        List of parent revision_ids
 
51
 
 
52
    properties
 
53
        Dictionary of revision properties.  These are attached to the
 
54
        revision as extra metadata.  The name must be a single 
 
55
        word; the value can be an arbitrary string.
55
56
    """
56
 
    inventory_id = None
57
 
    inventory_sha1 = None
58
 
    revision_id = None
59
 
    timestamp = None
60
 
    message = None
61
 
    timezone = None
62
 
    committer = None
63
57
    
64
 
    def __init__(self, **args):
 
58
    def __init__(self, revision_id, properties=None, **args):
 
59
        self.revision_id = revision_id
 
60
        self.properties = properties or {}
 
61
        self._check_properties()
 
62
        self.parent_ids = []
 
63
        self.parent_sha1s = []
 
64
        """Not used anymore - legacy from for 4."""
65
65
        self.__dict__.update(args)
66
 
        self.parents = []
67
 
 
68
66
 
69
67
    def __repr__(self):
70
68
        return "<Revision id %s>" % self.revision_id
71
69
 
72
 
        
73
 
    def to_element(self):
74
 
        from bzrlib.xml import Element, SubElement
75
 
        
76
 
        root = Element('revision',
77
 
                       committer = self.committer,
78
 
                       timestamp = '%.9f' % self.timestamp,
79
 
                       revision_id = self.revision_id,
80
 
                       inventory_id = self.inventory_id,
81
 
                       inventory_sha1 = self.inventory_sha1,
82
 
                       )
83
 
        if self.timezone:
84
 
            root.set('timezone', str(self.timezone))
85
 
        root.text = '\n'
86
 
        
87
 
        msg = SubElement(root, 'message')
88
 
        msg.text = self.message
89
 
        msg.tail = '\n'
90
 
 
91
 
        if self.parents:
92
 
            pelts = SubElement(root, 'parents')
93
 
            pelts.tail = pelts.text = '\n'
94
 
            for rr in self.parents:
95
 
                assert isinstance(rr, RevisionReference)
96
 
                p = SubElement(pelts, 'revision_ref')
97
 
                p.tail = '\n'
98
 
                assert rr.revision_id
99
 
                p.set('revision_id', rr.revision_id)
100
 
                if rr.revision_sha1:
101
 
                    p.set('revision_sha1', rr.revision_sha1)
102
 
 
103
 
        return root
104
 
 
105
 
 
106
 
    def from_element(cls, elt):
107
 
        return unpack_revision(elt)
108
 
 
109
 
    from_element = classmethod(from_element)
110
 
 
111
 
 
112
 
 
113
 
def unpack_revision(elt):
114
 
    """Convert XML element into Revision object."""
115
 
    # <changeset> is deprecated...
116
 
    from bzrlib.errors import BzrError
117
 
    
118
 
    if elt.tag not in ('revision', 'changeset'):
119
 
        raise BzrError("unexpected tag in revision file: %r" % elt)
120
 
 
121
 
    rev = Revision(committer = elt.get('committer'),
122
 
                   timestamp = float(elt.get('timestamp')),
123
 
                   revision_id = elt.get('revision_id'),
124
 
                   inventory_id = elt.get('inventory_id'),
125
 
                   inventory_sha1 = elt.get('inventory_sha1')
126
 
                   )
127
 
 
128
 
    precursor = elt.get('precursor')
129
 
    precursor_sha1 = elt.get('precursor_sha1')
130
 
 
131
 
    pelts = elt.find('parents')
132
 
 
133
 
    if pelts:
134
 
        for p in pelts:
135
 
            assert p.tag == 'revision_ref', \
136
 
                   "bad parent node tag %r" % p.tag
137
 
            rev_ref = RevisionReference(p.get('revision_id'),
138
 
                                        p.get('revision_sha1'))
139
 
            rev.parents.append(rev_ref)
140
 
 
141
 
        if precursor:
142
 
            # must be consistent
143
 
            prec_parent = rev.parents[0].revision_id
144
 
            assert prec_parent == precursor
145
 
    elif precursor:
146
 
        # revisions written prior to 0.0.5 have a single precursor
147
 
        # give as an attribute
148
 
        rev_ref = RevisionReference(precursor, precursor_sha1)
149
 
        rev.parents.append(rev_ref)
150
 
 
151
 
    v = elt.get('timezone')
152
 
    rev.timezone = v and int(v)
153
 
 
154
 
    rev.message = elt.findtext('message') # text of <message>
155
 
    return rev
156
 
 
157
 
 
158
 
 
159
 
REVISION_ID_RE = None
160
 
 
161
 
def validate_revision_id(rid):
162
 
    """Check rid is syntactically valid for a revision id."""
163
 
    global REVISION_ID_RE
164
 
    if not REVISION_ID_RE:
165
 
        import re
166
 
        REVISION_ID_RE = re.compile('[\w.-]+@[\w.-]+--?\d+--?[0-9a-f]+\Z')
167
 
 
168
 
    if not REVISION_ID_RE.match(rid):
169
 
        raise ValueError("malformed revision-id %r" % rid)
170
 
 
171
 
def is_ancestor(revision_id, candidate_id, revision_source):
172
 
    """Return true if candidate_id is an ancestor of revision_id.
173
 
    A false negative will be returned if any intermediate descendent of
174
 
    candidate_id is not present in any of the revision_sources.
175
 
    
176
 
    revisions_source is an object supporting a get_revision operation that
177
 
    behaves like Branch's.
178
 
    """
179
 
 
180
 
    from bzrlib.branch import NoSuchRevision
 
70
    def __eq__(self, other):
 
71
        if not isinstance(other, Revision):
 
72
            return False
 
73
        # FIXME: rbc 20050930 parent_ids are not being compared
 
74
        return (
 
75
                self.inventory_sha1 == other.inventory_sha1
 
76
                and self.revision_id == other.revision_id
 
77
                and self.timestamp == other.timestamp
 
78
                and self.message == other.message
 
79
                and self.timezone == other.timezone
 
80
                and self.committer == other.committer
 
81
                and self.properties == other.properties)
 
82
 
 
83
    def __ne__(self, other):
 
84
        return not self.__eq__(other)
 
85
 
 
86
    def _check_properties(self):
 
87
        """Verify that all revision properties are OK."""
 
88
        for name, value in self.properties.iteritems():
 
89
            if not isinstance(name, basestring) or contains_whitespace(name):
 
90
                raise ValueError("invalid property name %r" % name)
 
91
            if not isinstance(value, basestring):
 
92
                raise ValueError("invalid property value %r for %r" % 
 
93
                                 (name, value))
 
94
 
 
95
    def get_history(self, repository):
 
96
        """Return the canonical line-of-history for this revision.
 
97
 
 
98
        If ghosts are present this may differ in result from a ghost-free
 
99
        repository.
 
100
        """
 
101
        current_revision = self
 
102
        reversed_result = []
 
103
        while current_revision is not None:
 
104
            reversed_result.append(current_revision.revision_id)
 
105
            if not len (current_revision.parent_ids):
 
106
                reversed_result.append(None)
 
107
                current_revision = None
 
108
            else:
 
109
                next_revision_id = current_revision.parent_ids[0]
 
110
                current_revision = repository.get_revision(next_revision_id)
 
111
        reversed_result.reverse()
 
112
        return reversed_result
 
113
 
 
114
    def get_summary(self):
 
115
        """Get the first line of the log message for this revision.
 
116
        """
 
117
        return self.message.lstrip().split('\n', 1)[0]
 
118
 
 
119
    def get_apparent_author(self):
 
120
        """Return the apparent author of this revision.
 
121
 
 
122
        If the revision properties contain the author name,
 
123
        return it. Otherwise return the committer name.
 
124
        """
 
125
        return self.properties.get('author', self.committer)
 
126
 
 
127
 
 
128
def iter_ancestors(revision_id, revision_source, only_present=False):
181
129
    ancestors = (revision_id,)
 
130
    distance = 0
182
131
    while len(ancestors) > 0:
183
132
        new_ancestors = []
184
133
        for ancestor in ancestors:
185
 
            if ancestor == candidate_id:
186
 
                return True
 
134
            if not only_present:
 
135
                yield ancestor, distance
187
136
            try:
188
137
                revision = revision_source.get_revision(ancestor)
189
 
            except NoSuchRevision, e:
 
138
            except errors.NoSuchRevision, e:
190
139
                if e.revision == revision_id:
191
 
                    raise e
 
140
                    raise 
192
141
                else:
193
142
                    continue
194
 
            new_ancestors.extend([p.revision_id for p in revision.parents])
 
143
            if only_present:
 
144
                yield ancestor, distance
 
145
            new_ancestors.extend(revision.parent_ids)
195
146
        ancestors = new_ancestors
 
147
        distance += 1
 
148
 
 
149
 
 
150
def find_present_ancestors(revision_id, revision_source):
 
151
    """Return the ancestors of a revision present in a branch.
 
152
 
 
153
    It's possible that a branch won't have the complete ancestry of
 
154
    one of its revisions.  
 
155
 
 
156
    """
 
157
    found_ancestors = {}
 
158
    anc_iter = enumerate(iter_ancestors(revision_id, revision_source,
 
159
                         only_present=True))
 
160
    for anc_order, (anc_id, anc_distance) in anc_iter:
 
161
        if anc_id not in found_ancestors:
 
162
            found_ancestors[anc_id] = (anc_order, anc_distance)
 
163
    return found_ancestors
 
164
    
 
165
 
 
166
def __get_closest(intersection):
 
167
    intersection.sort()
 
168
    matches = [] 
 
169
    for entry in intersection:
 
170
        if entry[0] == intersection[0][0]:
 
171
            matches.append(entry[2])
 
172
    return matches
 
173
 
 
174
 
 
175
@deprecated_function(symbol_versioning.one_four)
 
176
def revision_graph(revision, revision_source):
 
177
    """Produce a graph of the ancestry of the specified revision.
 
178
    
 
179
    :return: root, ancestors map, descendants map
 
180
    """
 
181
    revision_source.lock_read()
 
182
    try:
 
183
        return _revision_graph(revision, revision_source)
 
184
    finally:
 
185
        revision_source.unlock()
 
186
 
 
187
 
 
188
def _revision_graph(revision, revision_source):
 
189
    """See revision_graph."""
 
190
    from bzrlib.tsort import topo_sort
 
191
    graph = revision_source.get_revision_graph(revision)
 
192
    # mark all no-parent revisions as being NULL_REVISION parentage.
 
193
    for node, parents in graph.items():
 
194
        if len(parents) == 0:
 
195
            graph[node] = [NULL_REVISION]
 
196
    # add NULL_REVISION to the graph
 
197
    graph[NULL_REVISION] = []
 
198
 
 
199
    # pick a root. If there are multiple roots
 
200
    # this could pick a random one.
 
201
    topo_order = topo_sort(graph.items())
 
202
    root = topo_order[0]
 
203
 
 
204
    ancestors = {}
 
205
    descendants = {}
 
206
 
 
207
    # map the descendants of the graph.
 
208
    # and setup our set based return graph.
 
209
    for node in graph.keys():
 
210
        descendants[node] = {}
 
211
    for node, parents in graph.items():
 
212
        for parent in parents:
 
213
            descendants[parent][node] = 1
 
214
        ancestors[node] = set(parents)
 
215
    return root, ancestors, descendants
 
216
 
 
217
 
 
218
@deprecated_function(symbol_versioning.one_three)
 
219
def combined_graph(revision_a, revision_b, revision_source):
 
220
    """Produce a combined ancestry graph.
 
221
    Return graph root, ancestors map, descendants map, set of common nodes"""
 
222
    root, ancestors, descendants = revision_graph(
 
223
        revision_a, revision_source)
 
224
    root_b, ancestors_b, descendants_b = revision_graph(
 
225
        revision_b, revision_source)
 
226
    if root != root_b:
 
227
        raise errors.NoCommonRoot(revision_a, revision_b)
 
228
    common = set()
 
229
    for node, node_anc in ancestors_b.iteritems():
 
230
        if node in ancestors:
 
231
            common.add(node)
 
232
        else:
 
233
            ancestors[node] = set()
 
234
        ancestors[node].update(node_anc)
 
235
    for node, node_dec in descendants_b.iteritems():
 
236
        if node not in descendants:
 
237
            descendants[node] = {}
 
238
        descendants[node].update(node_dec)
 
239
    return root, ancestors, descendants, common
 
240
 
 
241
 
 
242
@deprecated_function(symbol_versioning.one_three)
 
243
def common_ancestor(revision_a, revision_b, revision_source, 
 
244
                    pb=DummyProgress()):
 
245
    if None in (revision_a, revision_b):
 
246
        return None
 
247
    if NULL_REVISION in (revision_a, revision_b):
 
248
        return NULL_REVISION
 
249
    # trivial optimisation
 
250
    if revision_a == revision_b:
 
251
        return revision_a
 
252
    try:
 
253
        try:
 
254
            pb.update('Picking ancestor', 1, 3)
 
255
            graph = revision_source.get_revision_graph_with_ghosts(
 
256
                [revision_a, revision_b])
 
257
            # Shortcut the case where one of the tips is already included in
 
258
            # the other graphs ancestry.
 
259
            ancestry_a = graph.get_ancestry(revision_a, topo_sorted=False)
 
260
            if revision_b in ancestry_a:
 
261
                return revision_b
 
262
            ancestry_b = graph.get_ancestry(revision_b, topo_sorted=False)
 
263
            if revision_a in ancestry_b:
 
264
                return revision_a
 
265
            # convert to a NULL_REVISION based graph.
 
266
            ancestors = graph.get_ancestors()
 
267
            descendants = graph.get_descendants()
 
268
            common = set(ancestry_a)
 
269
            common.intersection_update(ancestry_b)
 
270
            descendants[NULL_REVISION] = {}
 
271
            ancestors[NULL_REVISION] = []
 
272
            for root in graph.roots:
 
273
                descendants[NULL_REVISION][root] = 1
 
274
                ancestors[root].append(NULL_REVISION)
 
275
            for ghost in graph.ghosts:
 
276
                # ghosts act as roots for the purpose of finding 
 
277
                # the longest paths from the root: any ghost *might*
 
278
                # be directly attached to the root, so we treat them
 
279
                # as being such.
 
280
                # ghost now descends from NULL
 
281
                descendants[NULL_REVISION][ghost] = 1
 
282
                # that is it has an ancestor of NULL
 
283
                ancestors[ghost] = [NULL_REVISION]
 
284
                # ghost is common if any of ghosts descendants are common:
 
285
                for ghost_descendant in descendants[ghost]:
 
286
                    if ghost_descendant in common:
 
287
                        common.add(ghost)
 
288
                
 
289
            root = NULL_REVISION
 
290
            common.add(NULL_REVISION)
 
291
        except errors.NoCommonRoot:
 
292
            raise errors.NoCommonAncestor(revision_a, revision_b)
 
293
            
 
294
        pb.update('Picking ancestor', 2, 3)
 
295
        distances = node_distances (descendants, ancestors, root)
 
296
        pb.update('Picking ancestor', 3, 2)
 
297
        farthest = select_farthest(distances, common)
 
298
        if farthest is None or farthest == NULL_REVISION:
 
299
            raise errors.NoCommonAncestor(revision_a, revision_b)
 
300
    finally:
 
301
        pb.clear()
 
302
    return farthest
196
303
 
197
304
 
198
305
class MultipleRevisionSources(object):
 
306
    """Proxy that looks in multiple branches for revisions."""
 
307
 
 
308
    @symbol_versioning.deprecated_method(symbol_versioning.one_three)
199
309
    def __init__(self, *args):
200
310
        object.__init__(self)
201
 
        assert len(args) != 0
202
311
        self._revision_sources = args
203
312
 
 
313
    def revision_parents(self, revision_id):
 
314
        for source in self._revision_sources:
 
315
            try:
 
316
                return source.revision_parents(revision_id)
 
317
            except (errors.WeaveRevisionNotPresent, errors.NoSuchRevision), e:
 
318
                pass
 
319
        raise e
 
320
 
204
321
    def get_revision(self, revision_id):
205
 
        from bzrlib.branch import NoSuchRevision
206
322
        for source in self._revision_sources:
207
323
            try:
208
324
                return source.get_revision(revision_id)
209
 
            except NoSuchRevision, e:
 
325
            except errors.NoSuchRevision, e:
210
326
                pass
211
327
        raise e
 
328
 
 
329
    def get_revision_graph(self, revision_id):
 
330
        # we could probe incrementally until the pending
 
331
        # ghosts list stop growing, but its cheaper for now
 
332
        # to just ask for the complete graph for each repository.
 
333
        graphs = []
 
334
        for source in self._revision_sources:
 
335
            ghost_graph = source.get_revision_graph_with_ghosts()
 
336
            graphs.append(ghost_graph)
 
337
        absent = 0
 
338
        for graph in graphs:
 
339
            if not revision_id in graph.get_ancestors():
 
340
                absent += 1
 
341
        if absent == len(graphs):
 
342
            raise errors.NoSuchRevision(self._revision_sources[0], revision_id)
 
343
 
 
344
        # combine the graphs
 
345
        result = {}
 
346
        pending = set([revision_id])
 
347
        def find_parents(node_id):
 
348
            """find the parents for node_id."""
 
349
            for graph in graphs:
 
350
                ancestors = graph.get_ancestors()
 
351
                try:
 
352
                    return ancestors[node_id]
 
353
                except KeyError:
 
354
                    pass
 
355
            raise errors.NoSuchRevision(self._revision_sources[0], node_id)
 
356
        while len(pending):
 
357
            # all the graphs should have identical parent lists
 
358
            node_id = pending.pop()
 
359
            try:
 
360
                result[node_id] = find_parents(node_id)
 
361
                for parent_node in result[node_id]:
 
362
                    if not parent_node in result:
 
363
                        pending.add(parent_node)
 
364
            except errors.NoSuchRevision:
 
365
                # ghost, ignore it.
 
366
                pass
 
367
        return result
 
368
 
 
369
    def get_revision_graph_with_ghosts(self, revision_ids):
 
370
        # query all the sources for their entire graphs 
 
371
        # and then build a combined graph for just
 
372
        # revision_ids.
 
373
        graphs = []
 
374
        for source in self._revision_sources:
 
375
            ghost_graph = source.get_revision_graph_with_ghosts()
 
376
            graphs.append(ghost_graph.get_ancestors())
 
377
        for revision_id in revision_ids:
 
378
            absent = 0
 
379
            for graph in graphs:
 
380
                    if not revision_id in graph:
 
381
                        absent += 1
 
382
            if absent == len(graphs):
 
383
                raise errors.NoSuchRevision(self._revision_sources[0],
 
384
                                            revision_id)
 
385
 
 
386
        # combine the graphs
 
387
        result = Graph()
 
388
        pending = set(revision_ids)
 
389
        done = set()
 
390
        def find_parents(node_id):
 
391
            """find the parents for node_id."""
 
392
            for graph in graphs:
 
393
                try:
 
394
                    return graph[node_id]
 
395
                except KeyError:
 
396
                    pass
 
397
            raise errors.NoSuchRevision(self._revision_sources[0], node_id)
 
398
        while len(pending):
 
399
            # all the graphs should have identical parent lists
 
400
            node_id = pending.pop()
 
401
            try:
 
402
                parents = find_parents(node_id)
 
403
                for parent_node in parents:
 
404
                    # queued or done? 
 
405
                    if (parent_node not in pending and
 
406
                        parent_node not in done):
 
407
                        # no, queue
 
408
                        pending.add(parent_node)
 
409
                result.add_node(node_id, parents)
 
410
                done.add(node_id)
 
411
            except errors.NoSuchRevision:
 
412
                # ghost
 
413
                result.add_ghost(node_id)
 
414
                continue
 
415
        return result
 
416
 
 
417
    def lock_read(self):
 
418
        for source in self._revision_sources:
 
419
            source.lock_read()
 
420
 
 
421
    def unlock(self):
 
422
        for source in self._revision_sources:
 
423
            source.unlock()
 
424
 
 
425
 
 
426
def is_reserved_id(revision_id):
 
427
    """Determine whether a revision id is reserved
 
428
 
 
429
    :return: True if the revision is is reserved, False otherwise
 
430
    """
 
431
    return isinstance(revision_id, basestring) and revision_id.endswith(':')
 
432
 
 
433
 
 
434
def check_not_reserved_id(revision_id):
 
435
    """Raise ReservedId if the supplied revision_id is reserved"""
 
436
    if is_reserved_id(revision_id):
 
437
        raise errors.ReservedId(revision_id)
 
438
 
 
439
 
 
440
def ensure_null(revision_id):
 
441
    """Ensure only NULL_REVISION is used to represent the null revision"""
 
442
    if revision_id is None:
 
443
        symbol_versioning.warn('NULL_REVISION should be used for the null'
 
444
            ' revision instead of None, as of bzr 0.91.',
 
445
            DeprecationWarning, stacklevel=2)
 
446
        return NULL_REVISION
 
447
    else:
 
448
        return revision_id
 
449
 
 
450
 
 
451
def is_null(revision_id):
 
452
    if revision_id is None:
 
453
        symbol_versioning.warn('NULL_REVISION should be used for the null'
 
454
            ' revision instead of None, as of bzr 0.90.',
 
455
            DeprecationWarning, stacklevel=2)
 
456
    return revision_id in (None, NULL_REVISION)