~abentley/bzrtools/bzrtools.dev

646 by Aaron Bentley
More import fixes
1
# Copyright (C) 2005, 2008 Aaron Bentley
612 by Aaron Bentley
Update email address
2
# <aaron@aaronbentley.com>
257.1.2 by Aaron Bentley
Updated GPL notices
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
646 by Aaron Bentley
More import fixes
17
18
19
import time
20
647 by Aaron Bentley
More import tweaks
21
from bzrlib.branch import Branch
22
from bzrlib.errors import BzrCommandError, NoSuchRevision
23
from bzrlib.revision import NULL_REVISION
24
292 by Aaron Bentley
Introduced branch-history command
25
from bzrtools import short_committer
646 by Aaron Bentley
More import fixes
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
128 by Aaron Bentley
Got initial graphing functionality working
39
756.1.1 by Jelmer Vernooij
Merge in bzrlib.deprecated_graph functions still used by bzrtools.
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
110
183 by Aaron Bentley
Code cleanup
111
mail_map = {'aaron.bentley@utoronto.ca'     : 'Aaron Bentley',
112
            'abentley@panoramicfeedback.com': 'Aaron Bentley',
113
            'abentley@lappy'                : 'Aaron Bentley',
114
            'john@arbash-meinel.com'        : 'John Arbash Meinel',
115
            'mbp@sourcefrog.net'            : 'Martin Pool',
116
            'robertc@robertcollins.net'     : 'Robert Collins',
117
            }
140 by Aaron Bentley
Mapped some email addresses to names
118
166 by Aaron Bentley
Fixed username-in-ancestry-graph issue
119
committer_alias = {'abentley': 'Aaron Bentley'}
145 by aaron.bentley at utoronto
Reduced graph-collapsing aggressivness
120
def can_skip(rev_id, descendants, ancestors):
121
    if rev_id not in descendants:
122
        return False
174 by Aaron Bentley
Added ancestry collapsing to graphs
123
    elif rev_id not in ancestors:
124
        return False
145 by aaron.bentley at utoronto
Reduced graph-collapsing aggressivness
125
    elif len(ancestors[rev_id]) != 1:
126
        return False
174 by Aaron Bentley
Added ancestry collapsing to graphs
127
    elif len(descendants[list(ancestors[rev_id])[0]]) != 1:
145 by aaron.bentley at utoronto
Reduced graph-collapsing aggressivness
128
        return False
129
    elif len(descendants[rev_id]) != 1:
130
        return False
131
    else:
132
        return True
136 by Aaron Bentley
Allowed disabling ancestry collapsing
133
176 by Aaron Bentley
Added skip labels to edges
134
def compact_ancestors(descendants, ancestors, exceptions=()):
174 by Aaron Bentley
Added ancestry collapsing to graphs
135
    new_ancestors={}
136
    skip = set()
137
    for me, my_parents in ancestors.iteritems():
138
        if me in skip:
139
            continue
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
140
        new_ancestors[me] = {}
174 by Aaron Bentley
Added ancestry collapsing to graphs
141
        for parent in my_parents:
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
142
            new_parent = parent
176 by Aaron Bentley
Added skip labels to edges
143
            distance = 0
174 by Aaron Bentley
Added ancestry collapsing to graphs
144
            while can_skip(new_parent, descendants, ancestors):
176 by Aaron Bentley
Added skip labels to edges
145
                if new_parent in exceptions:
146
                    break
174 by Aaron Bentley
Added ancestry collapsing to graphs
147
                skip.add(new_parent)
148
                if new_parent in new_ancestors:
149
                    del new_ancestors[new_parent]
150
                new_parent = list(ancestors[new_parent])[0]
176 by Aaron Bentley
Added skip labels to edges
151
                distance += 1
152
            new_ancestors[me][new_parent] = distance
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
153
    return new_ancestors
