~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to graph.py

  • Committer: Aaron Bentley
  • Date: 2011-09-25 02:01:39 UTC
  • mfrom: (773.1.3 fix-nick-reference)
  • Revision ID: aaron@aaronbentley.com-20110925020139-hkr2kb7jp1vnbs77
Fix bug #263065.  (andialbrecht)

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005 Aaron Bentley
2
 
# <aaron.bentley@utoronto.ca>
 
1
# Copyright (C) 2005, 2008 Aaron Bentley
 
2
# <aaron@aaronbentley.com>
3
3
#
4
4
#    This program is free software; you can redistribute it and/or modify
5
5
#    it under the terms of the GNU General Public License as published by
14
14
#    You should have received a copy of the GNU General Public License
15
15
#    along with this program; if not, write to the Free Software
16
16
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
17
 
 
18
 
 
19
import time
 
20
 
 
21
from bzrlib.branch import Branch
 
22
from bzrlib.errors import BzrCommandError, NoSuchRevision
 
23
from bzrlib.revision import NULL_REVISION
 
24
 
17
25
from bzrtools import short_committer
18
 
from dotgraph import Node, dot_output, invoke_dot, invoke_dot_aa, NoDot, NoRsvg
19
 
from dotgraph import RSVG_OUTPUT_TYPES, DOT_OUTPUT_TYPES, Edge, invoke_dot_html
20
 
from bzrlib.branch import Branch
21
 
from bzrlib.errors import BzrCommandError, NoCommonRoot, NoSuchRevision
22
 
from bzrlib.graph import node_distances, select_farthest
23
 
from bzrlib.revision import combined_graph, revision_graph
24
 
from bzrlib.revision import MultipleRevisionSources
25
 
import bzrlib.errors
26
 
import re
27
 
import os.path
28
 
import time
 
26
from dotgraph import (
 
27
    dot_output,
 
28
    DOT_OUTPUT_TYPES,
 
29
    Edge,
 
30
    invoke_dot,
 
31
    invoke_dot_aa,
 
32
    invoke_dot_html,
 
33
    Node,
 
34
    NoDot,
 
35
    NoRsvg,
 
36
    RSVG_OUTPUT_TYPES,
 
37
    )
 
38
 
 
39
 
 
40
def max_distance(node, ancestors, distances, root_descendants):
 
41
    """Calculate the max distance to an ancestor.
 
42
    Return None if not all possible ancestors have known distances"""
 
43
    best = None
 
44
    if node in distances:
 
45
        best = distances[node]
 
46
    for ancestor in ancestors[node]:
 
47
        # skip ancestors we will never traverse:
 
48
        if root_descendants is not None and ancestor not in root_descendants:
 
49
            continue
 
50
        # An ancestor which is not listed in ancestors will never be in
 
51
        # distances, so we pretend it never existed.
 
52
        if ancestor not in ancestors:
 
53
            continue
 
54
        if ancestor not in distances:
 
55
            return None
 
56
        if best is None or distances[ancestor]+1 > best:
 
57
            best = distances[ancestor] + 1
 
58
    return best
 
59
 
 
60
 
 
61
def node_distances(graph, ancestors, start, root_descendants=None):
 
62
    """Produce a list of nodes, sorted by distance from a start node.
 
63
    This is an algorithm devised by Aaron Bentley, because applying Dijkstra
 
64
    backwards seemed too complicated.
 
65
 
 
66
    For each node, we walk its descendants.  If all the descendant's ancestors
 
67
    have a max-distance-to-start, (excluding ones that can never reach start),
 
68
    we calculate their max-distance-to-start, and schedule their descendants.
 
69
 
 
70
    So when a node's last parent acquires a distance, it will acquire a
 
71
    distance on the next iteration.
 
72
 
 
73
    Once we know the max distances for all nodes, we can return a list sorted
 
74
    by distance, farthest first.
 
75
    """
 
76
    distances = {start: 0}
 
77
    lines = set([start])
 
78
    while len(lines) > 0:
 
79
        new_lines = set()
 
80
        for line in lines:
 
81
            line_descendants = graph[line]
 
82
            for descendant in line_descendants:
 
83
                distance = max_distance(descendant, ancestors, distances,
 
84
                                        root_descendants)
 
85
                if distance is None:
 
86
                    continue
 
87
                distances[descendant] = distance
 
88
                new_lines.add(descendant)
 
89
        lines = new_lines
 
90
    return distances
 
91
 
 
92
 
 
93
def nodes_by_distance(distances):
 
94
    """Return a list of nodes sorted by distance"""
 
95
    def by_distance(n):
 
96
        return distances[n],n
 
97
 
 
98
    node_list = distances.keys()
 
99
    node_list.sort(key=by_distance, reverse=True)
 
100
    return node_list
 
101
 
 
102
 
 
103
def select_farthest(distances, common):
 
104
    """Return the farthest common node, or None if no node qualifies."""
 
105
    node_list = nodes_by_distance(distances)
 
106
    for node in node_list:
 
107
        if node in common:
 
108
            return node
 
109
 
29
110
 
