~abentley/bzrtools/bzrtools.dev

257.1.2 by Aaron Bentley
Updated GPL notices
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
125 by Aaron Bentley
Added pastegraph
18
from subprocess import Popen, PIPE
19
from urllib import urlencode
20
from xml.sax.saxutils import escape
21
import os.path
139 by Aaron Bentley
Tweaked missing-dot handling
22
import errno
143 by Aaron Bentley
Used rsvga for nice antialiasing
23
import tempfile
24
import shutil
189.1.1 by John Arbash Meinel
Adding an html target.
25
import time
139 by Aaron Bentley
Tweaked missing-dot handling
26
167 by Aaron Bentley
Moved extention lists to dotgraph
27
RSVG_OUTPUT_TYPES = ('png', 'jpg')
189 by Aaron Bentley
Enabled client-side imagemaps
28
DOT_OUTPUT_TYPES = ('svg', 'svgz', 'gif', 'jpg', 'ps', 'fig', 'mif', 'png', 
29
                    'cmapx')
140 by Aaron Bentley
Mapped some email addresses to names
30
139 by Aaron Bentley
Tweaked missing-dot handling
31
class NoDot(Exception):
32
    def __init__(self):
33
        Exception.__init__(self, "Can't find dot!")
125 by Aaron Bentley
Added pastegraph
34
143 by Aaron Bentley
Used rsvga for nice antialiasing
35
class NoRsvg(Exception):
36
    def __init__(self):
37
        Exception.__init__(self, "Can't find rsvg!")
38
125 by Aaron Bentley
Added pastegraph
39
class Node(object):
142 by Aaron Bentley
Clustered the branch revision history
40
    def __init__(self, name, color=None, label=None, rev_id=None,
189.1.1 by John Arbash Meinel
Adding an html target.
41
                 cluster=None, node_style=None, date=None, message=None):
125 by Aaron Bentley
Added pastegraph
42
        self.name = name
43
        self.color = color
128 by Aaron Bentley
Got initial graphing functionality working
44
        self.label = label
130 by Aaron Bentley
Added committer to revisions
45
        self.committer = None
137 by Aaron Bentley
Put dotted outlines on missing revisions
46
        self.rev_id = rev_id
172 by Aaron Bentley
Marked missing nodes
47
        if node_style is None:
48
            self.node_style = []
142 by Aaron Bentley
Clustered the branch revision history
49
        self.cluster = cluster
178 by Aaron Bentley
Switched from clusters to forced ranking
50
        self.rank = None
189.1.1 by John Arbash Meinel
Adding an html target.
51
        self.date = date
52
        self.message = message
189 by Aaron Bentley
Enabled client-side imagemaps
53
        self.href = None
135 by Aaron Bentley
Enhanced revision-crediting
54
125 by Aaron Bentley
Added pastegraph
55
    def define(self):
137 by Aaron Bentley
Put dotted outlines on missing revisions
56
        attributes = []
130 by Aaron Bentley
Added committer to revisions
57
        style = []
125 by Aaron Bentley
Added pastegraph
58
        if self.color is not None:
137 by Aaron Bentley
Put dotted outlines on missing revisions
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))
157 by Aaron Bentley
Got graph showing merge selection decently
64
        label = self.label
135 by Aaron Bentley
Enhanced revision-crediting
65
        if label is not None:
137 by Aaron Bentley
Put dotted outlines on missing revisions
66
            attributes.append('label="%s"' % label)
141 by Aaron Bentley
Switched to use boxes
67
        attributes.append('shape="box"')
189.1.1 by John Arbash Meinel
Adding an html target.
68
        tooltip = ''
69
        if self.message is not None:
70
            tooltip += self.message.replace('"', '\\"')
71
        if tooltip:
72
            attributes.append('tooltip="%s"' % tooltip)
189 by Aaron Bentley
Enabled client-side imagemaps
73
        if self.href is not None:
74
            attributes.append('href="%s"' % self.href)
189.1.1 by John Arbash Meinel
Adding an html target.
75
        elif tooltip:
76
            attributes.append('href="#"')
137 by Aaron Bentley
Put dotted outlines on missing revisions
77
        if len(attributes) > 0:
78
            return '%s[%s]' % (self.name, " ".join(attributes))
125 by Aaron Bentley
Added pastegraph
79
80
    def __str__(self):
81
        return self.name
82
175 by Aaron Bentley
Added edges
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
178 by Aaron Bentley
Switched from clusters to forced ranking
90
    def dot(self, do_weight=False):
