/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

  • Committer: Ian Clatworthy
  • Date: 2008-06-06 11:55:58 UTC
  • mto: (3480.1.1 ianc-integration)
  • mto: This revision was merged to the branch mainline in revision 3481.
  • Revision ID: ian.clatworthy@canonical.com-20080606115558-qw0kh7p3dl1o6o9s
tweaks requested by jam & poolie during review

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005, 2006 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
# 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:"
 
38
 
 
39
 
 
40
class Revision(object):
 
41
    """Single revision on a branch.
 
42
 
 
43
    Revisions may know their revision_hash, but only once they've been
 
44
    written out.  This is not stored because you cannot write the hash
 
45
    into the file it describes.
 
46
 
 
47
    After bzr 0.0.5 revisions are allowed to have multiple parents.
 
48
 
 
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.
 
56
    """
 
57
    
 
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
        self.__dict__.update(args)
 
66
 
 
67
    def __repr__(self):
 
68
        return "<Revision id %s>" % self.revision_id
 
69
 
 
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):
 
129
    ancestors = (revision_id,)
 
130
    distance = 0
 
131
    while len(ancestors) > 0:
 
132
        new_ancestors = []
 
133
        for ancestor in ancestors:
 
134
            if not only_present:
 
135
                yield ancestor, distance
 
136
            try:
 
137
                revision = revision_source.get_revision(ancestor)
 
138
            except errors.NoSuchRevision, e:
 
139
                if e.revision == revision_id:
 
140
                    raise 
 
141
                else:
 
142
                    continue
 
143
            if only_present:
 
144
                yield ancestor, distance
 
145
            new_ancestors.extend(revision.parent_ids)
 
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
 
 
216
    assert root not in descendants[root]
 
217
    assert root not in ancestors[root]
 
218
    return root, ancestors, descendants
 
219
 
 
220
 
 
221
@deprecated_function(symbol_versioning.one_three)
 
222
def combined_graph(revision_a, revision_b, revision_source):
 
223
    """Produce a combined ancestry graph.
 
224
    Return graph root, ancestors map, descendants map, set of common nodes"""
 
225
    root, ancestors, descendants = revision_graph(
 
226
        revision_a, revision_source)
 
227
    root_b, ancestors_b, descendants_b = revision_graph(
 
228
        revision_b, revision_source)
 
229
    if root != root_b:
 
230
        raise errors.NoCommonRoot(revision_a, revision_b)
 
231
    common = set()
 
232
    for node, node_anc in ancestors_b.iteritems():
 
233
        if node in ancestors:
 
234
            common.add(node)
 
235
        else:
 
236
            ancestors[node] = set()
 
237
        ancestors[node].update(node_anc)
 
238
    for node, node_dec in descendants_b.iteritems():
 
239
        if node not in descendants:
 
240
            descendants[node] = {}
 
241
        descendants[node].update(node_dec)
 
242
    return root, ancestors, descendants, common
 
243
 
 
244
 
 
245
@deprecated_function(symbol_versioning.one_three)
 
246
def common_ancestor(revision_a, revision_b, revision_source, 
 
247
                    pb=DummyProgress()):
 
248
    if None in (revision_a, revision_b):
 
249
        return None
 
250
    if NULL_REVISION in (revision_a, revision_b):
 
251
        return NULL_REVISION
 
252
    # trivial optimisation
 
253
    if revision_a == revision_b:
 
254
        return revision_a
 
255
    try:
 
256
        try:
 
257
            pb.update('Picking ancestor', 1, 3)
 
258
            graph = revision_source.get_revision_graph_with_ghosts(
 
259
                [revision_a, revision_b])
 
260
            # Shortcut the case where one of the tips is already included in
 
261
            # the other graphs ancestry.
 
262
            ancestry_a = graph.get_ancestry(revision_a, topo_sorted=False)
 
263
            if revision_b in ancestry_a:
 
264
                return revision_b
 
265
            ancestry_b = graph.get_ancestry(revision_b, topo_sorted=False)
 
266
            if revision_a in ancestry_b:
 
