PageRenderTime 50ms CodeModel.GetById 16ms app.highlight 29ms RepoModel.GetById 1ms app.codeStats 0ms

/tortoisehg/hgtk/logview/revgraph.py

https://bitbucket.org/tortoisehg/hgtk/
Python | 556 lines | 538 code | 0 blank | 18 comment | 0 complexity | 5c58ffcb353faae772092331b7666766 MD5 | raw file
  1"""Directed graph production.
  2
  3This module contains the code to produce an ordered directed graph of a
  4Mercurial repository, such as we display in the tree view at the top of the
  5history window.  Original code was from graphlog extension.
  6
  7The generator functions walks through the revision history and for the
  8selected revision emits tuples with the following elements:
  9    
 10       (curr_rev, node, lines, parents)
 11
 12  - Current revision.
 13  - node; defined as tuple (rev_column, rev_color)
 14  - lines; a list of (col, next_col, color, type) indicating the edges between
 15    the current row and the next row
 16  - parent revisions of current revision
 17  
 18The node tuple has the following elements:
 19  - rev_column: Column for the node "circle"
 20  - rev_color: Color used to render the circle
 21  - rev_status: 
 22        0 for normal circle, 
 23        1 for add up arrow (outgoing cset)
 24        2 for add star (new cset)
 25        3 for add down arrow (incoming cset)
 26        All above +4 for larger circle = current workdir parent
 27      
 28The lines tuple has the following elements:
 29  - col: Column for the upper end of the line.
 30  - nextcol: Column for the lower end of the line.
 31  - color: Colour used for line
 32  - type: 0 for plain line, 1 for loose upper end, 2 for loose lower end
 33    loose ends are rendered as dashed lines. They indicate that the
 34    edge of the graph does not really end here. This is used to render 
 35    a branch view where the focus is on branches instead of revisions.
 36    
 37The data is used in treeview.populate with the following signature
 38    (rev, node, lines, parents) = self.grapher.next()
 39and stored in treeview.graphdata
 40
 41The treeview.model is an instance of treemodel which references 
 42treeview.graphdata
 43
 44treemodel stores it in self.line_graph_data, and extracts it 
 45in on_get_value, where it is mapped to several columns.
 46    REVID, NODE, LINES, PARENTS, LAST_LINES
 47LAST_LINES is a copy of LINES from the previous row
 48
 49treeview maps columns 
 50    treemodel.NODE, treemodel.LAST_LINES, treemodel.LINES
 51to CellRendererGraph attributes
 52    "node", "in-lines", "out-lines"
 53which stores it in varables
 54    node, in_lines, out_lines
 55"""
 56
 57__copyright__ = "Copyright 2007 Joel Rosdahl, 2008 Steve Borho"
 58__author__    = "Joel Rosdahl <joel@rosdahl.net>, Steve Borho <steve@borho.org>"
 59
 60import re
 61
 62from mercurial.node import nullrev
 63from mercurial import cmdutil, util, match
 64
 65from tortoisehg.util import hglib
 66
 67def __get_parents(repo, rev):
 68    return [x for x in repo.changelog.parentrevs(rev) if x != nullrev]
 69
 70known_branch_colors = {} # cache for repository colour configuration
 71
 72def _get_known_branch_colors(repo):
 73    global known_branch_colors
 74
 75    repo_setting = repo.ui.config('tortoisehg', 'branchcolors')
 76
 77    if known_branch_colors:
 78        branch_colors, setting = known_branch_colors
 79        if repo_setting == setting:
 80            return branch_colors
 81
 82    branchcolors = repo_setting
 83    if not branchcolors:
 84        return {}
 85
 86    branchcolors = hglib.tounicode(branchcolors)
 87    branchcolors = [x for x in re.split(r'(?:(?<=\\\\)|(?<!\\)) ', branchcolors) if x]
 88    values = {}
 89    for branchcolor in branchcolors:
 90        parts = re.split(r'(?:(?<=\\\\)|(?<!\\)):', branchcolor)
 91        if len(parts) != 2:
 92            continue # ignore badly formed entry
 93
 94        # Mercurial branch names are encoded in utf-8 so we must
 95        # make sure to encode back to that after having unescaped
 96        # the string.
 97        branch_name = hglib.toutf(parts[0].replace('\\:', ':').replace('\\ ', ' ').decode('unicode_escape'))
 98        values[branch_name] = hglib.toutf(parts[1])
 99
