churn.py
184 lines
| 5.8 KiB
| text/x-python
|
PythonLexer
/ hgext / churn.py
|
r7065 | # churn.py - create a graph of revisions count grouped by template | ||
|
r6348 | # | ||
# Copyright 2006 Josef "Jeff" Sipek <jeffpc@josefsipek.net> | ||||
|
r7065 | # Copyright 2008 Alexander Solovyov <piranha@piranha.org.ua> | ||
|
r6348 | # | ||
# This software may be used and distributed according to the terms | ||||
# of the GNU General Public License, incorporated herein by reference. | ||||
|
r7065 | '''allow graphing the number of lines (or count of revisions) grouped by template''' | ||
|
r6348 | |||
|
r7051 | from mercurial.i18n import _ | ||
|
r7065 | from mercurial import patch, cmdutil, util, templater | ||
|
r6348 | import os, sys | ||
|
r7065 | import time, datetime | ||
|
r6348 | |||
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 | ||||
|
r7065 | 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 | ||||
|
r6348 | |||
|
r7065 | 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 | ||||
|
r6348 | continue | ||
|
r7065 | ctx = repo[rev] | ||
key = getkey(ctx) | ||||
key = amap.get(key, key) # alias remap | ||||
|
r7070 | if opts.get('changesets'): | ||
rate[key] = rate.get(key, 0) + 1 | ||||
else: | ||||
|
r7065 | parents = ctx.parents() | ||
if len(parents) > 1: | ||||
ui.note(_('Revision %d is a merge, ignoring...\n') % (rev,)) | ||||
continue | ||||
|
r6348 | |||
|
r7065 | ctx1 = parents[0] | ||
lines = changedlines(ui, repo, ctx1, ctx) | ||||
rate[key] = rate.get(key, 0) + lines | ||||
|
r6348 | |||
|
r7065 | if opts.get('progress'): | ||
|
r6759 | count += 1 | ||
|
r7065 | newpct = int(100.0 * count / max(len(repo), 1)) | ||
|
r6759 | if pct < newpct: | ||
pct = newpct | ||||
|
r6955 | ui.write(_("\rGenerating stats: %d%%") % pct) | ||
|
r6348 | sys.stdout.flush() | ||
|
r7065 | if opts.get('progress'): | ||
|
r6348 | ui.write("\r") | ||
sys.stdout.flush() | ||||
|
r7065 | return rate | ||
|
r7070 | def churn(ui, repo, *pats, **opts): | ||
|
r7065 | '''Graph count of revisions grouped by template | ||
|
r6348 | |||
|
r7070 | Will graph count of changed lines or revisions grouped by template or | ||
alternatively by date, if dateformat is used. In this case it will override | ||||
template. | ||||
|
r7065 | |||
|
r7070 | By default statistics are counted for number of changed lines. | ||
|
r7065 | |||
Examples: | ||||
|
r6666 | |||
|
r7070 | # display count of changed lines for every committer | ||
hg churn -t '{author|email}' | ||||
|
r7065 | |||
# display daily activity graph | ||||
|
r7070 | hg churn -f '%H' -s -c | ||
|
r6666 | |||
|
r7065 | # display activity of developers by month | ||
|
r7070 | hg churn -f '%Y-%m' -s -c | ||
|
r6348 | |||
|
r7065 | # display count of lines changed in every year | ||
|
r7070 | hg churn -f '%Y' -s | ||
The map file format used to specify aliases is fairly simple: | ||||
<alias email> <actual email>''' | ||||
|
r6348 | def pad(s, l): | ||
|
r6759 | return (s + " " * l)[:l] | ||
|
r6348 | |||
amap = {} | ||||
aliases = opts.get('aliases') | ||||
if aliases: | ||||
|
r6759 | for l in open(aliases, "r"): | ||
l = l.strip() | ||||
alias, actual = l.split() | ||||
amap[alias] = actual | ||||
|
r6348 | |||
|
r7065 | rate = countrate(ui, repo, amap, *pats, **opts).items() | ||
if not rate: | ||||
|
r6348 | return | ||
|
r7065 | 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) | ||||
|
r6348 | |||
|
r6759 | ttywidth = get_tty_width() | ||
ui.debug(_("assuming %i character terminal\n") % ttywidth) | ||||
|
r7065 | width = ttywidth - maxname - 2 - 6 - 2 - 2 | ||
for date, count in rate: | ||||
print "%s %6d %s" % (pad(date, maxname), count, | ||||
"*" * int(count * width / maxcount)) | ||||
|
r6348 | |||
cmdtable = { | ||||
|
r7070 | "churn": | ||
(churn, | ||||
|
r7065 | [('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')), | ||||
|
r7070 | ('c', 'changesets', False, _('count rate by number of changesets')), | ||
|
r7065 | ('s', 'sort', False, _('sort by key (default: sort by count)')), | ||
('', 'aliases', '', _('file with email aliases')), | ||||
('', 'progress', None, _('show progress'))], | ||||
|
r7070 | _("hg stats [-d DATE] [-r REV] [--aliases FILE] [--progress] [FILE]")), | ||
|
r6348 | } | ||