~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to graph.py

  • Committer: Michael Ellerman
  • Date: 2005-11-29 07:12:26 UTC
  • mto: (0.3.1 shelf-dev) (325.1.2 bzrtools)
  • mto: This revision was merged to the branch mainline in revision 334.
  • Revision ID: michael@ellerman.id.au-20051129071226-a04b3f827880025d
Unshelve --pick was broken, because we deleted the whole patch, even when only
part of it was unshelved. So now if we unshelve part of a patch, the patch is
replaced with a new patch that has just the unshelved parts. That's a long way
of saying it does what you'd expect.

Implementing this required changing HunkSelector to return both the selected,
and unselected hunks (ie. patches to shelve, and patches to keep).

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 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
 
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.graph import node_distances, select_farthest
23
 
from bzrlib.revision import combined_graph, revision_graph
24
 
from bzrlib.revision import MultipleRevisionSources
25
 
import bzrlib.errors
26
 
import re
27
 
import os.path
28
 
import time
29
 
 
30
 
mail_map = {'aaron.bentley@utoronto.ca'     : 'Aaron Bentley',
31
 
            'abentley@panoramicfeedback.com': 'Aaron Bentley',
32
 
            'abentley@lappy'                : 'Aaron Bentley',
33
 
            'john@arbash-meinel.com'        : 'John Arbash Meinel',
34
 
            'mbp@sourcefrog.net'            : 'Martin Pool',
35
 
            'robertc@robertcollins.net'     : 'Robert Collins',
36
 
            }
37
 
 
38
 
committer_alias = {'abentley': 'Aaron Bentley'}
39
 
def can_skip(rev_id, descendants, ancestors):
40
 
    if rev_id not in descendants:
41
 
        return False
42
 
    elif rev_id not in ancestors:
43
 
        return False
44
 
    elif len(ancestors[rev_id]) != 1:
45
 
        return False
46
 
    elif len(descendants[list(ancestors[rev_id])[0]]) != 1:
47
 
        return False
48
 
    elif len(descendants[rev_id]) != 1:
49
 
        return False
50
 
    else:
51
 
        return True
52
 
 
53
 
def compact_ancestors(descendants, ancestors, exceptions=()):
54
 
    new_ancestors={}
55
 
    skip = set()
56
 
    for me, my_parents in ancestors.iteritems():
57
 
        if me in skip:
58
 
            continue
59
 
        new_ancestors[me] = {} 
60
 
        for parent in my_parents:
61
 
            new_parent = parent 
62
 
            distance = 0
63
 
            while can_skip(new_parent, descendants, ancestors):
64
 
                if new_parent in exceptions:
65
 
                    break
66
 
                skip.add(new_parent)
67
 
                if new_parent in new_ancestors:
68
 
                    del new_ancestors[new_parent]
69
 
                new_parent = list(ancestors[new_parent])[0]
70
 
                distance += 1
71
 
            new_ancestors[me][new_parent] = distance
72
 
    return new_ancestors    
73
 
 
74
 
def get_rev_info(rev_id, source):
75
 
    """Return the committer, message, and date of a revision."""
76
 
    committer = None
77
 
    message = None
78
 
    date = None
79
 
    if rev_id == 'null:':
80
 
        return None, 'Null Revision', None, None
81
 
    try:
82
 
        rev = source.get_revision(rev_id)
83
 
    except NoSuchRevision:
84
 
        try:
85
 
            committer = '-'.join(rev_id.split('-')[:-2]).strip(' ')
86
 
            if committer == '':
87
 
                return None, None, None, None
88
 
        except ValueError:
89
 
            return None, None, None, None
90
 
    else:
91
 
        committer = short_committer(rev.committer)
92
 
        if rev.message is not None:
93
 
            message = rev.message.split('\n')[0]
94
 
        gmtime = time.gmtime(rev.timestamp + (rev.timezone or 0))
95
 
        date = time.strftime('%Y/%m/%d', gmtime)
96
 
        nick = rev.properties.get('branch-nick')
97
 
    if '@' in committer:
98
 
        try:
99
 
            committer = mail_map[committer]
100
 
        except KeyError:
101
 
            pass
102
 
    try:
103
 
        committer = committer_alias[committer]
104
 
    except KeyError:
105
 
        pass
106
 
    return committer, message, nick, date
107
 
 
108
 
class Grapher(object):
109
 
    def __init__(self, branch, other_branch=None):
110
 
        object.__init__(self)
111
 
        self.branch = branch
112
 
        self.other_branch = other_branch
113
 
        revision_a = self.branch.last_revision()
114
 
        if other_branch is not None:
115
 
            branch.fetch(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, 
130
 
                                        self.root)
131
 
        if other_branch is not None:
132
 
            self.base = select_farthest(self.distances, self.common)
133
 
            self.m_history = other_branch.revision_history() 
134
 
        else:
135
 
            self.base = None
136
 
            self.m_history = []
137
 
 
138
 
    def dot_node(self, node, num):
139
 
        try:
140
 
            n_rev = self.n_history.index(node) + 1
141
 
        except ValueError:
142
 
            n_rev = None
143
 
        try:
144
 
            m_rev = self.m_history.index(node) + 1
145
 
        except ValueError:
146
 
            m_rev = None
147
 
        if (n_rev, m_rev) == (None, None):
148
 
            name = node[-5:]
149
 
            cluster = None
150
 
        elif n_rev == m_rev:
151
 
            name = "rR%d" % n_rev
152
 
        else:
153
 
            namelist = []
154
 
            for prefix, revno in (('r', n_rev), ('R', m_rev)):
155
 
                if revno is not None:
156
 
                    namelist.append("%s%d" % (prefix, revno))
157
 
            name = ' '.join(namelist)
158
 
        if None not in (n_rev, m_rev):
159
 
            cluster = "common_history"
160
 
            color = "#ff9900"
161
 
        elif (None, None) == (n_rev, m_rev):
162
 
            cluster = None
163
 
            if node in self.common:
164
 
                color = "#6699ff"
165
 
            else:
166
 
                color = "white"
167
 
        elif n_rev is not None:
168
 
            cluster = "my_history"
169
 
            color = "#ffff00"
170
 
        else:
171
 
            assert m_rev is not None
172
 
            cluster = "other_history"
173
 
            color = "#ff0000"
174
 
        if node == self.base:
175
 
            color = "#33ff99"
176
 
 
177
 
        label = [name]
178
 
        committer, message, nick, date = get_rev_info(node, 
179
 
                                                      self.branch.repository)
180
 
        if committer is not None:
181
 
            label.append(committer)
182
 
 
183
 
        if nick is not None:
184
 
            label.append(nick)
185
 
 
186
 
        if date is not None:
187
 
            label.append(date)
188
 
 
189
 
        if node in self.distances:
190
 
            rank = self.distances[node]
191
 
            label.append('d%d' % self.distances[node])
192
 
        else:
193
 
            rank = None
194
 
 
195
 
        d_node = Node("n%d" % num, color=color, label="\\n".join(label), 
196
 
                    rev_id=node, cluster=cluster, message=message,
197
 
                    date=date)
198
 
        d_node.rank = rank
199
 
 
200
 
        if node not in self.ancestors:
201
 
            d_node.node_style.append('dotted')
202
 
 
203
 
        return d_node
204
 
       
205
 
    def get_relations(self, collapse=False, max_distance=None):
206
 
        dot_nodes = {}
207
 
        node_relations = []
208
 
        num = 0
209
 
        if collapse:
210
 
            visible_ancestors = compact_ancestors(self.descendants, 
211
 
                                                  self.ancestors, (self.base,))
212
 
        else:
213
 
            visible_ancestors = self.ancestors
214
 
        if max_distance is not None:
215
 
            min_distance = max(self.distances.values()) - max_distance
216
 
            visible_ancestors = dict((n, p) for n, p in visible_ancestors.iteritems() if
217
 
                    self.distances[n] >= min_distance)
218
 
        for node, parents in visible_ancestors.iteritems():
219
 
            if node not in dot_nodes:
220
 
                dot_nodes[node] = self.dot_node(node, num)
221
 
                num += 1
222
 
            if visible_ancestors is self.ancestors:
223
 
                parent_iter = ((f, 0) for f in parents)
224
 
            else:
225
 
                parent_iter = (f for f in parents.iteritems())
226
 
            for parent, skipped in parent_iter:
227
 
                if parent not in dot_nodes:
228
 
                    dot_nodes[parent] = self.dot_node(parent, num)
229
 
                    num += 1
230
 
                edge = Edge(dot_nodes[parent], dot_nodes[node])
231
 
                if skipped != 0:
232
 
                    edge.label = "%d" % skipped
233
 
                node_relations.append(edge)
234
 
        return node_relations
235
 
 
236
 
 
237
 
def write_ancestry_file(branch, filename, collapse=True, antialias=True,
238
 
                        merge_branch=None, ranking="forced", max_distance=None):
239
 
    b = Branch.open_containing(branch)[0]
240
 
    if merge_branch is not None:
241
 
        m = Branch.open_containing(merge_branch)[0]
242
 
    else:
243
 
        m = None
244
 
    b.lock_write()
245
 
    try:
246
 
        if m is not None:
247
 
            m.lock_read()
248
 
        try:
249
 
            grapher = Grapher(b, m)
250
 
            relations = grapher.get_relations(collapse, max_distance)
251
 
        finally:
252
 
            if m is not None:
253
 
                m.unlock()
254
 
    finally:
255
 
        b.unlock()
256
 
 
257
 
    ext = filename.split('.')[-1]
258
 
    output = dot_output(relations, ranking)
259
 
    done = False
260
 
    if ext not in RSVG_OUTPUT_TYPES:
261
 
        antialias = False
262
 
    if antialias: 
263
 
        output = list(output)
264
 
        try:
265
 
            invoke_dot_aa(output, filename, ext)
266
 
            done = True
267
 
        except NoDot, e:
268
 
            raise BzrCommandError("Can't find 'dot'.  Please ensure Graphviz"\
269
 
                " is installed correctly.")
270
 
        except NoRsvg, e:
271
 
            print "Not antialiasing because rsvg (from librsvg-bin) is not"\
272
 
                " installed."
273
 
            antialias = False
274
 
    if ext in DOT_OUTPUT_TYPES and not antialias and not done:
275
 
        try:
276
 
            invoke_dot(output, filename, ext)
277
 
            done = True
278
 
        except NoDot, e:
279
 
            raise BzrCommandError("Can't find 'dot'.  Please ensure Graphviz"\
280
 
                " is installed correctly, or use --noantialias")
281
 
    elif ext == 'dot' and not done:
282
 
        my_file = file(filename, 'wb')
283
 
        for fragment in output:
284
 
            my_file.write(fragment.encode('utf-8'))
285
 
    elif ext == 'html':
286
 
        try:
287
 
            invoke_dot_html(output, filename)
288
 
        except NoDot, e:
289
 
            raise BzrCommandError("Can't find 'dot'.  Please ensure Graphviz"\
290
 
                " is installed correctly, or use --noantialias")
291
 
    elif not done:
292
 
        print "Unknown file extension: %s" % ext
293