/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: Robert Collins
  • Date: 2006-03-04 13:03:57 UTC
  • mfrom: (1505.1.30 bzr-bound-branch)
  • mto: This revision was merged to the branch mainline in revision 1590.
  • Revision ID: robertc@robertcollins.net-20060304130357-95990a95920f57bb
Update bound branch implementation to 0.8.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# (C) 2005 Canonical
 
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
import bzrlib.errors
 
21
from bzrlib.graph import node_distances, select_farthest, all_descendants
 
22
from bzrlib.osutils import contains_whitespace
 
23
from bzrlib.progress import DummyProgress
 
24
 
 
25
NULL_REVISION="null:"
 
26
 
 
27
class Revision(object):
 
28
    """Single revision on a branch.
 
29
 
 
30
    Revisions may know their revision_hash, but only once they've been
 
31
    written out.  This is not stored because you cannot write the hash
 
32
    into the file it describes.
 
33
 
 
34
    After bzr 0.0.5 revisions are allowed to have multiple parents.
 
35
 
 
36
    parent_ids
 
37
        List of parent revision_ids
 
38
 
 
39
    properties
 
40
        Dictionary of revision properties.  These are attached to the
 
41
        revision as extra metadata.  The name must be a single 
 
42
        word; the value can be an arbitrary string.
 
43
    """
 
44
    
 
45
    def __init__(self, revision_id, properties=None, **args):
 
46
        self.revision_id = revision_id
 
47
        self.properties = properties or {}
 
48
        self._check_properties()
 
49
        self.parent_ids = []
 
50
        self.parent_sha1s = []
 
51
        self.__dict__.update(args)
 
52
 
 
53
    def __repr__(self):
 
54
        return "<Revision id %s>" % self.revision_id
 
55
 
 
56
    def __eq__(self, other):
 
57
        if not isinstance(other, Revision):
 
58
            return False
 
59
        # FIXME: rbc 20050930 parent_ids are not being compared
 
60
        return (
 
61
                self.inventory_sha1 == other.inventory_sha1
 
62
                and self.revision_id == other.revision_id
 
63
                and self.timestamp == other.timestamp
 
64
                and self.message == other.message
 
65
                and self.timezone == other.timezone
 
66
                and self.committer == other.committer
 
67
                and self.properties == other.properties)
 
68
 
 
69
    def __ne__(self, other):
 
70
        return not self.__eq__(other)
 
71
 
 
72
    def _check_properties(self):
 
73
        """Verify that all revision properties are OK.
 
74
        """
 
75
        for name, value in self.properties.iteritems():
 
76
            if not isinstance(name, basestring) or contains_whitespace(name):
 
77
                raise ValueError("invalid property name %r" % name)
 
78
            if not isinstance(value, basestring):
 
79
                raise ValueError("invalid property value %r for %r" % 
 
80
                                 (name, value))
 
81
 
 
82
    def get_history(self, repository):
 
83
        """Return the canonical line-of-history for this revision.
 
84
 
 
85
        If ghosts are present this may differ in result from a ghost-free
 
86
        repository.
 
87
        """
 
88
        current_revision = self
 
89
        reversed_result = []
 
90
        while current_revision is not None:
 
91
            reversed_result.append(current_revision.revision_id)
 
92
            if not len (current_revision.parent_ids):
 
93
                reversed_result.append(None)
 
94
                current_revision = None
 
95
            else:
 
96
                next_revision_id = current_revision.parent_ids[0]
 
97
                current_revision = repository.get_revision(next_revision_id)
 
98
        reversed_result.reverse()
 
99
        return reversed_result
 
100
 
 
101
 
 
102
def is_ancestor(revision_id, candidate_id, branch):
 
103
    """Return true if candidate_id is an ancestor of revision_id.
 
104
 
 
105
    A false negative will be returned if any intermediate descendent of
 
106
    candidate_id is not present in any of the revision_sources.
 
107
    
 
108
    revisions_source is an object supporting a get_revision operation that
 
109
    behaves like Branch's.
 
110
    """
 
111
    return candidate_id in branch.repository.get_ancestry(revision_id)
 
112
 
 
113
 
 
114
def iter_ancestors(revision_id, revision_source, only_present=False):
 
115
    ancestors = (revision_id,)
 
116
    distance = 0
 
117
    while len(ancestors) > 0:
 
118
        new_ancestors = []
 
119
        for ancestor in ancestors:
 
120
            if not only_present:
 
121
                yield ancestor, distance
 
122
            try:
 
123
                revision = revision_source.get_revision(ancestor)
 
124
            except bzrlib.errors.NoSuchRevision, e:
 