174 by Aaron Bentley
Added ancestry collapsing to graphs
154
189.1.1 by John Arbash Meinel
Adding an html target.
155
def get_rev_info(rev_id, source):
773.1.1 by Andi Albrecht
Fix unreferenced variable nick if revision_id has committer but is a ghost.
156
    """Return the committer, message, nick and date of a revision."""
189.1.1 by John Arbash Meinel
Adding an html target.
157
    committer = None
158
    message = None
159
    date = None
773.1.1 by Andi Albrecht
Fix unreferenced variable nick if revision_id has committer but is a ghost.
160
    nick = None
189.1.1 by John Arbash Meinel
Adding an html target.
161
    if rev_id == 'null:':
476.1.1 by Aaron Bentley
graph-ancestry shows branch nick
162
        return None, 'Null Revision', None, None
161 by Aaron Bentley
Added committer names to nodes
163
    try:
189.1.1 by John Arbash Meinel
Adding an html target.
164
        rev = source.get_revision(rev_id)
161 by Aaron Bentley
Added committer names to nodes
165
    except NoSuchRevision:
166
        try:
173 by Aaron Bentley
Fixed empty committer for null revision
167
            committer = '-'.join(rev_id.split('-')[:-2]).strip(' ')
168
            if committer == '':
476.1.1 by Aaron Bentley
graph-ancestry shows branch nick
169
                return None, None, None, None
161 by Aaron Bentley
Added committer names to nodes
170
        except ValueError:
476.1.1 by Aaron Bentley
graph-ancestry shows branch nick
171
            return None, None, None, None
189.1.1 by John Arbash Meinel
Adding an html target.
172
    else:
173
        committer = short_committer(rev.committer)
174
        if rev.message is not None:
175
            message = rev.message.split('\n')[0]
194 by Aaron Bentley
tweaked Meinel's changes
176
        gmtime = time.gmtime(rev.timestamp + (rev.timezone or 0))
197 by Aaron Bentley
Restored slashes in date, because my rsvg-bin is borken wrt hyphens
177
        date = time.strftime('%Y/%m/%d', gmtime)
476.1.1 by Aaron Bentley
graph-ancestry shows branch nick
178
        nick = rev.properties.get('branch-nick')
166 by Aaron Bentley
Fixed username-in-ancestry-graph issue
179
    if '@' in committer:
180
        try:
181
            committer = mail_map[committer]
182
        except KeyError:
183
            pass
184
    try:
185
        committer = committer_alias[committer]
186
    except KeyError:
187
        pass
476.1.1 by Aaron Bentley
graph-ancestry shows branch nick
188
    return committer, message, nick, date
161 by Aaron Bentley
Added committer names to nodes
189
177 by Aaron Bentley
Restructured graph code as an object
190
class Grapher(object):
629 by Aaron Bentley
Use Graph for graph-ancestry
191
180 by Aaron Bentley
Use Grapher for both kinds of graphs
192
    def __init__(self, branch, other_branch=None):
177 by Aaron Bentley
Restructured graph code as an object
193
        object.__init__(self)
194
        self.branch = branch
195
        self.other_branch = other_branch
629 by Aaron Bentley
Use Graph for graph-ancestry
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)
208 by Aaron Bentley
Fixed last_revision calls
203
        revision_a = self.branch.last_revision()
629 by Aaron Bentley
Use Graph for graph-ancestry
204
        self.scan_graph(revision_a, revision_b)
180 by Aaron Bentley
Use Grapher for both kinds of graphs
205
        self.n_history = branch.revision_history()
595 by Aaron Bentley
Use dotted revnos in graph-ancestry
206
        self.n_revnos = branch.get_revision_id_to_revno_map()
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
207
        self.distances = node_distances(self.descendants, self.ancestors,
177 by Aaron Bentley
Restructured graph code as an object
208
                                        self.root)
180 by Aaron Bentley
Use Grapher for both kinds of graphs
209
        if other_branch is not None:
210
            self.base = select_farthest(self.distances, self.common)
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
211
            self.m_history = other_branch.revision_history()