267
                return revision_a
 
268
            # convert to a NULL_REVISION based graph.
 
269
            ancestors = graph.get_ancestors()
 
270
            descendants = graph.get_descendants()
 
271
            common = set(ancestry_a)
 
272
            common.intersection_update(ancestry_b)
 
273
            descendants[NULL_REVISION] = {}
 
274
            ancestors[NULL_REVISION] = []
 
275
            for root in graph.roots:
 
276
                descendants[NULL_REVISION][root] = 1
 
277
                ancestors[root].append(NULL_REVISION)
 
278
            for ghost in graph.ghosts:
 
279
                # ghosts act as roots for the purpose of finding 
 
280
                # the longest paths from the root: any ghost *might*
 
281
                # be directly attached to the root, so we treat them
 
282
                # as being such.
 
283
                # ghost now descends from NULL
 
284
                descendants[NULL_REVISION][ghost] = 1
 
285
                # that is it has an ancestor of NULL
 
286
                ancestors[ghost] = [NULL_REVISION]
 
287
                # ghost is common if any of ghosts descendants are common:
 
288
                for ghost_descendant in descendants[ghost]:
 
289
                    if ghost_descendant in common:
 
290
                        common.add(ghost)
 
291
                
 
292
            root = NULL_REVISION
 
293
            common.add(NULL_REVISION)
 
294
        except errors.NoCommonRoot:
 
295
            raise errors.NoCommonAncestor(revision_a, revision_b)
 
296
            
 
297
        pb.update('Picking ancestor', 2, 3)
 
298
        distances = node_distances (descendants, ancestors, root)
 
299
        pb.update('Picking ancestor', 3, 2)
 
300
        farthest = select_farthest(distances, common)
 
301
        if farthest is None or farthest == NULL_REVISION:
 
302
            raise errors.NoCommonAncestor(revision_a, revision_b)
 
303
    finally:
 
304
        pb.clear()
 
305
    return farthest
 
306
 
 
307
 
 
308
class MultipleRevisionSources(object):
 
309
    """Proxy that looks in multiple branches for revisions."""
 
310
 
 
311
    @symbol_versioning.deprecated_method(symbol_versioning.one_three)
 
312
    def __init__(self, *args):
 
313
        object.__init__(self)
 
314
        assert len(args) != 0
 
315
        self._revision_sources = args
 
316
 
 
317
    def revision_parents(self, revision_id):
 
318
        for source in self._revision_sources:
 
319
            try:
 
320
                return source.revision_parents(revision_id)
 
321
            except (errors.WeaveRevisionNotPresent, errors.NoSuchRevision), e:
 
322
                pass
 
323
        raise e
 
324
 
 
325
    def get_revision(self, revision_id):
 
326
        for source in self._revision_sources:
 
327
            try:
 
328
                return source.get_revision(revision_id)
 
329
            except errors.NoSuchRevision, e:
 
330
                pass
 
331
        raise e
 
332
 
 
333
    def get_revision_graph(self, revision_id):
 
334
        # we could probe incrementally until the pending
 
335
        # ghosts list stop growing, but its cheaper for now
 
336
        # to just ask for the complete graph for each repository.
 
337
        graphs = []
 
338
        for source in self._revision_sources:
 
339
            ghost_graph = source.get_revision_graph_with_ghosts()
 
340
            graphs.append(ghost_graph)
 
341
        absent = 0
 
342
        for graph in graphs:
 
343
            if not revision_id in graph.get_ancestors():
 
344
                absent += 1
 
345
        if absent == len(graphs):
 
346
            raise errors.NoSuchRevision(self._revision_sources[0], revision_id)
 
347
 
 
348
        # combine the graphs
 
349
        result = {}
 
350
        pending = set([revision_id])
 
351
        def find_parents(node_id):
 
352
            """find the parents for node_id."""
 
353
            for graph in graphs:
 
354
                ancestors = graph.get_ancestors()
 
355
                try:
 
356
                    return ancestors[node_id]
 
357
                except KeyError:
 
358
                    pass
 
