~abentley/bzrtools/bzrtools.dev

125 by Aaron Bentley
Added pastegraph
1
#!/usr/bin/env python2.4
2
from subprocess import Popen, PIPE
3
from urllib import urlencode
4
from xml.sax.saxutils import escape
5
import os.path
139 by Aaron Bentley
Tweaked missing-dot handling
6
import errno
143 by Aaron Bentley
Used rsvga for nice antialiasing
7
import tempfile
8
import shutil
139 by Aaron Bentley
Tweaked missing-dot handling
9
140 by Aaron Bentley
Mapped some email addresses to names
10
mail_map = {}
11
139 by Aaron Bentley
Tweaked missing-dot handling
12
class NoDot(Exception):
13
    def __init__(self):
14
        Exception.__init__(self, "Can't find dot!")
125 by Aaron Bentley
Added pastegraph
15
143 by Aaron Bentley
Used rsvga for nice antialiasing
16
class NoRsvg(Exception):
17
    def __init__(self):
18
        Exception.__init__(self, "Can't find rsvg!")
19
125 by Aaron Bentley
Added pastegraph
20
class Node(object):
142 by Aaron Bentley
Clustered the branch revision history
21
    def __init__(self, name, color=None, label=None, rev_id=None,
22
                 cluster=None):
125 by Aaron Bentley
Added pastegraph
23
        self.name = name
24
        self.color = color
128 by Aaron Bentley
Got initial graphing functionality working
25
        self.label = label
130 by Aaron Bentley
Added committer to revisions
26
        self.committer = None
137 by Aaron Bentley
Put dotted outlines on missing revisions
27
        self.rev_id = rev_id
28
        self.node_style = []
142 by Aaron Bentley
Clustered the branch revision history
29
        self.cluster = cluster
135 by Aaron Bentley
Enhanced revision-crediting
30
140 by Aaron Bentley
Mapped some email addresses to names
31
    def get_committer(self):
135 by Aaron Bentley
Enhanced revision-crediting
32
        if self.committer is not None:
140 by Aaron Bentley
Mapped some email addresses to names
33
            if '@' in self.committer:
34
                try:
35
                    return mail_map[self.committer]
36
                except KeyError:
37
                    pass
38
            return self.committer
135 by Aaron Bentley
Enhanced revision-crediting
39
        elif self.rev_id is not None:
40
            try:
140 by Aaron Bentley
Mapped some email addresses to names
41
                first_segment = '-'.join(self.rev_id.split('-')[:-2])\
42
                    .strip(' ')
135 by Aaron Bentley
Enhanced revision-crediting
43
            except ValueError:
44
                first_segment = []
45
            if '@' in first_segment:
140 by Aaron Bentley
Mapped some email addresses to names
46
                try:
47
                    return mail_map[first_segment]
48
                except KeyError:
49
                    return first_segment
50
51
    def get_label(self):
52
        label = None
53
        committer = self.get_committer()
54
        if committer is not None:
55
            label = "%s\\n%s" % (self.name, committer)
135 by Aaron Bentley
Enhanced revision-crediting
56
        return label
125 by Aaron Bentley
Added pastegraph
57
58
    def define(self):
137 by Aaron Bentley
Put dotted outlines on missing revisions
59
        attributes = []
130 by Aaron Bentley
Added committer to revisions
60
        style = []
125 by Aaron Bentley
Added pastegraph
61
        if self.color is not None:
137 by Aaron Bentley
Put dotted outlines on missing revisions
62
            attributes.append('fillcolor="%s"' % self.color)
63
            style.append('filled')
64
        style.extend(self.node_style)
65
        if len(style) > 0:
66
            attributes.append('style="%s"' % ",".join(style))
135 by Aaron Bentley
Enhanced revision-crediting
67
        label = self.get_label()
68
        if label is not None:
137 by Aaron Bentley
Put dotted outlines on missing revisions
69
            attributes.append('label="%s"' % label)
