/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: Martin Pool
  • Date: 2008-05-08 04:33:38 UTC
  • mfrom: (3414 +trunk)
  • mto: This revision was merged to the branch mainline in revision 3415.
  • Revision ID: mbp@sourcefrog.net-20080508043338-ru3vflx8z641a76k
Merge trunk

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005-2010 Canonical Ltd
 
1
# Copyright (C) 2005, 2006 Canonical Ltd
2
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
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
 
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
16
 
17
 
# TODO: Some kind of command-line display of revision properties:
 
17
# TODO: Some kind of command-line display of revision properties: 
18
18
# perhaps show them in log -v and allow them as options to the commit command.
19
19
 
20
20
 
21
 
from bzrlib.lazy_import import lazy_import
22
 
lazy_import(globals(), """
23
 
from bzrlib import deprecated_graph
24
 
from bzrlib import bugtracker
25
 
""")
26
21
from bzrlib import (
27
22
    errors,
28
23
    symbol_versioning,
29
24
    )
 
25
from bzrlib.deprecated_graph import (
 
26
    all_descendants,
 
27
    Graph,
 
28
    node_distances,
 
29
    select_farthest,
 
30
    )
30
31
from bzrlib.osutils import contains_whitespace
 
32
from bzrlib.progress import DummyProgress
 
33
from bzrlib.symbol_versioning import (deprecated_function,
 
34
        )
31
35
 
32
36
NULL_REVISION="null:"
33
37
CURRENT_REVISION="current:"
47
51
 
48
52
    properties
49
53
        Dictionary of revision properties.  These are attached to the
50
 
        revision as extra metadata.  The name must be a single
 
54
        revision as extra metadata.  The name must be a single 
51
55
        word; the value can be an arbitrary string.
52
56
    """
53
 
 
 
57
    
54
58
    def __init__(self, revision_id, properties=None, **args):
55
59
        self.revision_id = revision_id
56
 
        if properties is None:
57
 
            self.properties = {}
58
 
        else:
59
 
            self.properties = properties
60
 
            self._check_properties()
61
 
        self.committer = None
 
60
        self.properties = properties or {}
 
61
        self._check_properties()
62
62
        self.parent_ids = []
63
63
        self.parent_sha1s = []
64
64
        """Not used anymore - legacy from for 4."""
70
70
    def __eq__(self, other):
71
71
        if not isinstance(other, Revision):
72
72
            return False
 
73
        # FIXME: rbc 20050930 parent_ids are not being compared
73
74
        return (
74
75
                self.inventory_sha1 == other.inventory_sha1
75
76
                and self.revision_id == other.revision_id
77
78
                and self.message == other.message
78
79
                and self.timezone == other.timezone
79
80
                and self.committer == other.committer
80
 
                and self.properties == other.properties
81
 
                and self.parent_ids == other.parent_ids)
 
81
                and self.properties == other.properties)
82
82
 
83
83
    def __ne__(self, other):
84
84
        return not self.__eq__(other)
89
89
            if not isinstance(name, basestring) or contains_whitespace(name):
90
90
                raise ValueError("invalid property name %r" % name)
91
91
            if not isinstance(value, basestring):
92
 
                raise ValueError("invalid property value %r for %r" %
93
 
                                 (value, name))
 
92
                raise ValueError("invalid property value %r for %r" % 
 
93
                                 (name, value))
94
94
 
95
95
    def get_history(self, repository):
96
96
        """Return the canonical line-of-history for this revision.
113
113
 
114
114
    def get_summary(self):
115
115
        """Get the first line of the log message for this revision.
116
 
 
117
 
        Return an empty string if message is None.
118
116
        """
119
 
        if self.message:
120
 
            return self.message.lstrip().split('\n', 1)[0]
121
 
        else:
122
 
            return ''
 
117
        return self.message.lstrip().split('\n', 1)[0]
123
118
 
