~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to dotgraph.py

  • Committer: Aaron Bentley
  • Date: 2008-01-11 03:01:54 UTC
  • Revision ID: aaron.bentley@utoronto.ca-20080111030154-apm50v0b0tu93prh
Support branch6 formats in rspush

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2004, 2005 Aaron Bentley
 
2
# <aaron.bentley@utoronto.ca>
 
3
#
 
4
#    This program is free software; you can redistribute it and/or modify
 
5
#    it under the terms of the GNU General Public License as published by
 
6
#    the Free Software Foundation; either version 2 of the License, or
 
7
#    (at your option) any later version.
 
8
#
 
9
#    This program is distributed in the hope that it will be useful,
 
10
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
 
11
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
12
#    GNU General Public License for more details.
 
13
#
 
14
#    You should have received a copy of the GNU General Public License
 
15
#    along with this program; if not, write to the Free Software
 
16
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
17
 
 
18
from subprocess import Popen, PIPE
 
19
from urllib import urlencode
 
20
from xml.sax.saxutils import escape
 
21
import os.path
 
22
import errno
 
23
import tempfile
 
24
import shutil
 
25
import time
 
26
 
 
27
RSVG_OUTPUT_TYPES = ('png', 'jpg')
 
28
DOT_OUTPUT_TYPES = ('svg', 'svgz', 'gif', 'jpg', 'ps', 'fig', 'mif', 'png',
 
29
                    'cmapx')
 
30
 
 
31
class NoDot(Exception):
 
32
    def __init__(self):
 
33
        Exception.__init__(self, "Can't find dot!")
 
34
 
 
35
class NoRsvg(Exception):
 
36
    def __init__(self):
 
37
        Exception.__init__(self, "Can't find rsvg!")
 
38
 
 
39
class Node(object):
 
40
    def __init__(self, name, color=None, label=None, rev_id=None,
 
41
                 cluster=None, node_style=None, date=None, message=None):
 
42
        self.name = name
 
43
        self.color = color
 
44
        self.label = label
 
45
        self.committer = None
 
46
        self.rev_id = rev_id
 
47
        if node_style is None:
 
48
            self.node_style = []
 
49
        self.cluster = cluster
 
50
        self.rank = None
 
51
        self.date = date
 
52
        self.message = message
 
53
        self.href = None
 
54
 
 
55
    def define(self):
 
56
        attributes = []
 
57
        style = []
 
58
        if self.color is not None:
 
59
            attributes.append('fillcolor="%s"' % self.color)
 
60
            style.append('filled')
 
61
        style.extend(self.node_style)
 
62
        if len(style) > 0:
 
63
            attributes.append('style="%s"' % ",".join(style))
 
64
        label = self.label
 
65
        if label is not None:
 
66
            attributes.append('label="%s"' % label)
 
67
        attributes.append('shape="box"')
 
68
        tooltip = ''
 
69
        if self.message is not None:
 
70
            tooltip += self.message.replace('"', '\\"')
 
71
        if tooltip:
 
72
            attributes.append('tooltip="%s"' % tooltip)
 
73
        if self.href is not None:
 
74
            attributes.append('href="%s"' % self.href)
 
75
        elif tooltip:
 
76
            attributes.append('href="#"')
 
77
        if len(attributes) > 0:
 
78
            return '%s[%s]' % (self.name, " ".join(attributes))
 
79
 
 
80
    def __str__(self):
 
81
        return self.name
 
82
 
 
83
class Edge(object):
 
84
    def __init__(self, start, end, label=None):
 
85
        object.__init__(self)
 
86
        self.start = start
 
87
        self.end = end
 
88
        self.label = label
 
89
 
 
90
    def dot(self, do_weight=False):
 
91
        attributes = []
 
92
        if self.label is not None:
 
93
            attributes.append(('label', self.label))
 
94
        if do_weight:
 
95
            weight = '0'
 
96
            if self.start.cluster == self.end.cluster:
 
97
                weight = '1'
 
98
            elif self.start.rank is None:
 
99
                weight = '1'
 
100
            elif self.end.rank is None:
 
101
                weight = '1'
 
102
            attributes.append(('weight', weight))
 
103
        if len(attributes) > 0:
 
104
            atlist = []
 
105
            for key, value in attributes:
 
106
                atlist.append("%s=\"%s\"" % (key, value))
 
107
            pq = ' '.join(atlist)
 
108
            op = "[%s]" % pq
 
109
        else:
 
110
            op = ""
 
111
        return "%s->%s%s;" % (self.start.name, self.end.name, op)
 
112
 
 
113
def make_edge(relation):
 
114
    if hasattr(relation, 'start') and hasattr(relation, 'end'):
 
115
        return relation
 
116
    return Edge(relation[0], relation[1])
 
117
 
 
118
def dot_output(relations, ranking="forced"):
 
119
    defined = {}
 
120
    yield "digraph G\n"
 
121
    yield "{\n"
 
122
    clusters = set()
 
123
    edges = [make_edge(f) for f in relations]
 
124
    def rel_appropriate(start, end, cluster):
 
125
        if cluster is None:
 
126
            return (start.cluster is None and end.cluster is None) or \
 
127
                start.cluster != end.cluster
 
128
        else:
 
129
            return start.cluster==cluster and end.cluster==cluster
 
130
 
 
131
    for edge in edges:
 
132
        if edge.start.cluster is not None:
 
133
            clusters.add(edge.start.cluster)
 
134
        if edge.end.cluster is not None:
 