125
                if e.revision == revision_id:
 
126
                    raise 
 
127
                else:
 
128
                    continue
 
129
            if only_present:
 
130
                yield ancestor, distance
 
131
            new_ancestors.extend(revision.parent_ids)
 
132
        ancestors = new_ancestors
 
133
        distance += 1
 
134
 
 
135
 
 
136
def find_present_ancestors(revision_id, revision_source):
 
137
    """Return the ancestors of a revision present in a branch.
 
138
 
 
139
    It's possible that a branch won't have the complete ancestry of
 
140
    one of its revisions.  
 
141
 
 
142
    """
 
143
    found_ancestors = {}
 
144
    anc_iter = enumerate(iter_ancestors(revision_id, revision_source,
 
145
                         only_present=True))
 
146
    for anc_order, (anc_id, anc_distance) in anc_iter:
 
147
        if not found_ancestors.has_key(anc_id):
 
148
            found_ancestors[anc_id] = (anc_order, anc_distance)
 
149
    return found_ancestors
 
150
    
 
151
 
 
152
def __get_closest(intersection):
 
153
    intersection.sort()
 
154
    matches = [] 
 
155
    for entry in intersection:
 
156
        if entry[0] == intersection[0][0]:
 
157
            matches.append(entry[2])
 
158
    return matches
 
159
 
 
160
 
 
161
def revision_graph(revision, revision_source):
 
162
    """Produce a graph of the ancestry of the specified revision.
 
163
    Return root, ancestors map, descendants map
 
164
 
 
165
    TODO: Produce graphs with the NULL revision as root, so that we can find
 
166
    a common even when trees are not branches don't represent a single line
 
167
    of descent.
 
168
    RBC: 20051024: note that when we have two partial histories, this may not
 
169
         be possible. But if we are willing to pretend :)... sure.
 
170
    """
 
171
    revision_source.lock_read()
 
172
    try:
 
173
        return _revision_graph(revision, revision_source)
 
174
    finally:
 
175
        revision_source.unlock()
 
176
 
 
177
def _revision_graph(revision, revision_source):
 
178
    """See revision_graph."""
 
179
    ancestors = {}
 
180
    descendants = {}
 
181
    lines = [revision]
 
182
    root = None
 
183
    descendants[revision] = {}
 
184
    while len(lines) > 0:
 
185
        new_lines = set()
 
186
        for line in lines:
 
187
            if line == NULL_REVISION:
 
188
                parents = []
 
189
                root = NULL_REVISION
 
190
            else:
 
191
                try:
 
192
                    rev = revision_source.get_revision(line)
 
193
                    parents = list(rev.parent_ids)
 
194
                    if len(parents) == 0:
 
195
                        parents = [NULL_REVISION]
 
196
                except bzrlib.errors.NoSuchRevision:
 
197
                    if line == revision:
 
198
                        raise
 
199
                    parents = None
 
200
            if parents is not None:
 
201
                for parent in parents:
 
202
                    if parent not in ancestors:
 
203
                        new_lines.add(parent)
 
204
                    if parent not in descendants:
 
205
                        descendants[parent] = {}
 
206
                    descendants[parent][line] = 1
 
207
            if parents is not None:
 
208
                ancestors[line] = set(parents)
 
209
        lines = new_lines
 
210
    if root is None:
 
211
        # The history for revision becomes inaccessible without
 
212
        # actually hitting a no-parents revision. This then
 
213
        # makes these asserts below trigger. So, if root is None
 
214
        # determine the actual root by walking the accessible tree
 
215
        # and then stash NULL_REVISION at the end.
 
216
        root = NULL_REVISION
 
217
        descendants[root] = {}
 
218
        # for every revision, check we can access at least
 
219
        # one parent, if we cant, add NULL_REVISION and
 
220
        # a link
 
221
        for rev in ancestors:
 
222
            if len(ancestors[rev]) == 0:
 
223
                raise RuntimeError('unreachable code ?!')
 
224
            ok = False
 
225
            for parent in ancestors[rev]:
 
226
                if parent in ancestors:
 
227
                    ok = True
 
228
            if ok:
 
229
                continue
 
230
            descendants[root][rev] = 1
 
231
            ancestors[rev].add(root)
 
232
        ancestors[root] = set()
 
233
    assert root not in descendants[root]
 
234
    assert root not in ancestors[root]
 
235
    return root, ancestors, descendants
 
236
 
 
237
 
 
238
def combined_graph(revision_a, revision_b, revision_source):
 
239
    """Produce a combined ancestry graph.
 
240
    Return graph root, ancestors map, descendants map, set of common nodes"""
 
