PageRenderTime 29ms CodeModel.GetById 7ms app.highlight 18ms RepoModel.GetById 1ms app.codeStats 0ms

/tortoisehg/hgqt/graph.py

https://bitbucket.org/tortoisehg/hgtk/
Python | 362 lines | 333 code | 1 blank | 28 comment | 0 complexity | a918bd345093394f0889eb0ca4f9936d MD5 | raw file
  1# Copyright (c) 2003-2010 LOGILAB S.A. (Paris, FRANCE).
  2# http://www.logilab.fr/ -- mailto:contact@logilab.fr
  3#
  4# This program is free software; you can redistribute it and/or modify it under
  5# the terms of the GNU General Public License as published by the Free Software
  6# Foundation; either version 2 of the License, or (at your option) any later
  7# version.
  8#
  9# This program is distributed in the hope that it will be useful, but WITHOUT
 10# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 11# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
 12#
 13# You should have received a copy of the GNU General Public License along with
 14# this program; if not, write to the Free Software Foundation, Inc.,
 15# 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 16
 17"""helper functions and classes to ease hg revision graph building
 18
 19Based on graphlog's algorithm, with insipration stolen from TortoiseHg
 20revision grapher (now stolen back).
 21"""
 22
 23import time
 24import os
 25import itertools
 26
 27from mercurial import util, error
 28
 29from tortoisehg.util.hglib import tounicode
 30
 31def revision_grapher(repo, **opts):
 32    """incremental revision grapher
 33
 34    This generator function walks through the revision history from
 35    revision start_rev to revision stop_rev (which must be less than
 36    or equal to start_rev) and for each revision emits tuples with the
 37    following elements:
 38
 39      - current revision
 40      - column of the current node in the set of ongoing edges
 41      - color of the node (?)
 42      - lines; a list of (col, next_col, color) indicating the edges between
 43        the current row and the next row
 44      - parent revisions of current revision
 45      - author of the current revision
 46
 47    If follow is True, only generated the subtree from the start_rev head.
 48
 49    If branch is set, only generated the subtree for the given named branch.
 50
 51    If allparents is set, include the branch heads for the selected named
 52    branch heads and all ancestors. If not set, include only the revisions
 53    on the selected named branch.
 54    """
 55
 56    revset = opts.get('revset', None)
 57    if revset:
 58        revset = sorted([repo[r].rev() for r in revset])
 59        start_rev = max(revset)
 60        stop_rev = min(revset)
 61        branch = None
 62        follow = False
 63        hidden = lambda rev: rev not in revset
 64    else:
 65        start_rev = opts.get('start_rev', None)
 66        stop_rev = opts.get('stop_rev', 0)
 67        branch = opts.get('branch', None)
 68        follow = opts.get('follow', False)
 69        hidden = lambda rev: False
 70
 71    assert start_rev is None or start_rev >= stop_rev
 72
 73    curr_rev = start_rev
 74    revs = []
 75    rev_color = {}
 76    nextcolor = 0
 77
 78    if opts.get('allparents') or not branch:
 79        def getparents(ctx):
 80            return [x.rev() for x in ctx.parents() if x]
 81    else:
 82        def getparents(ctx):
 83            return [x.rev() for x in ctx.parents() \
 84                    if x and x.branch() == branch]
 85
 86    while curr_rev is None or curr_rev >= stop_rev:
 87        if hidden(curr_rev):
 88            curr_rev -= 1
 89            continue
 90
 91        # Compute revs and next_revs.
 92        ctx = repo[curr_rev]
 93        # Compute revs and next_revs.
 94        if curr_rev not in revs:
 95            if branch and ctx.branch() != branch:
 96                if curr_rev is None:
 97                    curr_rev = len(repo)
 98                else:
 99                    curr_rev -= 1