595 by Aaron Bentley
Use dotted revnos in graph-ancestry
212
            self.m_revnos = other_branch.get_revision_id_to_revno_map()
629 by Aaron Bentley
Use Graph for graph-ancestry
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)
180 by Aaron Bentley
Use Grapher for both kinds of graphs
216
        else:
217
            self.base = None
544 by Aaron Bentley
Update graph-ancestry to support new graph API
218
            self.new_base = None
219
            self.lcas = set()
180 by Aaron Bentley
Use Grapher for both kinds of graphs
220
            self.m_history = []
595 by Aaron Bentley
Use dotted revnos in graph-ancestry
221
            self.m_revnos = {}
222
629 by Aaron Bentley
Use Graph for graph-ancestry
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
595 by Aaron Bentley
Use dotted revnos in graph-ancestry
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))
161 by Aaron Bentley
Added committer names to nodes
254
177 by Aaron Bentley
Restructured graph code as an object
255
    def dot_node(self, node, num):
156 by Aaron Bentley
Switched to merge base pick graphing temporarily
256
        try:
177 by Aaron Bentley
Restructured graph code as an object
257
            n_rev = self.n_history.index(node) + 1
156 by Aaron Bentley
Switched to merge base pick graphing temporarily
258
        except ValueError:
259
            n_rev = None
260
        try:
177 by Aaron Bentley
Restructured graph code as an object
261
            m_rev = self.m_history.index(node) + 1
156 by Aaron Bentley
Switched to merge base pick graphing temporarily
262
        except ValueError:
263
            m_rev = None
155 by Aaron Bentley
Did a bunch of work on merge-base graphs
264
        if (n_rev, m_rev) == (None, None):
595 by Aaron Bentley
Use dotted revnos in graph-ancestry
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:]
155 by Aaron Bentley
Did a bunch of work on merge-base graphs
270
            cluster = None
271
        elif n_rev == m_rev:
272
            name = "rR%d" % n_rev
273
        else:
274
            namelist = []
164 by Aaron Bentley
Fixed varname reuse generating nodes
275
            for prefix, revno in (('r', n_rev), ('R', m_rev)):
276
                if revno is not None:
277
                    namelist.append("%s%d" % (prefix, revno))
155 by Aaron Bentley
Did a bunch of work on merge-base graphs
278
            name = ' '.join(namelist)
279
        if None not in (n_rev, m_rev):
280
            cluster = "common_history"
281
            color = "#ff9900"
282
        elif (None, None) == (n_rev, m_rev):
283
            cluster = None
177 by Aaron Bentley
Restructured graph code as an object
284
            if node in self.common:
155 by Aaron Bentley
Did a bunch of work on merge-base graphs
285
                color = "#6699ff"
286
            else:
190 by Aaron Bentley
Set normal revisions to white
287
                color = "white"
155 by Aaron Bentley
Did a bunch of work on merge-base graphs
288
        elif n_rev is not None:
289
            cluster = "my_history"
290
            color = "#ffff00"
291
        else:
292
            assert m_rev is not None
293
            cluster = "other_history"
294
            color = "#ff0000"
544 by Aaron Bentley
Update graph-ancestry to support new graph API
295
        if node in self.lcas:
296
            color = "#9933cc"
177 by Aaron Bentley
Restructured graph code as an object
297
        if node == self.base:
544 by Aaron Bentley
Update graph-ancestry to support new graph API
298
            color = "#669933"
299
            if node == self.new_base:
300
                color = "#33ff33"
301
        if node == self.new_base:
302
            color = '#33cc99'
155 by Aaron Bentley
Did a bunch of work on merge-base graphs
303
304
        label = [name]
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
305
        committer, message, nick, date = get_rev_info(node,
476.1.1 by Aaron Bentley
graph-ancestry shows branch nick
306
                                                      self.branch.repository)
161 by Aaron Bentley
Added committer names to nodes
307
        if committer is not None:
308
            label.append(committer)
309
476.1.1 by Aaron Bentley
graph-ancestry shows branch nick
310
        if nick is not None:
311
            label.append(nick)
312
189.1.1 by John Arbash Meinel
Adding an html target.
313
        if date is not None:
314
            label.append(date)
188 by Aaron Bentley
Added dates to graph
315
177 by Aaron Bentley
Restructured graph code as an object
316
        if node in self.distances:
178 by Aaron Bentley
Switched from clusters to forced ranking
317
            rank = self.distances[node]
177 by Aaron Bentley
Restructured graph code as an object
318
            label.append('d%d' % self.distances[node])
178 by Aaron Bentley
Switched from clusters to forced ranking
319
        else:
320
            rank = None
172 by Aaron Bentley
Marked missing nodes
321
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
322
        d_node = Node("n%d" % num, color=color, label="\\n".join(label),
189.1.1 by John Arbash Meinel
Adding an html target.
323
                    rev_id=node, cluster=cluster, message=message,
324
                    date=date)
178 by Aaron Bentley
Switched from clusters to forced ranking
325
        d_node.rank = rank
326
177 by Aaron Bentley
Restructured graph code as an object
327
        if node not in self.ancestors:
172 by Aaron Bentley
Marked missing nodes
328
            d_node.node_style.append('dotted')
329
330
        return d_node
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
331
476.1.2 by Aaron Bentley
graph-ancestry can restrict the number of nodes shown by distance
332
    def get_relations(self, collapse=False, max_distance=None):
177 by Aaron Bentley
Restructured graph code as an object
333
        dot_nodes = {}
334
        node_relations = []
335
        num = 0
336
        if collapse:
544 by Aaron Bentley
Update graph-ancestry to support new graph API
337
            exceptions = self.lcas.union([self.base, self.new_base])
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
338
            visible_ancestors = compact_ancestors(self.descendants,
544 by Aaron Bentley
Update graph-ancestry to support new graph API
339
                                                  self.ancestors,
340
                                                  exceptions)
176 by Aaron Bentley
Added skip labels to edges
341
        else:
547 by Aaron Bentley
Fix no-collapse behavior
342
            visible_ancestors = {}
343
            for revision, parents in self.ancestors.iteritems():
344
                visible_ancestors[revision] = dict((p, 0) for p in parents)
476.1.2 by Aaron Bentley
graph-ancestry can restrict the number of nodes shown by distance
345
        if max_distance is not None:
346
            min_distance = max(self.distances.values()) - max_distance
547 by Aaron Bentley
Fix no-collapse behavior
347
            visible_ancestors = dict((n, p) for n, p in
348
                                     visible_ancestors.iteritems() if
349
                                     self.distances[n] >= min_distance)
177 by Aaron Bentley
Restructured graph code as an object
350
        for node, parents in visible_ancestors.iteritems():
351
            if node not in dot_nodes:
352
                dot_nodes[node] = self.dot_node(node, num)
176 by Aaron Bentley
Added skip labels to edges
353
                num += 1
547 by Aaron Bentley
Fix no-collapse behavior
354
            for parent, skipped in parents.iteritems():
177 by Aaron Bentley
Restructured graph code as an object
355
                if parent not in dot_nodes:
356
                    dot_nodes[parent] = self.dot_node(parent, num)
357
                    num += 1
358
                edge = Edge(dot_nodes[parent], dot_nodes[node])
359
                if skipped != 0:
360
                    edge.label = "%d" % skipped
361
                node_relations.append(edge)
362
        return node_relations
155 by Aaron Bentley
Did a bunch of work on merge-base graphs
363
364
160 by Aaron Bentley
Restored old graph-ancestry functionality
365
def write_ancestry_file(branch, filename, collapse=True, antialias=True,
476.1.2 by Aaron Bentley
graph-ancestry can restrict the number of nodes shown by distance
366
                        merge_branch=None, ranking="forced", max_distance=None):
234 by Aaron Bentley
Adopted Branch.open_containing's new format
367
    b = Branch.open_containing(branch)[0]
180 by Aaron Bentley
Use Grapher for both kinds of graphs
368
    if merge_branch is not None:
234 by Aaron Bentley
Adopted Branch.open_containing's new format
369
        m = Branch.open_containing(merge_branch)[0]
180 by Aaron Bentley
Use Grapher for both kinds of graphs
370
    else:
371
        m = None
309 by Aaron Bentley
Fixed graph-ancestry
372
    b.lock_write()
258 by Aaron Bentley
Added locking to graph-ancestry
373
    try:
374
        if m is not None:
375
            m.lock_read()
376
        try:
377
            grapher = Grapher(b, m)
476.1.2 by Aaron Bentley
graph-ancestry can restrict the number of nodes shown by distance
378
            relations = grapher.get_relations(collapse, max_distance)
258 by Aaron Bentley
Added locking to graph-ancestry
379
        finally:
380
            if m is not None:
381
                m.unlock()
382
    finally:
383
        b.unlock()
160 by Aaron Bentley
Restored old graph-ancestry functionality
384
134 by Aaron Bentley
support multiple image formats for graph-ancestry
385
    ext = filename.split('.')[-1]
178 by Aaron Bentley
Switched from clusters to forced ranking
386
    output = dot_output(relations, ranking)
186 by Aaron Bentley
Warn instead of failing when librsvg is not installed
387
    done = False
187 by Aaron Bentley
Fixed output handling for non-antialiased types
388
    if ext not in RSVG_OUTPUT_TYPES:
389
        antialias = False
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
390
    if antialias:
186 by Aaron Bentley
Warn instead of failing when librsvg is not installed
391
        output = list(output)
143 by Aaron Bentley
Used rsvga for nice antialiasing
392
        try:
178 by Aaron Bentley
Switched from clusters to forced ranking
393
            invoke_dot_aa(output, filename, ext)
186 by Aaron Bentley
Warn instead of failing when librsvg is not installed
394
            done = True
143 by Aaron Bentley
Used rsvga for nice antialiasing
395
        except NoDot, e:
396
            raise BzrCommandError("Can't find 'dot'.  Please ensure Graphviz"\
397
                " is installed correctly.")
398
        except NoRsvg, e:
186 by Aaron Bentley
Warn instead of failing when librsvg is not installed
399
            print "Not antialiasing because rsvg (from librsvg-bin) is not"\
400
                " installed."
401
            antialias = False
402
    if ext in DOT_OUTPUT_TYPES and not antialias and not done:
139 by Aaron Bentley
Tweaked missing-dot handling
403
        try:
178 by Aaron Bentley
Switched from clusters to forced ranking
404
            invoke_dot(output, filename, ext)
186 by Aaron Bentley
Warn instead of failing when librsvg is not installed
405
            done = True
139 by Aaron Bentley
Tweaked missing-dot handling
406
        except NoDot, e:
407
            raise BzrCommandError("Can't find 'dot'.  Please ensure Graphviz"\
590 by Aaron Bentley
Fix error when dot not found
408
                " is installed correctly.")
194 by Aaron Bentley
tweaked Meinel's changes
409
    elif ext == 'dot' and not done:
178 by Aaron Bentley
Switched from clusters to forced ranking
410
        my_file = file(filename, 'wb')
411
        for fragment in output:
321.1.1 by Aaron Bentley
Forced dot output to UTF-8
412
            my_file.write(fragment.encode('utf-8'))
194 by Aaron Bentley
tweaked Meinel's changes
413
    elif ext == 'html':
189.1.1 by John Arbash Meinel
Adding an html target.
414
        try:
415
            invoke_dot_html(output, filename)
416
        except NoDot, e:
417
            raise BzrCommandError("Can't find 'dot'.  Please ensure Graphviz"\
590 by Aaron Bentley
Fix error when dot not found
418
                " is installed correctly.")
186 by Aaron Bentley
Warn instead of failing when librsvg is not installed
419
    elif not done:
134 by Aaron Bentley
support multiple image formats for graph-ancestry
420
        print "Unknown file extension: %s" % ext