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.
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.
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
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.
21
from bzrlib.graph import node_distances, select_farthest, all_descendants
22
from bzrlib.osutils import contains_whitespace
23
from bzrlib.progress import DummyProgress
27
class Revision(object):
28
"""Single revision on a branch.
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.
34
After bzr 0.0.5 revisions are allowed to have multiple parents.
37
List of parent revision_ids
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.
45
def __init__(self, revision_id, properties=None, **args):
46
self.revision_id = revision_id
47
self.properties = properties or {}
48
self._check_properties()
50
self.parent_sha1s = []
51
self.__dict__.update(args)
54
return "<Revision id %s>" % self.revision_id
56
def __eq__(self, other):
57
if not isinstance(other, Revision):
59
# FIXME: rbc 20050930 parent_ids are not being compared
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)
69
def __ne__(self, other):
70
return not self.__eq__(other)
72
def _check_properties(self):
73
"""Verify that all revision properties are OK.
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" %
82
def get_history(self, repository):
83
"""Return the canonical line-of-history for this revision.
85
If ghosts are present this may differ in result from a ghost-free
88
current_revision = self
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
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
102
def is_ancestor(revision_id, candidate_id, branch):
103
"""Return true if candidate_id is an ancestor of revision_id.
105
A false negative will be returned if any intermediate descendent of
106
candidate_id is not present in any of the revision_sources.
108
revisions_source is an object supporting a get_revision operation that
109
behaves like Branch's.
111
return candidate_id in branch.repository.get_ancestry(revision_id)
114
def iter_ancestors(revision_id, revision_source, only_present=False):
115
ancestors = (revision_id,)
117
while len(ancestors) > 0:
119
for ancestor in ancestors:
121
yield ancestor, distance
123
revision = revision_source.get_revision(ancestor)
124
except bzrlib.errors.NoSuchRevision, e:
125
if e.revision == revision_id:
130
yield ancestor, distance
131
new_ancestors.extend(revision.parent_ids)
132
ancestors = new_ancestors
136
def find_present_ancestors(revision_id, revision_source):
137
"""Return the ancestors of a revision present in a branch.
139
It's possible that a branch won't have the complete ancestry of
140
one of its revisions.
144
anc_iter = enumerate(iter_ancestors(revision_id, revision_source,
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
152
def __get_closest(intersection):
155
for entry in intersection:
156
if entry[0] == intersection[0][0]:
157
matches.append(entry[2])
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
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
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.
171
revision_source.lock_read()
173
return _revision_graph(revision, revision_source)
175
revision_source.unlock()
177
def _revision_graph(revision, revision_source):
178
"""See revision_graph."""
183
descendants[revision] = {}
184
while len(lines) > 0:
187
if line == NULL_REVISION:
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:
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)
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.
217
descendants[root] = {}
218
# for every revision, check we can access at least
219
# one parent, if we cant, add NULL_REVISION and
221
for rev in ancestors:
222
if len(ancestors[rev]) == 0:
223
raise RuntimeError('unreachable code ?!')
225
for parent in ancestors[rev]:
226
if parent in ancestors:
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
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,
245
raise bzrlib.errors.NoCommonRoot(revision_a, revision_b)
247
for node, node_anc in ancestors_b.iteritems():
248
if node in ancestors:
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
260
def common_ancestor(revision_a, revision_b, revision_source,
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)
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)
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
288
def get_revision(self, revision_id):
289
for source in self._revision_sources:
291
return source.get_revision(revision_id)
292
except bzrlib.errors.NoSuchRevision, e:
297
for source in self._revision_sources:
301
for source in self._revision_sources:
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.
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
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)
326
def best_ancestor(rev_id):
328
for anc_id in ancestors[rev_id]:
330
distance = distances[anc_id]
333
if revision_history is not None and anc_id in revision_history:
335
elif best is None or distance > best[1]:
336
best = (anc_id, distance)
341
while next != ancestor_id:
343
next = best_ancestor(next)