175 by Aaron Bentley
Added edges
91
        attributes = []
92
        if self.label is not None:
93
            attributes.append(('label', self.label))
178 by Aaron Bentley
Switched from clusters to forced ranking
94
        if do_weight:
182 by Aaron Bentley
Set edge weight to 1 for missing revisions
95
            weight = '0'
178 by Aaron Bentley
Switched from clusters to forced ranking
96
            if self.start.cluster == self.end.cluster:
182 by Aaron Bentley
Set edge weight to 1 for missing revisions
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))
175 by Aaron Bentley
Added edges
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):
176 by Aaron Bentley
Added skip labels to edges
114
    if hasattr(relation, 'start') and hasattr(relation, 'end'):
115
        return relation
175 by Aaron Bentley
Added edges
116
    return Edge(relation[0], relation[1])
117
178 by Aaron Bentley
Switched from clusters to forced ranking
118
def dot_output(relations, ranking="forced"):
119
    defined = {}
125 by Aaron Bentley
Added pastegraph
120
    yield "digraph G\n"
121
    yield "{\n"
142 by Aaron Bentley
Clustered the branch revision history
122
    clusters = set()
175 by Aaron Bentley
Added edges
123
    edges = [make_edge(f) for f in relations]
142 by Aaron Bentley
Clustered the branch revision history
124
    def rel_appropriate(start, end, cluster):
125
        if cluster is None:
169 by Aaron Bentley
Got ancestry-graph showing common and revision-history nodes properly
126
            return (start.cluster is None and end.cluster is None) or \
127
                start.cluster != end.cluster
142 by Aaron Bentley
Clustered the branch revision history
128
        else:
129
            return start.cluster==cluster and end.cluster==cluster
130
175 by Aaron Bentley
Added edges
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)
142 by Aaron Bentley
Clustered the branch revision history
136
    clusters = list(clusters)
137
    clusters.append(None)
138
    for index, cluster in enumerate(clusters):
178 by Aaron Bentley
Switched from clusters to forced ranking
139
        if cluster is not None and ranking == "cluster":
142 by Aaron Bentley
Clustered the branch revision history
140
            yield "subgraph cluster_%s\n" % index
141
            yield "{\n"
142
            yield '    label="%s"\n' % cluster
175 by Aaron Bentley
Added edges
143
        for edge in edges:
144
            if edge.start.name not in defined and edge.start.cluster == cluster:
178 by Aaron Bentley
Switched from clusters to forced ranking
145
                defined[edge.start.name] = edge.start
175 by Aaron Bentley
Added edges
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:
178 by Aaron Bentley
Switched from clusters to forced ranking
150
                defined[edge.end.name] = edge.end
175 by Aaron Bentley
Added edges
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):
178 by Aaron Bentley
Switched from clusters to forced ranking
155
                yield "    %s\n" % edge.dot(do_weight=ranking=="forced")
156
        if cluster is not None and ranking == "cluster":
142 by Aaron Bentley
Clustered the branch revision history
157
            yield "}\n"
178 by Aaron Bentley
Switched from clusters to forced ranking
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)
125 by Aaron Bentley
Added pastegraph
180
    yield "}\n"
181
143 by Aaron Bentley
Used rsvga for nice antialiasing
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
179 by Aaron Bentley
Enforced a font and size
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]
125 by Aaron Bentley
Added pastegraph
206
    if out_file is not None:
207
        cmdline.extend(('-o', out_file))
138 by Aaron Bentley
Handle systems without dot (the horror!). From Magnus Therning.
208
    try:
209
        dot_proc = Popen(cmdline, stdin=PIPE)
210
    except OSError, e:
139 by Aaron Bentley
Tweaked missing-dot handling
211
        if e.errno == errno.ENOENT:
212
            raise NoDot()
213
        else:
214
            raise
125 by Aaron Bentley
Added pastegraph
215
    for line in input:
312 by Aaron Bentley
forced ancestry graph to use utf-8 Dot output
216
        dot_proc.stdin.write(line.encode('utf-8'))
125 by Aaron Bentley
Added pastegraph
217
    dot_proc.stdin.close()
218
    return dot_proc.wait()
189.1.1 by John Arbash Meinel
Adding an html target.
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')
189.1.2 by John Arbash Meinel
Fix datestamp, reuse pre-layed out dot file.
228
        status = invoke_dot(input, temp_dot, file_type='dot')
229
189.1.1 by John Arbash Meinel
Adding an html target.
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