hgext/churn.py
author Alexander Solovyov <piranha@piranha.org.ua>
Fri, 03 Oct 2008 00:07:38 +0300
changeset 7065 9bc46d069a76
parent 7051 5f201f711932
child 7070 2627ef59195d
permissions -rw-r--r--
churn: generalisation, now it is possible to see statistics grouped by custom template

# churn.py - create a graph of revisions count grouped by template
#
# Copyright 2006 Josef "Jeff" Sipek <jeffpc@josefsipek.net>
# Copyright 2008 Alexander Solovyov <piranha@piranha.org.ua>
#
# This software may be used and distributed according to the terms
# of the GNU General Public License, incorporated herein by reference.
'''allow graphing the number of lines (or count of revisions) grouped by template'''

from mercurial.i18n import _
from mercurial import patch, cmdutil, util, templater
import os, sys
import time, datetime

def get_tty_width():
    if 'COLUMNS' in os.environ:
        try:
            return int(os.environ['COLUMNS'])
        except ValueError:
            pass
    try:
        import termios, array, fcntl
        for dev in (sys.stdout, sys.stdin):
            try:
                fd = dev.fileno()
                if not os.isatty(fd):
                    continue
                arri = fcntl.ioctl(fd, termios.TIOCGWINSZ, '\0' * 8)
                return array.array('h', arri)[1]
            except ValueError:
                pass
    except ImportError:
        pass
    return 80

def maketemplater(ui, repo, tmpl):
    tmpl = templater.parsestring(tmpl, quoted=False)
    try:
        t = cmdutil.changeset_templater(ui, repo, False, None, False)
    except SyntaxError, inst:
        raise util.Abort(inst.args[0])
    t.use_template(tmpl)
    return t

def changedlines(ui, repo, ctx1, ctx2):
    lines = 0
    ui.pushbuffer()
    patch.diff(repo, ctx1.node(), ctx2.node())
    diff = ui.popbuffer()
    for l in diff.split('\n'):
        if (l.startswith("+") and not l.startswith("+++ ") or
            l.startswith("-") and not l.startswith("--- ")):
            lines += 1
    return lines

def countrate(ui, repo, amap, *pats, **opts):
    """Calculate stats"""
    if opts.get('dateformat'):
        def getkey(ctx):
            t, tz = ctx.date()
            date = datetime.datetime(*time.gmtime(float(t) - tz)[:6])
            return date.strftime(opts['dateformat'])
    else:
        tmpl = opts.get('template', '{author|email}')
        tmpl = maketemplater(ui, repo, tmpl)
        def getkey(ctx):
            ui.pushbuffer()
            tmpl.show(changenode=ctx.node())
            return ui.popbuffer()

    count = pct = 0
    rate = {}
    df = False
    if opts.get('date'):
        df = util.matchdate(opts['date'])

    get = util.cachefunc(lambda r: repo[r].changeset())
    changeiter, matchfn = cmdutil.walkchangerevs(ui, repo, pats, get, opts)
    for st, rev, fns in changeiter:
        if not st == 'add':
            continue
        if df and not df(get(rev)[2][0]): # doesn't match date format
            continue

        ctx = repo[rev]
        key = getkey(ctx)
        key = amap.get(key, key) # alias remap
        if opts.get('lines'):
            parents = ctx.parents()
            if len(parents) > 1:
                ui.note(_('Revision %d is a merge, ignoring...\n') % (rev,))
                continue

            ctx1 = parents[0]
            lines = changedlines(ui, repo, ctx1, ctx)
            rate[key] = rate.get(key, 0) + lines
        else:
            rate[key] = rate.get(key, 0) + 1

        if opts.get('progress'):
            count += 1
            newpct = int(100.0 * count / max(len(repo), 1))
            if pct < newpct:
                pct = newpct
                ui.write(_("\rGenerating stats: %d%%") % pct)
                sys.stdout.flush()

    if opts.get('progress'):
        ui.write("\r")
        sys.stdout.flush()

    return rate


def stats(ui, repo, *pats, **opts):
    '''Graph count of revisions grouped by template

    Will graph count of revisions grouped by template or alternatively by
    date, if dateformat is used. In this case it will override template.

    By default statistics are counted for number of revisions.

    Examples:

      # display count of revisions for every committer
      hg stats -t '{author|email}'

      # display daily activity graph
      hg stats -f '%H' -s

      # display activity of developers by month
      hg stats -f '%Y-%m' -s

      # display count of lines changed in every year
      hg stats -l -f '%Y' -s
    '''
    def pad(s, l):
        return (s + " " * l)[:l]

    amap = {}
    aliases = opts.get('aliases')
    if aliases:
        for l in open(aliases, "r"):
            l = l.strip()
            alias, actual = l.split()
            amap[alias] = actual

    rate = countrate(ui, repo, amap, *pats, **opts).items()
    if not rate:
        return

    keyfn = (not opts.get('sort')) and (lambda (k,v): (v,k)) or None
    rate.sort(key=keyfn, reverse=not opts.get('sort'))

    maxcount = float(max(v for k, v in rate))
    maxname = max(len(k) for k, v in rate)

    ttywidth = get_tty_width()
    ui.debug(_("assuming %i character terminal\n") % ttywidth)
    width = ttywidth - maxname - 2 - 6 - 2 - 2

    for date, count in rate:
        print "%s %6d %s" % (pad(date, maxname), count,
                             "*" * int(count * width / maxcount))

def churn(ui, repo, **opts):
    '''graphs the number of lines changed

    The map file format used to specify aliases is fairly simple:

    <alias email> <actual email>'''
    stats(ui, repo, lines=True, sort=False, template='{author|email}', **opts)


cmdtable = {
    "stats":
        (stats,
         [('r', 'rev', [], _('count rate for the specified revision or range')),
          ('d', 'date', '', _('count rate for revs matching date spec')),
          ('t', 'template', '{author|email}', _('template to group changesets')),
          ('f', 'dateformat', '',
              _('strftime-compatible format for grouping by date')),
          ('l', 'lines', False, _('count rate by number of changed lines')),
          ('s', 'sort', False, _('sort by key (default: sort by count)')),
          ('', 'aliases', '', _('file with email aliases')),
          ('', 'progress', None, _('show progress'))],
         _("hg stats [-d DATE] [-r REV] [FILE]")),
    "churn":
        (churn,
         [('r', 'rev', [], _('limit statistics to the specified revisions')),
          ('', 'aliases', '', _('file with email aliases')),
          ('', 'progress', None, _('show progress'))],
         'hg churn [-r REVISIONS] [--aliases FILE] [--progress]'),
}