100                yield None
101                continue
102
103            # New head.
104            if start_rev and follow and curr_rev != start_rev:
105                curr_rev -= 1
106                continue
107            revs.append(curr_rev)
108            rev_color[curr_rev] = curcolor = nextcolor
109            nextcolor += 1
110            p_revs = getparents(ctx)
111            while p_revs:
112                rev0 = p_revs[0]
113                if rev0 < stop_rev or rev0 in rev_color:
114                    break
115                rev_color[rev0] = curcolor
116                p_revs = getparents(repo[rev0])
117        curcolor = rev_color[curr_rev]
118        rev_index = revs.index(curr_rev)
119        next_revs = revs[:]
120
121        # Add parents to next_revs.
122        parents = [p for p in getparents(ctx) if not hidden(p)]
123        try:
124            author = ctx.user()
125        except error.Abort:
126            author = ''
127        parents_to_add = []
128        if len(parents) > 1:
129            preferred_color = None
130        else:
131            preferred_color = curcolor
132        for parent in parents:
133            if parent not in next_revs:
134                parents_to_add.append(parent)
135                if parent not in rev_color:
136                    if preferred_color:
137                        rev_color[parent] = preferred_color
138                        preferred_color = None
139                    else:
140                        rev_color[parent] = nextcolor
141                        nextcolor += 1
142            preferred_color = None
143
144        # parents_to_add.sort()
145        next_revs[rev_index:rev_index + 1] = parents_to_add
146
147        lines = []
148        for i, rev in enumerate(revs):
149            if rev in next_revs:
150                color = rev_color[rev]
151                lines.append( (i, next_revs.index(rev), color) )
152            elif rev == curr_rev:
153                for parent in parents:
154                    color = rev_color[parent]
155                    lines.append( (i, next_revs.index(parent), color) )
156
157        yield (curr_rev, rev_index, curcolor, lines, parents, author)
158        revs = next_revs
159        if curr_rev is None:
160            curr_rev = len(repo)
161        else:
162            curr_rev -= 1
163
164
165def filelog_grapher(repo, path):
166    '''
167    Graph the ancestry of a single file (log).  Deletions show
168    up as breaks in the graph.
169    '''
170    filerev = len(repo.file(path)) - 1
171    fctx = repo.filectx(path, fileid=filerev)
172    rev = fctx.rev()
173
174    flog = fctx.filelog()
175    heads = [repo.filectx(path, fileid=flog.rev(x)).rev() for x in flog.heads()]
176    assert rev in heads
177    heads.remove(rev)
178
179    revs = []
180    rev_color = {}
181    nextcolor = 0
182    _paths = {}
183
184    while rev >= 0:
185        # Compute revs and next_revs
186        if rev not in revs:
187            revs.append(rev)
188            rev_color[rev] = nextcolor ; nextcolor += 1
189        curcolor = rev_color[rev]
190        index = revs.index(rev)
191        next_revs = revs[:]
192
193        # Add parents to next_revs
194        fctx = repo.filectx(_paths.get(rev, path), changeid=rev)
195        for pfctx in fctx.parents():
196            _paths[pfctx.rev()] = pfctx.path()
197        parents = [pfctx.rev() for pfctx in fctx.parents()]# if f.path() == path]
198        parents_to_add = []
199        for parent in parents:
200            if parent not in next_revs:
201                parents_to_add.append(parent)
202                if len(parents) > 1:
203                    rev_color[parent] = nextcolor ; nextcolor += 1
204                else:
205                    rev_color[parent] = curcolor
206        parents_to_add.sort()
207        next_revs[index:index + 1] = parents_to_add
208
209        lines = []
210        for i, nrev in enumerate(revs):
211            if nrev in next_revs:
212                color = rev_color[nrev]
213                lines.append( (i, next_revs.index(nrev), color) )
214            elif nrev == rev:
215                for parent in parents:
216                    color = rev_color[parent]
217                    lines.append( (i, next_revs.index(parent), color) )
218
219        pcrevs = [pfc.rev() for pfc in fctx.parents()]
220        yield (fctx.rev(), index, curcolor, lines, pcrevs,
221               _paths.get(fctx.rev(), path), fctx.user())
222        revs = next_revs
223
224        if revs:
225            rev = max(revs)
226        else:
227            rev = -1
228        if heads and rev <= heads[-1]:
229            rev = heads.pop()
230
231def mq_patch_grapher(repo):
232    """Graphs unapplied MQ patches"""
233    for patchname in reversed(repo.thgmqunappliedpatches):
234        yield (patchname, 0, "", [], [], "")
235
236class GraphNode(object):
237    """
238    Simple class to encapsulate e hg node in the revision graph. Does
239    nothing but declaring attributes.
240    """
241    def __init__(self, rev, xposition, color, lines, parents, ncols=None,
242                 extra=None):
243        self.rev = rev
244        self.x = xposition
245        self.color = color
246        if ncols is None:
247            ncols = len(lines)
248        self.cols = ncols
249        self.parents = parents
250        self.bottomlines = lines
251        self.toplines = []
252        self.extra = extra
253
254class Graph(object):
255    """
256    Graph object to ease hg repo navigation. The Graph object
257    instanciate a `revision_grapher` generator, and provide a `fill`
258    method to build the graph progressively.
259    """
260    #@timeit
261    def __init__(self, repo, grapher, include_mq=False):
262        self.repo = repo
263        self.maxlog = len(repo)
264        if include_mq:
265            patch_grapher = mq_patch_grapher(self.repo)
266            self.grapher = itertools.chain(patch_grapher, grapher)
267        else:
268            self.grapher = grapher
269        self.nodes = []
270        self.nodesdict = {}
271        self.max_cols = 0
272        self.authors = set()
273
274    def __getitem__(self, idx):
275        if isinstance(idx, slice):
276            # XXX TODO: ensure nodes are built
277            return self.nodes.__getitem__(idx)
278        if idx >= len(self.nodes):
279            # build as many graph nodes as required to answer the
280            # requested idx
281            self.build_nodes(idx)
282        if idx > len(self):
283            return self.nodes[-1]
284        return self.nodes[idx]
285
286    def __len__(self):
287        # len(graph) is the number of actually built graph nodes
288        return len(self.nodes)
289
290    def build_nodes(self, nnodes=None, rev=None):
291        """
292        Build up to `nnodes` more nodes in our graph, or build as many
293        nodes required to reach `rev`.
294
295        If both rev and nnodes are set, build as many nodes as
296        required to reach rev plus nnodes more.
297        """
298        if self.grapher is None:
299            return False
300
301        usetimer = nnodes is None and rev is None
302        if usetimer:
303            if os.name == "nt":
304                timer = time.clock
305            else:
306                timer = time.time
307            startsec = timer()
308
309        stopped = False
310        mcol = set([self.max_cols])
311
312        for vnext in self.grapher:
313            if vnext is None:
314                continue
315            nrev, xpos, color, lines, parents, author = vnext[:6]
316            self.authors.add(author)
317            if not type(nrev) == str and nrev >= self.maxlog:
318                continue
319            gnode = GraphNode(nrev, xpos, color, lines, parents,
320                              extra=vnext[5:])
321            if self.nodes:
322                gnode.toplines = self.nodes[-1].bottomlines
323            self.nodes.append(gnode)
324            self.nodesdict[nrev] = gnode
325            mcol = mcol.union(set([xpos]))
326            mcol = mcol.union(set([max(x[:2]) for x in gnode.bottomlines]))
327            if rev is not None and nrev <= rev:
328                rev = None # we reached rev, switching to nnode counter
329            if rev is None:
330                if nnodes is not None:
331                    nnodes -= 1
332                    if not nnodes:
333                        break
334            if usetimer:
335                cursec = timer()
336                if cursec < startsec or cursec > startsec + 0.1:
337                    break
338        else:
339            self.grapher = None
340            stopped = True
341
342        self.max_cols = max(mcol) + 1
343        return not stopped
344
345    def isfilled(self):
346        return self.grapher is None
347
348    def index(self, rev):
349        if len(self) == 0: # graph is empty, let's build some nodes
350            self.build_nodes(10)
351        if rev is not None and rev < self.nodes[-1].rev:
352            self.build_nodes(self.nodes[-1].rev - rev)
353        if rev in self.nodesdict:
354            return self.nodes.index(self.nodesdict[rev])
355        return -1
356
357    #
358    # File graph method
359    #
360
361    def filename(self, rev):
362        return self.nodesdict[rev].extra[0]