241
    root, ancestors, descendants = revision_graph(revision_a, revision_source)
 
242
    root_b, ancestors_b, descendants_b = revision_graph(revision_b, 
 
243
                                                        revision_source)
 
244
    if root != root_b:
 
245
        raise bzrlib.errors.NoCommonRoot(revision_a, revision_b)
 
246
    common = set()
 
247
    for node, node_anc in ancestors_b.iteritems():
 
248
        if node in ancestors:
 
249
            common.add(node)
 
250
        else:
 
251
            ancestors[node] = set()
 
252
        ancestors[node].update(node_anc)
 
253
    for node, node_dec in descendants_b.iteritems():
 
254
        if node not in descendants:
 
255
            descendants[node] = {}
 
256
        descendants[node].update(node_dec)
 
257
    return root, ancestors, descendants, common
 
258
 
 
259
 
 
260
def common_ancestor(revision_a, revision_b, revision_source, 
 
261
                    pb=DummyProgress()):
 
262
    try:
 
263
        try:
 
264
            pb.update('Picking ancestor', 1, 3)
 
265
            root, ancestors, descendants, common = \
 
266
                combined_graph(revision_a, revision_b, revision_source)
 
267
        except bzrlib.errors.NoCommonRoot:
 
268
            raise bzrlib.errors.NoCommonAncestor(revision_a, revision_b)
 
269
            
 
270
        pb.update('Picking ancestor', 2, 3)
 
271
        distances = node_distances (descendants, ancestors, root)
 
272
        pb.update('Picking ancestor', 3, 2)
 
273
        farthest = select_farthest(distances, common)
 
274
        if farthest is None or farthest == NULL_REVISION:
 
275
            raise bzrlib.errors.NoCommonAncestor(revision_a, revision_b)
 
276
    finally:
 
277
        pb.clear()
 
278
    return farthest
 
279
 
 
280
 
 
281
class MultipleRevisionSources(object):
 
282
    """Proxy that looks in multiple branches for revisions."""
 
283
    def __init__(self, *args):
 
284
        object.__init__(self)
 
285
        assert len(args) != 0
 
286
        self._revision_sources = args
 
287
 
 
288
    def get_revision(self, revision_id):
 
289
        for source in self._revision_sources:
 
290
            try:
 
291
                return source.get_revision(revision_id)
 
292
            except bzrlib.errors.NoSuchRevision, e:
 
293
                pass
 
294
        raise e
 
295
 
 
296
    def lock_read(self):
 
297
        for source in self._revision_sources:
 
298
            source.lock_read()
 
299
 
 
300
    def unlock(self):
 
301
        for source in self._revision_sources:
 
302
            source.unlock()
 
303
 
 
304
def get_intervening_revisions(ancestor_id, rev_id, rev_source, 
 
305
                              revision_history=None):
 
306
    """Find the longest line of descent from maybe_ancestor to revision.
 
307
    Revision history is followed where possible.
 
308
 
 
309
    If ancestor_id == rev_id, list will be empty.
 
310
    Otherwise, rev_id will be the last entry.  ancestor_id will never appear.
 
311
    If ancestor_id is not an ancestor, NotAncestor will be thrown
 
312
    """
 
313
    root, ancestors, descendants = revision_graph(rev_id, rev_source)
 
314
    if len(descendants) == 0:
 
315
        raise NoSuchRevision(rev_source, rev_id)
 
316
    if ancestor_id not in descendants:
 
317
        rev_source.get_revision(ancestor_id)
 
318
        raise bzrlib.errors.NotAncestor(rev_id, ancestor_id)
 
319
    root_descendants = all_descendants(descendants, ancestor_id)
 
320
    root_descendants.add(ancestor_id)
 
321
    if rev_id not in root_descendants:
 
322
        raise bzrlib.errors.NotAncestor(rev_id, ancestor_id)
 
323
    distances = node_distances(descendants, ancestors, ancestor_id,
 
324
                               root_descendants=root_descendants)
 
325
 
 
326
    def best_ancestor(rev_id):
 
327
        best = None
 
328
        for anc_id in ancestors[rev_id]:
 
329
            try:
 
330
                distance = distances[anc_id]
 
331
            except KeyError:
 
332
                continue
 
333
            if revision_history is not None and anc_id in revision_history:
 
334
                return anc_id
 
335
            elif best is None or distance > best[1]:
 
336
                best = (anc_id, distance)
 
337
        return best[0]
 
338
 
 
339
    next = rev_id
 
340
    path = []
 
341
    while next != ancestor_id:
 
342
        path.append(next)
 
343
        next = best_ancestor(next)
 
344
    path.reverse()
 
345
    return path