100    known_branch_colors = values, repo_setting
101    return values
102
103def _color_of_branch(repo, rev):
104    branch = repo[rev].branch()
105
106    candidates = _get_known_branch_colors(repo)
107
108    if branch in candidates:
109        return candidates[branch]
110
111    if branch == 'default':
112        color = 0
113    else:
114        color = sum([ord(c) for c in branch])
115    return color
116
117def _color_of(repo, rev, nextcolor, preferredcolor, branch_color=False):
118    if not branch_color:
119        if preferredcolor[0]:
120            rv = preferredcolor[0]
121            preferredcolor[0] = None
122        else:
123            rv = nextcolor[0]
124            nextcolor[0] = nextcolor[0] + 1
125        return rv
126    else:
127        return _color_of_branch(repo, rev)
128
129type_PLAIN = 0
130type_LOOSE_LOW = 1
131type_LOOSE_HIGH = 2
132
133def revision_grapher(repo, start_rev=-1, stop_rev=-1, branch=None, noheads=False,
134    branch_color=False, only_revs=[]):
135    """incremental revision grapher
136
137    This grapher generates a full graph where every edge is visible.
138    This means that repeated merges between two branches may make
139    the graph very wide.
140    
141    if branch is set to the name of a branch, only this branch is shown.
142    if noheads is True, other heads than start_rev is hidden. Use this
143    to show ancestors of a revision.
144    if branch_color is True, the branch colour is determined by a hash
145    of the branch tip, and will thus always be the same.
146    if only_revs is not [], only those revisions will be shown.
147    """
148
149    shown_revs = None
150    if only_revs:
151        shown_revs = set([repo[repo.lookup(r)].rev() for r in only_revs])
152        start_rev = max(shown_revs)
153        stop_rev = min(shown_revs)
154
155    assert start_rev >= stop_rev
156    curr_rev = start_rev
157    revs = []
158    rev_color = {}
159    nextcolor = [0]
160    
161    def hidden(rev):
162        return (shown_revs and (rev not in shown_revs)) or False
163    
164    while curr_rev >= stop_rev:
165        if hidden(curr_rev):
166            curr_rev -= 1
167            continue
168        # Compute revs and next_revs.
169        if curr_rev not in revs:
170            if noheads and curr_rev != start_rev:
171                curr_rev -= 1
172                continue
173            if branch:
174                ctx = repo.changectx(curr_rev)
175                if ctx.branch() != branch:
176                    curr_rev -= 1
177                    continue
178            # New head.
179            revs.append(curr_rev)
180            rev_color[curr_rev] = _color_of(repo, curr_rev, nextcolor, [None], branch_color)
181            r = __get_parents(repo, curr_rev)
182            while r:
183                r0 = r[0]
184                if r0 < stop_rev: break
185                if r0 in rev_color: break
186                if not branch_color:
187                    rev_color[r0] = rev_color[curr_rev]
188                else:
189                    rev_color[r0] = _color_of(repo, r0, None, None, True)
190                r = __get_parents(repo, r0)
191        rev_index = revs.index(curr_rev)
192        next_revs = revs[:]
193
194        # Add parents to next_revs.
195        parents = [p for p in __get_parents(repo, curr_rev) if not hidden(p)]
196        parents_to_add = []
197        preferred_color = [rev_color[curr_rev]]
198        for parent in parents:
199            if parent not in next_revs:
200                parents_to_add.append(parent)
201                if parent not in rev_color:
202                    rev_color[parent] = _color_of(repo, parent, nextcolor, preferred_color, branch_color)
203        next_revs[rev_index:rev_index + 1] = parents_to_add
204
205        lines = []
206        for i, rev in enumerate(revs):
207            if rev in next_revs:
208                color = rev_color[rev]
209                lines.append( (i, next_revs.index(rev), color, type_PLAIN) )
210            elif rev == curr_rev:
211                for parent in parents:
212                    color = rev_color[parent]
213                    lines.append( (i, next_revs.index(parent), color, type_PLAIN) )
214
215        yield (curr_rev, (rev_index, rev_color[curr_rev]), lines, None)
216        revs = next_revs
217        curr_rev -= 1
218
219
220class BranchGrapher:
221    """Incremental branch grapher
222
223    This generator function produces a graph that uses loose ends to keep
224    focus on branches. All revisions in the range are shown, but not all edges.
225    The function identifies branches and may use loose ends if an edge links
226    two branches.
227    """
228    
229    def __init__(self, repo, start_rev, stop_rev, branch_filter, branch_color):
230        ''' 
231        start_rev - first (newest) changeset to cover
232        stop_rev - last (oldest) changeset to cover
233        branch_filter - if not None, show this branch and all its ancestors
234        branch_color - true if branch name determines colours
235        '''
236        assert start_rev >= stop_rev
237        self.repo = repo
238        
239        #
240        #  Iterator content
241        #  Variables that keep track of the iterator
242        #
243        
244        # The current revision to process
245        self.curr_rev = start_rev
246        
247        # Process all revs from start_rev to and including stop_rev
248        self.stop_rev = stop_rev
249
250        # List current branches. For each branch the next rev is listed
251        # The order of branches determine their columns
252        self.curr_branches = [start_rev]
253        
254        # For each branch, tell the next version that will show up
255        self.next_in_branch = {}
256        
257        #
258        #  Graph variables
259        #  These hold information related to the graph. Most are computed lazily.
260        #  The graph is split into a number of "branches", defined as paths within
261        #  a named branch.
262        #
263        
264        # Map rev to next rev in branch = parent with same branch name
265        # If two parents, use first that has same branch name.
266        # The parent of last rev in a branch is undefined, 
267        # even if the revsion has a parent rev.
268        self.parent_of = {}
269        
270        # Map rev to newest rev in branch. This identifies the branch that the rev
271        # is part of
272        self.branch4rev = {}
273        
274        # Last revision in branch
275        self.branch_tail = {}
276        
277        # Map branch-id (head-rev of branch) to color
278        self.color4branch = {}
279        
280        # Next colour used. for branches
281        self.nextcolor = 0
282        
283        # If set, show only this branch and all descendants.
284        self.branch_filter = branch_filter
285        
286        # Flag to indicate if coloring is done pr micro-branch or pr named branch
287        self.branch_color = branch_color
288
289    def _get_parents(self, rev):
290        return [x for x in self.repo.changelog.parentrevs(rev) if x != nullrev]
291        
292    def _branch_name(self, rev):
293        return self.repo[rev].branch()
294        
295    def _covered_rev(self, rev):
296        """True if rev is inside the revision range for the iterator"""
297        return self.stop_rev <= rev
298        
299    def _new_branch(self, branch_head):
300        """Mark all unknown revisions in range that are direct ancestors
301        of branch_head as part of the same branch. Stops when stop_rev
302        is passed or a known revision is found"""
303        assert not branch_head in self.branch4rev
304        if self.branch_color:
305            self.color4branch[branch_head] = \
306                _color_of_branch(self.repo, branch_head)
307        else:
308            self.color4branch[branch_head] = self.nextcolor
309            self.nextcolor += 1
310        self.next_in_branch[branch_head] = branch_head
311        branch_name = self._branch_name(branch_head)
312        rev = branch_head
313        while not rev in self.branch4rev:
314            # Add rev to branch
315            self.branch4rev[rev] = branch_head
316            self.branch_tail[branch_head] = rev
317            # Exit if rev is outside visible range
318            if not self._covered_rev(rev):
319                # rev is outside visible range, so we don't know tail location
320                self.branch_tail[branch_head] = 0 # Prev revs wasn't tail
321                return
322            # Find next revision in branch
323            self.parent_of[rev] = None
324            for parent in self._get_parents(rev):
325                if self._branch_name(parent) == branch_name:
326                    self.parent_of[rev] = parent
327                    break
328            if self.parent_of[rev] is None:
329                return
330            rev = self.parent_of[rev]
331
332    def _get_rev_branch(self, rev):
333        """Find revision branch or create a new branch"""
334        branch = self.branch4rev.get(rev)
335        if branch is None:
336            branch = rev
337            self._new_branch(branch)
338        assert rev in self.branch4rev
339        assert branch in self.branch_tail
340        return branch
341        
342    def _compute_next_branches(self):
343        """Compute next row of branches"""
344        next_branches = self.curr_branches[:]
345        # Find branch used by current revision
346        curr_branch = self._get_rev_branch(self.curr_rev)
347        self.next_in_branch[curr_branch] = self.parent_of[self.curr_rev]
348        branch_index = self.curr_branches.index(curr_branch)
349        # Insert branches if parents point to new branches
350        new_parents = 0
351        for parent in self._get_parents(self.curr_rev):
352            branch = self._get_rev_branch(parent)
353            if not branch in next_branches:
354                new_parents += 1
355                next_branches.insert(branch_index + new_parents, branch)
356        # Delete branch if last revision
357        if self.curr_rev == self.branch_tail[curr_branch]:
358            del next_branches[branch_index]
359        # Return result
360        return next_branches
361    
362    def _rev_color(self, rev):
363        """Map a revision to a color"""
364        return self.color4branch[self.branch4rev[rev]]
365
366    def _compute_lines(self, parents, next_branches):
367        # Compute lines (from CUR to NEXT branch row)
368        lines = []
369        curr_rev_branch = self.branch4rev[self.curr_rev]
370        for curr_column, branch in enumerate(self.curr_branches):
371            if branch == curr_rev_branch:
372                # Add lines from current branch to parents
373                for par_rev in parents:
374                    par_branch = self._get_rev_branch(par_rev)
375                    color = self.color4branch[par_branch]
376                    next_column = next_branches.index(par_branch)
377                    line_type = type_PLAIN
378                    if par_rev != self.next_in_branch.get(par_branch):
379                        line_type = type_LOOSE_LOW
380                    lines.append( (curr_column, next_column, color, line_type) )
381            else:
382                # Continue unrelated branch
383                color = self.color4branch[branch]
384                next_column = next_branches.index(branch)
385                lines.append( (curr_column, next_column, color, type_PLAIN) )
386        return lines
387        
388    def more(self):
389        return self.curr_rev >= self.stop_rev
390        
391    def next(self):
392        """Perform one iteration of the branch grapher"""
393        
394        # Compute revision (on CUR branch row)
395        while self.more():
396            rev = self.curr_rev
397            rev_branch = self._get_rev_branch(rev)
398            if rev_branch in self.curr_branches:
399                # Follow parent from known child
400                break
401            elif self.branch_filter is None:
402                # New head - no branch name filter
403                self.curr_branches.append(rev_branch)
404                break
405            elif self._branch_name(rev) == self.branch_filter:
406                # New head - matches branch name filter
407                self.curr_branches.append(rev_branch)
408                break
409            else:
410                # Skip this revision
411                self.curr_rev -= 1
412        
413        # Compute parents (indicates the branches on NEXT branch row that curr_rev links to)
414        parents = self._get_parents(rev)
415        # BUG: __get_parents is not defined - why?
416        
417        # Compute lines (from CUR to NEXT branch row)
418        next_branches = self._compute_next_branches()
419        lines = self._compute_lines(parents, next_branches)
420        
421        # Compute node info (for CUR branch row)
422        rev_column = self.curr_branches.index(rev_branch)
423        rev_color = self.color4branch[rev_branch]
424        node = (rev_column, rev_color)
425        
426        # Next loop
427        self.curr_branches = next_branches
428        self.curr_rev -= 1
429        
430        # Return result
431        return (rev, node, lines, None)
432    
433def branch_grapher(repo, start_rev, stop_rev, branch=None, branch_color=False):
434    grapher = BranchGrapher(repo, start_rev, stop_rev, branch, branch_color)
435    while grapher.more():
436        yield grapher.next()            
437
438
439def filelog_grapher(repo, path):
440    '''
441    Graph the ancestry of a single file (log).  Deletions show
442    up as breaks in the graph.
443    '''
444    fctx = repo.file(path)
445    if not len(fctx):
446        return
447    paths = { path : len(fctx)-1 }
448    revs = []
449    rev_color = {}
450    nextcolor = 0
451    type = type_PLAIN
452    while paths:
453
454        # pick largest revision from known filenames
455        fctx = None
456        for path, filerev in paths.iteritems():
457            f = repo.filectx(path, fileid=filerev)
458            if not fctx or f.rev() > fctx.rev():
459                fctx = f
460
461        # Compute revs and next_revs.
462        if fctx.rev() not in revs:
463            revs.append(fctx.rev())
464            rev_color[fctx.rev()] = nextcolor ; nextcolor += 1
465        curcolor = rev_color[fctx.rev()]
466        index = revs.index(fctx.rev())
467        next_revs = revs[:]
468
469        # Add parents to next_revs.
470        parents_to_add = []
471        for pfctx in fctx.parents():
472            if pfctx.path() != fctx.path():
473                continue
474            if pfctx.rev() not in next_revs:
475                parents_to_add.append(pfctx.rev())
476                if len(fctx.parents()) > 1:
477                    rev_color[pfctx.rev()] = nextcolor ; nextcolor += 1
478                else:
479                    rev_color[pfctx.rev()] = curcolor
480
481        ret = fctx.renamed()
482        if ret:
483            renpath, fnode = ret
484            flog = repo.file(renpath)
485            filerev = flog.rev(fnode)
486            paths[renpath] = filerev
487            rfctx = repo.filectx(renpath, fileid=filerev)
488            if rfctx.rev() not in revs:
489                parents_to_add.append(rfctx.rev())
490                rev_color[rfctx.rev()] = nextcolor ; nextcolor += 1
491
492        parents_to_add.sort()
493        next_revs[index:index + 1] = parents_to_add
494
495        lines = []
496        for i, rev in enumerate(revs):
497            if rev in next_revs:
498                color = rev_color[rev]
499                lines.append((i, next_revs.index(rev), color, type))
500            elif rev == fctx.rev():
501                for pfctx in fctx.parents():
502                    color = rev_color[pfctx.rev()]
503                    lines.append((i, next_revs.index(pfctx.rev()), color, type))
504
505        if fctx.filerev() == 0:
506            del paths[fctx.path()]
507        else:
508            paths[fctx.path()] = fctx.filerev()-1
509        revs = next_revs
510        yield (fctx.rev(), (index, curcolor), lines, fctx.path())
511
512
513
514def dumb_log_generator(repo, revs):
515    for revname in revs:
516        node = repo.lookup(revname)
517        rev = repo.changelog.rev(node)
518        yield (rev, (0,0), [], None)
519
520def filtered_log_generator(repo, pats, opts):
521    '''Fill view model iteratively
522       repo - Mercurial repository object
523       pats - list of file names or patterns
524       opts - command line options for log command
525    '''
526    matching_revs = []
527    only_branch = opts.get('branch', None)
528    df = False
529    if opts['date']:
530        df = util.matchdate(opts['date'])
531
532    def prep(ctx, fns):
533        if only_branch and ctx.branch() != only_branch:
534            return
535        if opts['no_merges'] and len(ctx.parents()) == 2:
536            return
537        if opts['only_merges'] and len(ctx.parents()) != 2:
538            return
539        if df and not df(ctx.date()[0]):
540            return
541        if opts['user'] and not [k for k in opts['user'] if k in ctx.user()]:
542            return
543        if opts['keyword']:
544            for k in [kw.lower() for kw in opts['keyword']]:
545                if (k in ctx.user().lower() or
546                    k in ctx.description().lower() or
547                    k in " ".join(ctx.files()).lower()):
548                    break
549            else:
550                return
551        matching_revs.append(ctx.rev())
552
553    m = match.match(repo.root, repo.root, pats)
554    for ctx in cmdutil.walkchangerevs(repo, m, opts, prep):
555        if ctx.rev() in matching_revs:
556            yield (ctx.rev(), (0,0), [], None)