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
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
import bzrlib.errors as errors
22
from bzrlib.graph import node_distances, select_farthest, all_descendants, Graph
23
from bzrlib.osutils import contains_whitespace
24
from bzrlib.progress import DummyProgress
25
from bzrlib.symbol_versioning import (deprecated_function,
31
class Revision(object):
20
from xml import XMLMixin
23
from cElementTree import Element, ElementTree, SubElement
25
from elementtree.ElementTree import Element, ElementTree, SubElement
27
from errors import BzrError
30
class Revision(XMLMixin):
32
31
"""Single revision on a branch.
34
33
Revisions may know their revision_hash, but only once they've been
35
34
written out. This is not stored because you cannot write the hash
36
35
into the file it describes.
38
After bzr 0.0.5 revisions are allowed to have multiple parents.
41
List of parent revision_ids
44
Dictionary of revision properties. These are attached to the
45
revision as extra metadata. The name must be a single
46
word; the value can be an arbitrary string.
37
TODO: Perhaps make predecessor be a child element, not an attribute?
49
def __init__(self, revision_id, properties=None, **args):
50
self.revision_id = revision_id
51
self.properties = properties or {}
52
self._check_properties()
54
self.parent_sha1s = []
55
"""Not used anymore - legacy from for 4."""
39
def __init__(self, **args):
40
self.inventory_id = None
41
self.revision_id = None
56
47
self.__dict__.update(args)
58
50
def __repr__(self):
59
51
return "<Revision id %s>" % self.revision_id
61
def __eq__(self, other):
62
if not isinstance(other, Revision):
64
# FIXME: rbc 20050930 parent_ids are not being compared
66
self.inventory_sha1 == other.inventory_sha1
67
and self.revision_id == other.revision_id
68
and self.timestamp == other.timestamp
69
and self.message == other.message
70
and self.timezone == other.timezone
71
and self.committer == other.committer
72
and self.properties == other.properties)
74
def __ne__(self, other):
75
return not self.__eq__(other)
77
def _check_properties(self):
78
"""Verify that all revision properties are OK.
80
for name, value in self.properties.iteritems():
81
if not isinstance(name, basestring) or contains_whitespace(name):
82
raise ValueError("invalid property name %r" % name)
83
if not isinstance(value, basestring):
84
raise ValueError("invalid property value %r for %r" %
87
def get_history(self, repository):
88
"""Return the canonical line-of-history for this revision.
90
If ghosts are present this may differ in result from a ghost-free
93
current_revision = self
95
while current_revision is not None:
96
reversed_result.append(current_revision.revision_id)
97
if not len (current_revision.parent_ids):
98
reversed_result.append(None)
99
current_revision = None
101
next_revision_id = current_revision.parent_ids[0]
102
current_revision = repository.get_revision(next_revision_id)
103
reversed_result.reverse()
104
return reversed_result
106
def get_summary(self):
107
"""Get the first line of the log message for this revision.
109
return self.message.split('\n', 1)[0]
112
def is_ancestor(revision_id, candidate_id, branch):
113
"""Return true if candidate_id is an ancestor of revision_id.
115
A false negative will be returned if any intermediate descendent of
116
candidate_id is not present in any of the revision_sources.
118
revisions_source is an object supporting a get_revision operation that
119
behaves like Branch's.
121
return candidate_id in branch.repository.get_ancestry(revision_id)
124
def iter_ancestors(revision_id, revision_source, only_present=False):
125
ancestors = (revision_id,)
127
while len(ancestors) > 0:
129
for ancestor in ancestors:
131
yield ancestor, distance
133
revision = revision_source.get_revision(ancestor)
134
except errors.NoSuchRevision, e:
135
if e.revision == revision_id:
140
yield ancestor, distance
141
new_ancestors.extend(revision.parent_ids)
142
ancestors = new_ancestors
146
def find_present_ancestors(revision_id, revision_source):
147
"""Return the ancestors of a revision present in a branch.
149
It's possible that a branch won't have the complete ancestry of
150
one of its revisions.
154
anc_iter = enumerate(iter_ancestors(revision_id, revision_source,
156
for anc_order, (anc_id, anc_distance) in anc_iter:
157
if not found_ancestors.has_key(anc_id):
158
found_ancestors[anc_id] = (anc_order, anc_distance)
159
return found_ancestors
162
def __get_closest(intersection):
165
for entry in intersection:
166
if entry[0] == intersection[0][0]:
167
matches.append(entry[2])
171
def revision_graph(revision, revision_source):
172
"""Produce a graph of the ancestry of the specified revision.
174
:return: root, ancestors map, descendants map
176
revision_source.lock_read()
178
return _revision_graph(revision, revision_source)
180
revision_source.unlock()
183
def _revision_graph(revision, revision_source):
184
"""See revision_graph."""
185
from bzrlib.tsort import topo_sort
186
graph = revision_source.get_revision_graph(revision)
187
# mark all no-parent revisions as being NULL_REVISION parentage.
188
for node, parents in graph.items():
189
if len(parents) == 0:
190
graph[node] = [NULL_REVISION]
191
# add NULL_REVISION to the graph
192
graph[NULL_REVISION] = []
194
# pick a root. If there are multiple roots
195
# this could pick a random one.
196
topo_order = topo_sort(graph.items())
202
# map the descendants of the graph.
203
# and setup our set based return graph.
204
for node in graph.keys():
205
descendants[node] = {}
206
for node, parents in graph.items():
207
for parent in parents:
208
descendants[parent][node] = 1
209
ancestors[node] = set(parents)
211
assert root not in descendants[root]
212
assert root not in ancestors[root]
213
return root, ancestors, descendants
216
def combined_graph(revision_a, revision_b, revision_source):
217
"""Produce a combined ancestry graph.
218
Return graph root, ancestors map, descendants map, set of common nodes"""
219
root, ancestors, descendants = revision_graph(
220
revision_a, revision_source)
221
root_b, ancestors_b, descendants_b = revision_graph(
222
revision_b, revision_source)
224
raise errors.NoCommonRoot(revision_a, revision_b)
226
for node, node_anc in ancestors_b.iteritems():
227
if node in ancestors:
230
ancestors[node] = set()
231
ancestors[node].update(node_anc)
232
for node, node_dec in descendants_b.iteritems():
233
if node not in descendants:
234
descendants[node] = {}
235
descendants[node].update(node_dec)
236
return root, ancestors, descendants, common
239
def common_ancestor(revision_a, revision_b, revision_source,
241
if None in (revision_a, revision_b):
243
# trivial optimisation
244
if revision_a == revision_b:
248
pb.update('Picking ancestor', 1, 3)
249
graph = revision_source.get_revision_graph_with_ghosts(
250
[revision_a, revision_b])
251
# convert to a NULL_REVISION based graph.
252
ancestors = graph.get_ancestors()
253
descendants = graph.get_descendants()
254
common = set(graph.get_ancestry(revision_a)).intersection(
255
set(graph.get_ancestry(revision_b)))
256
descendants[NULL_REVISION] = {}
257
ancestors[NULL_REVISION] = []
258
for root in graph.roots:
259
descendants[NULL_REVISION][root] = 1
260
ancestors[root].append(NULL_REVISION)
261
for ghost in graph.ghosts:
262
# ghosts act as roots for the purpose of finding
263
# the longest paths from the root: any ghost *might*
264
# be directly attached to the root, so we treat them
266
# ghost now descends from NULL
267
descendants[NULL_REVISION][ghost] = 1
268
# that is it has an ancestor of NULL
269
ancestors[ghost] = [NULL_REVISION]
270
# ghost is common if any of ghosts descendants are common:
271
for ghost_descendant in descendants[ghost]:
272
if ghost_descendant in common:
276
common.add(NULL_REVISION)
277
except errors.NoCommonRoot:
278
raise errors.NoCommonAncestor(revision_a, revision_b)
280
pb.update('Picking ancestor', 2, 3)
281
distances = node_distances (descendants, ancestors, root)
282
pb.update('Picking ancestor', 3, 2)
283
farthest = select_farthest(distances, common)
284
if farthest is None or farthest == NULL_REVISION:
285
raise errors.NoCommonAncestor(revision_a, revision_b)
291
class MultipleRevisionSources(object):
292
"""Proxy that looks in multiple branches for revisions."""
293
def __init__(self, *args):
294
object.__init__(self)
295
assert len(args) != 0
296
self._revision_sources = args
298
def revision_parents(self, revision_id):
299
for source in self._revision_sources:
301
return source.revision_parents(revision_id)
302
except (errors.WeaveRevisionNotPresent, errors.NoSuchRevision), e:
306
def get_revision(self, revision_id):
307
for source in self._revision_sources:
309
return source.get_revision(revision_id)
310
except errors.NoSuchRevision, e:
314
def get_revision_graph(self, revision_id):
315
# we could probe incrementally until the pending
316
# ghosts list stop growing, but its cheaper for now
317
# to just ask for the complete graph for each repository.
319
for source in self._revision_sources:
320
ghost_graph = source.get_revision_graph_with_ghosts()
321
graphs.append(ghost_graph)
324
if not revision_id in graph.get_ancestors():
326
if absent == len(graphs):
327
raise errors.NoSuchRevision(self._revision_sources[0], revision_id)
331
pending = set([revision_id])
332
def find_parents(node_id):
333
"""find the parents for node_id."""
335
ancestors = graph.get_ancestors()
337
return ancestors[node_id]
340
raise errors.NoSuchRevision(self._revision_sources[0], node_id)
342
# all the graphs should have identical parent lists
343
node_id = pending.pop()
345
result[node_id] = find_parents(node_id)
346
for parent_node in result[node_id]:
347
if not parent_node in result:
348
pending.add(parent_node)
349
except errors.NoSuchRevision:
354
def get_revision_graph_with_ghosts(self, revision_ids):
355
# query all the sources for their entire graphs
356
# and then build a combined graph for just
359
for source in self._revision_sources:
360
ghost_graph = source.get_revision_graph_with_ghosts()
361
graphs.append(ghost_graph.get_ancestors())
362
for revision_id in revision_ids:
365
if not revision_id in graph:
367
if absent == len(graphs):
368
raise errors.NoSuchRevision(self._revision_sources[0],
373
pending = set(revision_ids)
375
def find_parents(node_id):
376
"""find the parents for node_id."""
379
return graph[node_id]
382
raise errors.NoSuchRevision(self._revision_sources[0], node_id)
384
# all the graphs should have identical parent lists
385
node_id = pending.pop()
387
parents = find_parents(node_id)
388
for parent_node in parents:
390
if (parent_node not in pending and
391
parent_node not in done):
393
pending.add(parent_node)
394
result.add_node(node_id, parents)
396
except errors.NoSuchRevision:
398
result.add_ghost(node_id)
403
for source in self._revision_sources:
407
for source in self._revision_sources:
411
@deprecated_function(zero_eight)
412
def get_intervening_revisions(ancestor_id, rev_id, rev_source,
413
revision_history=None):
414
"""Find the longest line of descent from maybe_ancestor to revision.
415
Revision history is followed where possible.
417
If ancestor_id == rev_id, list will be empty.
418
Otherwise, rev_id will be the last entry. ancestor_id will never appear.
419
If ancestor_id is not an ancestor, NotAncestor will be thrown
421
root, ancestors, descendants = revision_graph(rev_id, rev_source)
422
if len(descendants) == 0:
423
raise errors.NoSuchRevision(rev_source, rev_id)
424
if ancestor_id not in descendants:
425
rev_source.get_revision(ancestor_id)
426
raise errors.NotAncestor(rev_id, ancestor_id)
427
root_descendants = all_descendants(descendants, ancestor_id)
428
root_descendants.add(ancestor_id)
429
if rev_id not in root_descendants:
430
raise errors.NotAncestor(rev_id, ancestor_id)
431
distances = node_distances(descendants, ancestors, ancestor_id,
432
root_descendants=root_descendants)
434
def best_ancestor(rev_id):
436
for anc_id in ancestors[rev_id]:
438
distance = distances[anc_id]
441
if revision_history is not None and anc_id in revision_history:
443
elif best is None or distance > best[1]:
444
best = (anc_id, distance)
449
while next != ancestor_id:
451
next = best_ancestor(next)
55
root = Element('revision',
56
committer = self.committer,
57
timestamp = '%.9f' % self.timestamp,
58
revision_id = self.revision_id,
59
inventory_id = self.inventory_id,
60
timezone = str(self.timezone))
62
root.set('precursor', self.precursor)
65
msg = SubElement(root, 'message')
66
msg.text = self.message
72
def from_element(cls, elt):
73
# <changeset> is deprecated...
74
if elt.tag not in ('revision', 'changeset'):
75
raise BzrError("unexpected tag in revision file: %r" % elt)
77
cs = cls(committer = elt.get('committer'),
78
timestamp = float(elt.get('timestamp')),
79
precursor = elt.get('precursor'),
80
revision_id = elt.get('revision_id'),
81
inventory_id = elt.get('inventory_id'))
83
v = elt.get('timezone')
84
cs.timezone = v and int(v)
86
cs.message = elt.findtext('message') # text of <message>
89
from_element = classmethod(from_element)