135
            clusters.add(edge.end.cluster)
 
136
    clusters = list(clusters)
 
137
    clusters.append(None)
 
138
    for index, cluster in enumerate(clusters):
 
139
        if cluster is not None and ranking == "cluster":
 
140
            yield "subgraph cluster_%s\n" % index
 
141
            yield "{\n"
 
142
            yield '    label="%s"\n' % cluster
 
143
        for edge in edges:
 
144
            if edge.start.name not in defined and edge.start.cluster == cluster:
 
145
                defined[edge.start.name] = edge.start
 
146
                my_def = edge.start.define()
 
147
                if my_def is not None:
 
148
                    yield "    %s\n" % my_def
 
149
            if edge.end.name not in defined and edge.end.cluster == cluster:
 
150
                defined[edge.end.name] = edge.end
 
151
                my_def = edge.end.define()
 
152
                if my_def is not None:
 
153
                    yield "    %s;\n" % my_def
 
154
            if rel_appropriate(edge.start, edge.end, cluster):
 
155
                yield "    %s\n" % edge.dot(do_weight=ranking=="forced")
 
156
        if cluster is not None and ranking == "cluster":
 
157
            yield "}\n"
 
158
 
 
159
    if ranking == "forced":
 
160
        ranks = {}
 
161
        for node in defined.itervalues():
 
162
            if node.rank not in ranks:
 
163
                ranks[node.rank] = set()
 
164
            ranks[node.rank].add(node.name)
 
165
        sorted_ranks = [n for n in ranks.iteritems()]
 
166
        sorted_ranks.sort()
 
167
        last_rank = None
 
168
        for rank, nodes in sorted_ranks:
 
169
            if rank is None:
 
170
                continue
 
171
            yield 'rank%d[style="invis"];\n' % rank
 
172
            if last_rank is not None:
 
173
                yield 'rank%d -> rank%d[style="invis"];\n' % (last_rank, rank)
 
174
            last_rank = rank
 
175
        for rank, nodes in ranks.iteritems():
 
176
            if rank is None:
 
177
                continue
 
178
            node_text = "; ".join('"%s"' % n for n in nodes)
 
179
            yield ' {rank = same; "rank%d"; %s}\n' % (rank, node_text)
 
180
    yield "}\n"
 
181
 
 
182
def invoke_dot_aa(input, out_file, file_type='png'):
 
183
    """\
 
184
    Produce antialiased Dot output, invoking rsvg on an intermediate file.
 
185
    rsvg only supports png, jpeg and .ico files."""
 
186
    tempdir = tempfile.mkdtemp()
 
187
    try:
 
188
        temp_file = os.path.join(tempdir, 'temp.svg')
 
189
        invoke_dot(input, temp_file, 'svg')
 
190
        cmdline = ['rsvg', temp_file, out_file]
 
191
        try:
 
192
            rsvg_proc = Popen(cmdline)
 
193
        except OSError, e:
 
194
            if e.errno == errno.ENOENT:
 
195
                raise NoRsvg()
 
196
        status = rsvg_proc.wait()
 
197
    finally:
 
198
        shutil.rmtree(tempdir)
 
199
    return status
 
200
 
 
201
def invoke_dot(input, out_file=None, file_type='svg', antialias=None,
 
202
               fontname="Helvetica", fontsize=11):
 
203
    cmdline = ['dot', '-T%s' % file_type, '-Nfontname=%s' % fontname,
 
204
               '-Efontname=%s' % fontname, '-Nfontsize=%d' % fontsize,
 
205
               '-Efontsize=%d' % fontsize]
 
206
    if out_file is not None:
 
207
        cmdline.extend(('-o', out_file))
 
208
    try:
 
209
        dot_proc = Popen(cmdline, stdin=PIPE)
 
210
    except OSError, e:
 
211
        if e.errno == errno.ENOENT:
 
212
            raise NoDot()
 
213
        else:
 
214
            raise
 
215
    for line in input:
 
216
        dot_proc.stdin.write(line.encode('utf-8'))
 
217
    dot_proc.stdin.close()
 
218
    return dot_proc.wait()
 
219
 
 
220
def invoke_dot_html(input, out_file):
 
221
    """\
 
222
    Produce an html file, which uses a .png file, and a cmap to provide
 
223
    annotated revisions.
 
224
    """
 
225
    tempdir = tempfile.mkdtemp()
 
226
    try:
 
227
        temp_dot = os.path.join(tempdir, 'temp.dot')
 
228
        status = invoke_dot(input, temp_dot, file_type='dot')
 
229
 
 
230
        dot = open(temp_dot)
 
231
        temp_file = os.path.join(tempdir, 'temp.cmapx')
 
232
        status = invoke_dot(dot, temp_file, 'cmapx')
 
233
 
 
234
        png_file = '.'.join(out_file.split('.')[:-1] + ['png'])
 
235
        dot.seek(0)
 
236
        status = invoke_dot(dot, png_file, 'png')
 
237
 
 
238
        png_relative = png_file.split('/')[-1]
 
239
        html = open(out_file, 'wb')
 
240
        w = html.write
 
241
        w('<html><head><title></title></head>\n')
 
242
        w('<body>\n')
 
243
        w('<img src="%s" usemap="#G" border=0/>' % png_relative)
 
244
        w(open(temp_file).read())
 
245
        w('</body></html>\n')
 
246
    finally:
 
247
        shutil.rmtree(tempdir)
 
248
    return status
 
249