30
111
mail_map = {'aaron.bentley@utoronto.ca'     : 'Aaron Bentley',
31
112
            'abentley@panoramicfeedback.com': 'Aaron Bentley',
72
153
    return new_ancestors
73
154
 
74
155
def get_rev_info(rev_id, source):
75
 
    """Return the committer, message, and date of a revision."""
 
156
    """Return the committer, message, nick and date of a revision."""
76
157
    committer = None
77
158
    message = None
78
159
    date = None
 
160
    nick = None
79
161
    if rev_id == 'null:':
80
162
        return None, 'Null Revision', None, None
81
163
    try:
106
188
    return committer, message, nick, date
107
189
 
108
190
class Grapher(object):
 
191
 
109
192
    def __init__(self, branch, other_branch=None):
110
193
        object.__init__(self)
111
194
        self.branch = branch
112
195
        self.other_branch = other_branch
 
196
        if other_branch is not None:
 
197
            other_repo = other_branch.repository
 
198
            revision_b = self.other_branch.last_revision()
 
199
        else:
 
200
            other_repo = None
 
201
            revision_b = None
 
202
        self.graph = self.branch.repository.get_graph(other_repo)
113
203
        revision_a = self.branch.last_revision()
114
 
        if other_branch is not None:
115
 
            branch.fetch(other_branch)
116
 
            revision_b = self.other_branch.last_revision()
117
 
            try:
118
 
                self.root, self.ancestors, self.descendants, self.common = \
119
 
                    combined_graph(revision_a, revision_b,
120
 
                                   self.branch.repository)
121
 
            except bzrlib.errors.NoCommonRoot:
122
 
                raise bzrlib.errors.NoCommonAncestor(revision_a, revision_b)
123
 
        else:
124
 
            self.root, self.ancestors, self.descendants = \
125
 
                revision_graph(revision_a, branch.repository)
126
 
            self.common = []
127
 
 
 
204
        self.scan_graph(revision_a, revision_b)
128
205
        self.n_history = branch.revision_history()
 
206
        self.n_revnos = branch.get_revision_id_to_revno_map()
129
207
        self.distances = node_distances(self.descendants, self.ancestors,
130
208
                                        self.root)
131
209
        if other_branch is not None:
132
210
            self.base = select_farthest(self.distances, self.common)
133
211
            self.m_history = other_branch.revision_history()
134
 
            new_graph = getattr(branch.repository, 'get_graph', lambda: None)()
135
 
            if new_graph is None:
136
 
                self.new_base = None
137
 
                self.lcas = set()
138
 
            else:
139
 
                self.new_base = new_graph.find_unique_lca(revision_a,
140
 
                                                          revision_b)
141
 
                self.lcas = new_graph.find_lca(revision_a, revision_b)
 
212
            self.m_revnos = other_branch.get_revision_id_to_revno_map()
 
213
            self.new_base = self.graph.find_unique_lca(revision_a,
 
214
                                                       revision_b)
 
215
            self.lcas = self.graph.find_lca(revision_a, revision_b)
142
216
        else:
143
217
            self.base = None
144
218
            self.new_base = None
145
219
            self.lcas = set()
146
220
            self.m_history = []
 
221
            self.m_revnos = {}
 
222
 
 
223
    def scan_graph(self, revision_a, revision_b):
 
224
        a_ancestors = dict(self.graph.iter_ancestry([revision_a]))
 
225
        self.ancestors = a_ancestors
 
226
        self.root = NULL_REVISION
 
227
        if revision_b is not None:
 
228
            b_ancestors = dict(self.graph.iter_ancestry([revision_b]))
 
229
            self.common = set(a_ancestors.keys())
 
230
            self.common.intersection_update(b_ancestors)
 
231
            self.ancestors.update(b_ancestors)
 
232
        else:
 
233
            self.common = []
 
234
            revision_b = None
 
235
        self.descendants = {}
 
236
        ghosts = set()
 
237
        for revision, parents in self.ancestors.iteritems():
 
238
            self.descendants.setdefault(revision, [])
 
239
            if parents is None:
 
240
                ghosts.add(revision)
 
241
                parents = [NULL_REVISION]
 
242
            for parent in parents:
 
243
                self.descendants.setdefault(parent, []).append(revision)
 
244
        for ghost in ghosts:
 
245
            self.ancestors[ghost] = [NULL_REVISION]
 
246
 
 
247
    @staticmethod
 
248
    def _get_revno_str(prefix, revno_map, revision_id):
 
249
        try:
 
250
            revno = revno_map[revision_id]
 
251
        except KeyError:
 
252
            return None
 
253
        return '%s%s' % (prefix, '.'.join(str(n) for n in revno))
147
254
 
148
255
    def dot_node(self, node, num):
149
256
        try:
155
262
        except ValueError:
156
263
            m_rev = None
157
264
        if (n_rev, m_rev) == (None, None):
158
 
            name = node[-5:]
 
265
            name = self._get_revno_str('r', self.n_revnos, node)
 
266
            if name is None:
 
267
                name = self._get_revno_str('R', self.m_revnos, node)
 
268
            if name is None:
 
269
                name = node[-5:]
159
270
            cluster = None
160
271
        elif n_rev == m_rev:
161
272
            name = "rR%d" % n_rev
294
405
            done = True
295
406
        except NoDot, e:
296
407
            raise BzrCommandError("Can't find 'dot'.  Please ensure Graphviz"\
297
 
                " is installed correctly, or use --noantialias")
 
408
                " is installed correctly.")
298
409
    elif ext == 'dot' and not done:
299
410
        my_file = file(filename, 'wb')
300
411
        for fragment in output:
304
415
            invoke_dot_html(output, filename)
305
416
        except NoDot, e:
306
417
            raise BzrCommandError("Can't find 'dot'.  Please ensure Graphviz"\
307
 
                " is installed correctly, or use --noantialias")
 
418
                " is installed correctly.")
308
419
    elif not done:
309
420
        print "Unknown file extension: %s" % ext
310