359
            raise errors.NoSuchRevision(self._revision_sources[0], node_id)
 
360
        while len(pending):
 
361
            # all the graphs should have identical parent lists
 
362
            node_id = pending.pop()
 
363
            try:
 
364
                result[node_id] = find_parents(node_id)
 
365
                for parent_node in result[node_id]:
 
366
                    if not parent_node in result:
 
367
                        pending.add(parent_node)
 
368
            except errors.NoSuchRevision:
 
369
                # ghost, ignore it.
 
370
                pass
 
371
        return result
 
372
 
 
373
    def get_revision_graph_with_ghosts(self, revision_ids):
 
374
        # query all the sources for their entire graphs 
 
375
        # and then build a combined graph for just
 
376
        # revision_ids.
 
377
        graphs = []
 
378
        for source in self._revision_sources:
 
379
            ghost_graph = source.get_revision_graph_with_ghosts()
 
380
            graphs.append(ghost_graph.get_ancestors())
 
381
        for revision_id in revision_ids:
 
382
            absent = 0
 
383
            for graph in graphs:
 
384
                    if not revision_id in graph:
 
385
                        absent += 1
 
386
            if absent == len(graphs):
 
387
                raise errors.NoSuchRevision(self._revision_sources[0],
 
388
                                            revision_id)
 
389
 
 
390
        # combine the graphs
 
391
        result = Graph()
 
392
        pending = set(revision_ids)
 
393
        done = set()
 
394
        def find_parents(node_id):
 
395
            """find the parents for node_id."""
 
396
            for graph in graphs:
 
397
                try:
 
398
                    return graph[node_id]
 
399
                except KeyError:
 
400
                    pass
 
401
            raise errors.NoSuchRevision(self._revision_sources[0], node_id)
 
402
        while len(pending):
 
403
            # all the graphs should have identical parent lists
 
404
            node_id = pending.pop()
 
405
            try:
 
406
                parents = find_parents(node_id)
 
407
                for parent_node in parents:
 
408
                    # queued or done? 
 
409
                    if (parent_node not in pending and
 
410
                        parent_node not in done):
 
411
                        # no, queue
 
412
                        pending.add(parent_node)
 
413
                result.add_node(node_id, parents)
 
414
                done.add(node_id)
 
415
            except errors.NoSuchRevision:
 
416
                # ghost
 
417
                result.add_ghost(node_id)
 
418
                continue
 
419
        return result
 
420
 
 
421
    def lock_read(self):
 
422
        for source in self._revision_sources:
 
423
            source.lock_read()
 
424
 
 
425
    def unlock(self):
 
426
        for source in self._revision_sources:
 
427
            source.unlock()
 
428
 
 
429
 
 
430
def is_reserved_id(revision_id):
 
431
    """Determine whether a revision id is reserved
 
432
 
 
433
    :return: True if the revision is is reserved, False otherwise
 
434
    """
 
435
    return isinstance(revision_id, basestring) and revision_id.endswith(':')
 
436
 
 
437
 
 
438
def check_not_reserved_id(revision_id):
 
439
    """Raise ReservedId if the supplied revision_id is reserved"""
 
440
    if is_reserved_id(revision_id):
 
441
        raise errors.ReservedId(revision_id)
 
442
 
 
443
 
 
444
def ensure_null(revision_id):
 
445
    """Ensure only NULL_REVISION is used to represent the null revision"""
 
446
    if revision_id is None:
 
447
        symbol_versioning.warn('NULL_REVISION should be used for the null'
 
448
            ' revision instead of None, as of bzr 0.91.',
 
449
            DeprecationWarning, stacklevel=2)
 
450
        return NULL_REVISION
 
451
    else:
 
452
        return revision_id
 
453
 
 
454
 
 
455
def is_null(revision_id):
 
456
    if revision_id is None:
 
457
        symbol_versioning.warn('NULL_REVISION should be used for the null'
 
458
            ' revision instead of None, as of bzr 0.90.',
 
459
            DeprecationWarning, stacklevel=2)
 
460
    return revision_id in (None, NULL_REVISION)