~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to graph.py

  • Committer: Aaron Bentley
  • Date: 2013-08-20 03:02:43 UTC
  • Revision ID: aaron@aaronbentley.com-20130820030243-r8v1xfbcnd8f10p4
Fix zap command for 2.6/7

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.fetch import greedy_fetch
23
 
from bzrlib.graph import node_distances, select_farthest
24
 
from bzrlib.revision import combined_graph, revision_graph
25
 
from bzrlib.revision import MultipleRevisionSources
26
 
import bzrlib.errors
27
 
import re
28
 
import os.path
29
 
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
 
30
110
 
31
111
mail_map = {'aaron.bentley@utoronto.ca'     : 'Aaron Bentley',
32
112
            'abentley@panoramicfeedback.com': 'Aaron Bentley',
57
137
    for me, my_parents in ancestors.iteritems():
58
138
        if me in skip:
59
139
            continue
60
 
        new_ancestors[me] = {} 
 
140
        new_ancestors[me] = {}
61
141
        for parent in my_parents:
62
 
            new_parent = parent 
 
142
            new_parent = parent
63
143
            distance = 0
64
144
            while can_skip(new_parent, descendants, ancestors):
65
145
                if new_parent in exceptions:
70
150
                new_parent = list(ancestors[new_parent])[0]
71
151
                distance += 1
72
152
            new_ancestors[me][new_parent] = distance
73
 
    return new_ancestors    
 
153
    return new_ancestors
74
154
 
75
155
def get_rev_info(rev_id, source):
76
 
    """Return the committer, message, and date of a revision."""
 
156
    """Return the committer, message, nick and date of a revision."""
77
157
    committer = None
78
158
    message = None
79
159
    date = None
 
160
    nick = None
80
161
    if rev_id == 'null:':
81
 
        return None, 'Null Revision', None
 
162
        return None, 'Null Revision', None, None
82
163
    try:
83
164
        rev = source.get_revision(rev_id)
84
165
    except NoSuchRevision:
85
166
        try:
86
167
            committer = '-'.join(rev_id.split('-')[:-2]).strip(' ')
87
168
            if committer == '':
88
 
                return None, None, None
 
169
                return None, None, None, None
89
170
        except ValueError:
90
 
            return None, None, None
 
171
            return None, None, None, None
91
172
    else:
92
173
        committer = short_committer(rev.committer)
93
174
        if rev.message is not None:
94
175
            message = rev.message.split('\n')[0]
95
176
        gmtime = time.gmtime(rev.timestamp + (rev.timezone or 0))
96
177
        date = time.strftime('%Y/%m/%d', gmtime)
 
178
        nick = rev.properties.get('branch-nick')
97
179
    if '@' in committer:
98
180
        try:
99
181
            committer = mail_map[committer]
103
185
        committer = committer_alias[committer]
104
186
    except KeyError:
105
187
        pass
106
 
    return committer, message, date
 
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
 
            greedy_fetch(branch, 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
 
 
128
 
        self.n_history = branch.revision_history()
129
 
        self.distances = node_distances(self.descendants, self.ancestors, 
 
204
        self.scan_graph(revision_a, revision_b)
 
205
        self.n_history = list(self.graph.iter_lefthand_ancestry(revision_a))
 
206
        self.n_history.reverse()
 
207
        self.n_revnos = branch.get_revision_id_to_revno_map()
 
208
        self.distances = node_distances(self.descendants, self.ancestors,
130
209
                                        self.root)
131
210
        if other_branch is not None:
132
211
            self.base = select_farthest(self.distances, self.common)
133
 
            self.m_history = other_branch.revision_history() 
 
212
            self.m_history = self.graph.iter_lefthand_ancestry(revision_b)
 
213
            self.m_history = list(self.m_history)
 
214
            self.m_history.reverse()
 
215
            self.m_revnos = other_branch.get_revision_id_to_revno_map()
 
216
            self.new_base = self.graph.find_unique_lca(revision_a,
 
217
                                                       revision_b)
 
218
            self.lcas = self.graph.find_lca(revision_a, revision_b)
134
219
        else:
135
220
            self.base = None
 
221
            self.new_base = None
 
222
            self.lcas = set()
136
223
            self.m_history = []
 
224
            self.m_revnos = {}
 
225
 
 
226
    def scan_graph(self, revision_a, revision_b):
 
227
        a_ancestors = dict(self.graph.iter_ancestry([revision_a]))
 
228
        self.ancestors = a_ancestors
 
229
        self.root = NULL_REVISION
 
230
        if revision_b is not None:
 
231
            b_ancestors = dict(self.graph.iter_ancestry([revision_b]))
 
232
            self.common = set(a_ancestors.keys())
 
233
            self.common.intersection_update(b_ancestors)
 
234
            self.ancestors.update(b_ancestors)
 
235
        else:
 
236
            self.common = []
 
237
            revision_b = None
 
238
        self.descendants = {}
 
239
        ghosts = set()
 
240
        for revision, parents in self.ancestors.iteritems():
 
241
            self.descendants.setdefault(revision, [])
 
242
            if parents is None:
 
243
                ghosts.add(revision)
 
244
                parents = [NULL_REVISION]
 
245
            for parent in parents:
 
246
                self.descendants.setdefault(parent, []).append(revision)
 
247
        for ghost in ghosts:
 
248
            self.ancestors[ghost] = [NULL_REVISION]
 
249
 
 
250
    @staticmethod
 
251
    def _get_revno_str(prefix, revno_map, revision_id):
 
252
        try:
 
253
            revno = revno_map[revision_id]
 
254
        except KeyError:
 
255
            return None
 
256
        return '%s%s' % (prefix, '.'.join(str(n) for n in revno))
137
257
 
138
258
    def dot_node(self, node, num):
139
259
        try:
145
265
        except ValueError:
146
266
            m_rev = None
147
267
        if (n_rev, m_rev) == (None, None):
148
 
            name = node[-5:]
 
268
            name = self._get_revno_str('r', self.n_revnos, node)
 
269
            if name is None:
 
270
                name = self._get_revno_str('R', self.m_revnos, node)
 
271
            if name is None:
 
272
                name = node[-5:]
149
273
            cluster = None
150
274
        elif n_rev == m_rev:
151
275
            name = "rR%d" % n_rev
171
295
            assert m_rev is not None
172
296
            cluster = "other_history"
173
297
            color = "#ff0000"
 
298
        if node in self.lcas:
 
299
            color = "#9933cc"
174
300
        if node == self.base:
175
 
            color = "#33ff99"
 
301
            color = "#669933"
 
302
            if node == self.new_base:
 
303
                color = "#33ff33"
 
304
        if node == self.new_base:
 
305
            color = '#33cc99'
176
306
 
177
307
        label = [name]
178
 
        committer, message, date = get_rev_info(node, self.branch.repository)
 
308
        committer, message, nick, date = get_rev_info(node,
 
309
                                                      self.branch.repository)
179
310
        if committer is not None:
180
311
            label.append(committer)
181
312
 
 
313
        if nick is not None:
 
314
            label.append(nick)
 
315
 
182
316
        if date is not None:
183
317
            label.append(date)
184
318
 
188
322
        else:
189
323
            rank = None
190
324
 
191
 
        d_node = Node("n%d" % num, color=color, label="\\n".join(label), 
 
325
        d_node = Node("n%d" % num, color=color, label="\\n".join(label),
192
326
                    rev_id=node, cluster=cluster, message=message,
193
327
                    date=date)
194
328
        d_node.rank = rank
197
331
            d_node.node_style.append('dotted')
198
332
 
199
333
        return d_node
200
 
        
201
 
    def get_relations(self, collapse=False):
 
334
 
 
335
    def get_relations(self, collapse=False, max_distance=None):
202
336
        dot_nodes = {}
203
337
        node_relations = []
204
338
        num = 0
205
339
        if collapse:
206
 
            visible_ancestors = compact_ancestors(self.descendants, 
207
 
                                                  self.ancestors, (self.base,))
 
340
            exceptions = self.lcas.union([self.base, self.new_base])
 
341
            visible_ancestors = compact_ancestors(self.descendants,
 
342
                                                  self.ancestors,
 
343
                                                  exceptions)
208
344
        else:
209
 
            visible_ancestors = self.ancestors
 
345
            visible_ancestors = {}
 
346
            for revision, parents in self.ancestors.iteritems():
 
347
                visible_ancestors[revision] = dict((p, 0) for p in parents)
 
348
        if max_distance is not None:
 
349
            min_distance = max(self.distances.values()) - max_distance
 
350
            visible_ancestors = dict((n, p) for n, p in
 
351
                                     visible_ancestors.iteritems() if
 
352
                                     self.distances[n] >= min_distance)
210
353
        for node, parents in visible_ancestors.iteritems():
211
354
            if node not in dot_nodes:
212
355
                dot_nodes[node] = self.dot_node(node, num)
213
356
                num += 1
214
 
            if visible_ancestors is self.ancestors:
215
 
                parent_iter = ((f, 0) for f in parents)
216
 
            else:
217
 
                parent_iter = (f for f in parents.iteritems())
218
 
            for parent, skipped in parent_iter:
 
357
            for parent, skipped in parents.iteritems():
219
358
                if parent not in dot_nodes:
220
359
                    dot_nodes[parent] = self.dot_node(parent, num)
221
360
                    num += 1
227
366
 
228
367
 
229
368
def write_ancestry_file(branch, filename, collapse=True, antialias=True,
230
 
                        merge_branch=None, ranking="forced"):
 
369
                        merge_branch=None, ranking="forced", max_distance=None):
231
370
    b = Branch.open_containing(branch)[0]
232
371
    if merge_branch is not None:
233
372
        m = Branch.open_containing(merge_branch)[0]
239
378
            m.lock_read()
240
379
        try:
241
380
            grapher = Grapher(b, m)
242
 
            relations = grapher.get_relations(collapse)
 
381
            relations = grapher.get_relations(collapse, max_distance)
243
382
        finally:
244
383
            if m is not None:
245
384
                m.unlock()
251
390
    done = False
252
391
    if ext not in RSVG_OUTPUT_TYPES:
253
392
        antialias = False
254
 
    if antialias: 
 
393
    if antialias:
255
394
        output = list(output)
256
395
        try:
257
396
            invoke_dot_aa(output, filename, ext)
269
408
            done = True
270
409
        except NoDot, e:
271
410
            raise BzrCommandError("Can't find 'dot'.  Please ensure Graphviz"\
272
 
                " is installed correctly, or use --noantialias")
 
411
                " is installed correctly.")
273
412
    elif ext == 'dot' and not done:
274
413
        my_file = file(filename, 'wb')
275
414
        for fragment in output:
276
 
            my_file.write(fragment)
 
415
            my_file.write(fragment.encode('utf-8'))
277
416
    elif ext == 'html':
278
417
        try:
279
418
            invoke_dot_html(output, filename)
280
419
        except NoDot, e:
281
420
            raise BzrCommandError("Can't find 'dot'.  Please ensure Graphviz"\
282
 
                " is installed correctly, or use --noantialias")
 
421
                " is installed correctly.")
283
422
    elif not done:
284
423
        print "Unknown file extension: %s" % ext
285