141 by Aaron Bentley
Switched to use boxes
70
        attributes.append('shape="box"')
137 by Aaron Bentley
Put dotted outlines on missing revisions
71
        if len(attributes) > 0:
72
            return '%s[%s]' % (self.name, " ".join(attributes))
125 by Aaron Bentley
Added pastegraph
73
74
    def __str__(self):
75
        return self.name
76
128 by Aaron Bentley
Got initial graphing functionality working
77
def dot_output(relations):
125 by Aaron Bentley
Added pastegraph
78
    defined = set()
79
    yield "digraph G\n"
80
    yield "{\n"
142 by Aaron Bentley
Clustered the branch revision history
81
    clusters = set()
82
    def rel_appropriate(start, end, cluster):
83
        if cluster is None:
84
            return start.cluster is None or end.cluster is None
85
        else:
86
            return start.cluster==cluster and end.cluster==cluster
87
125 by Aaron Bentley
Added pastegraph
88
    for (start, end) in relations:
142 by Aaron Bentley
Clustered the branch revision history
89
        if start.cluster is not None:
90
            clusters.add(start.cluster)
91
        if end.cluster is not None:
92
            clusters.add(end.cluster)
93
    clusters = list(clusters)
94
    clusters.append(None)
95
    for index, cluster in enumerate(clusters):
96
        if cluster is not None:
97
            yield "subgraph cluster_%s\n" % index
98
            yield "{\n"
99
            yield '    label="%s"\n' % cluster
100
        for (start, end) in relations:
101
            if start.name not in defined and start.cluster == cluster:
102
                defined.add(start.name)
103
                my_def = start.define()
104
                if my_def is not None:
105
                    yield "    %s;\n" % my_def
106
            if end.name not in defined and end.cluster == cluster:
107
                defined.add(end.name)
108
                my_def = end.define()
109
                if my_def is not None:
110
                    yield "    %s;\n" % my_def
111
            if rel_appropriate(start, end, cluster):
112
                yield "    %s->%s;\n" % (start.name, end.name)
113
        if cluster is not None:
114
            yield "}\n"
125 by Aaron Bentley
Added pastegraph
115
    yield "}\n"
116
143 by Aaron Bentley
Used rsvga for nice antialiasing
117
def invoke_dot_aa(input, out_file, file_type='png'):
118
    """\
119
    Produce antialiased Dot output, invoking rsvg on an intermediate file.
120
    rsvg only supports png, jpeg and .ico files."""
121
    tempdir = tempfile.mkdtemp()
122
    try:
123
        temp_file = os.path.join(tempdir, 'temp.svg')
124
        invoke_dot(input, temp_file, 'svg')
125
        cmdline = ['rsvg', temp_file, out_file]
126
        try:
127
            rsvg_proc = Popen(cmdline)
128
        except OSError, e:
129
            if e.errno == errno.ENOENT:
130
                raise NoRsvg()
131
        status = rsvg_proc.wait()
132
    finally:
133
        shutil.rmtree(tempdir)
134
    return status
135
142 by Aaron Bentley
Clustered the branch revision history
136
def invoke_dot(input, out_file=None, file_type='svg', antialias=None):
134 by Aaron Bentley
support multiple image formats for graph-ancestry
137
    cmdline = ['dot', '-T%s' % file_type]
125 by Aaron Bentley
Added pastegraph
138
    if out_file is not None:
139
        cmdline.extend(('-o', out_file))
138 by Aaron Bentley
Handle systems without dot (the horror!). From Magnus Therning.
140
    try:
141
        dot_proc = Popen(cmdline, stdin=PIPE)
142
    except OSError, e:
139 by Aaron Bentley
Tweaked missing-dot handling
143
        if e.errno == errno.ENOENT:
144
            raise NoDot()
145
        else:
146
            raise
125 by Aaron Bentley
Added pastegraph
147
    for line in input:
148
        dot_proc.stdin.write(line)
149
    dot_proc.stdin.close()
150
    return dot_proc.wait()