124
 
    @symbol_versioning.deprecated_method(symbol_versioning.deprecated_in((1, 13, 0)))
125
119
    def get_apparent_author(self):
126
120
        """Return the apparent author of this revision.
127
121
 
128
 
        This method is deprecated in favour of get_apparent_authors.
129
 
 
130
 
        If the revision properties contain any author names,
131
 
        return the first. Otherwise return the committer name.
132
 
        """
133
 
        authors = self.get_apparent_authors()
134
 
        if authors:
135
 
            return authors[0]
136
 
        else:
137
 
            return None
138
 
 
139
 
    def get_apparent_authors(self):
140
 
        """Return the apparent authors of this revision.
141
 
 
142
 
        If the revision properties contain the names of the authors,
143
 
        return them. Otherwise return the committer name.
144
 
 
145
 
        The return value will be a list containing at least one element.
146
 
        """
147
 
        authors = self.properties.get('authors', None)
148
 
        if authors is None:
149
 
            author = self.properties.get('author', self.committer)
150
 
            if author is None:
151
 
                return []
152
 
            return [author]
153
 
        else:
154
 
            return authors.split("\n")
155
 
 
156
 
    def iter_bugs(self):
157
 
        """Iterate over the bugs associated with this revision."""
158
 
        bug_property = self.properties.get('bugs', None)
159
 
        if bug_property is None:
160
 
            return
161
 
        for line in bug_property.splitlines():
162
 
            try:
163
 
                url, status = line.split(None, 2)
164
 
            except ValueError:
165
 
                raise errors.InvalidLineInBugsProperty(line)
166
 
            if status not in bugtracker.ALLOWED_BUG_STATUSES:
167
 
                raise errors.InvalidBugStatus(status)
168
 
            yield url, status
 
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)
169
126
 
170
127
 
171
128
def iter_ancestors(revision_id, revision_source, only_present=False):
180
137
                revision = revision_source.get_revision(ancestor)
181
138
            except errors.NoSuchRevision, e:
182
139
                if e.revision == revision_id:
183
 
                    raise
 
140
                    raise 
184
141
                else:
185
142
                    continue
186
143
            if only_present:
194
151
    """Return the ancestors of a revision present in a branch.
195
152
 
196
153
    It's possible that a branch won't have the complete ancestry of
197
 
    one of its revisions.
 
154
    one of its revisions.  
198
155
 
199
156
    """
200
157
    found_ancestors = {}
204
161
        if anc_id not in found_ancestors:
205
162
            found_ancestors[anc_id] = (anc_order, anc_distance)
206
163
    return found_ancestors
207
 
 
 
164
    
208
165
 
209
166
def __get_closest(intersection):
210
167
    intersection.sort()
211
 
    matches = []
 
168
    matches = [] 
212
169
    for entry in intersection:
213
170
        if entry[0] == intersection[0][0]:
214
171
            matches.append(entry[2])
215
172
    return matches
216
173
 
217
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
 
303
 
 
304
 
 
305
class MultipleRevisionSources(object):
 
306
    """Proxy that looks in multiple branches for revisions."""
 
307
 
 
308
    @symbol_versioning.deprecated_method(symbol_versioning.one_three)
 
309
    def __init__(self, *args):
 
310
        object.__init__(self)
 
311
        self._revision_sources = args
 
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
 
 
321
    def get_revision(self, revision_id):
 
322
        for source in self._revision_sources:
 
323
            try:
 
324
                return source.get_revision(revision_id)
 
325
            except errors.NoSuchRevision, e:
 
326
                pass
 
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
 
218
426
def is_reserved_id(revision_id):
219
427
    """Determine whether a revision id is reserved
220
428
 
221
 
    :return: True if the revision is reserved, False otherwise
 
429
    :return: True if the revision is is reserved, False otherwise
222
430
    """
223
431
    return isinstance(revision_id, basestring) and revision_id.endswith(':')
224
432