churn.py
119 lines
| 3.5 KiB
| text/x-python
|
PythonLexer
/ hgext / churn.py
|
r6348 | # churn.py - create a graph showing who changed the most lines | ||
# | ||||
# Copyright 2006 Josef "Jeff" Sipek <jeffpc@josefsipek.net> | ||||
# | ||||
# This software may be used and distributed according to the terms | ||||
# of the GNU General Public License, incorporated herein by reference. | ||||
|
r6666 | '''allow graphing the number of lines changed per contributor''' | ||
|
r6348 | |||
from mercurial.i18n import gettext as _ | ||||
|
r6759 | from mercurial import patch, cmdutil, util, node | ||
|
r6348 | import os, sys | ||
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 | ||||
|
r6759 | def countrevs(ui, repo, amap, revs, progress=False): | ||
|
r6348 | stats = {} | ||
|
r6759 | count = pct = 0 | ||
|
r6348 | if not revs: | ||
|
r6759 | revs = range(len(repo)) | ||
|
r6348 | |||
for rev in revs: | ||||
|
r6759 | ctx2 = repo[rev] | ||
parents = ctx2.parents() | ||||
if len(parents) > 1: | ||||
|
r6348 | ui.note(_('Revision %d is a merge, ignoring...\n') % (rev,)) | ||
continue | ||||
|
r6759 | ctx1 = parents[0] | ||
lines = 0 | ||||
ui.pushbuffer() | ||||
patch.diff(repo, ctx1.node(), ctx2.node()) | ||||
diff = ui.popbuffer() | ||||
|
r6348 | |||
|
r6759 | for l in diff.split('\n'): | ||
if (l.startswith("+") and not l.startswith("+++ ") or | ||||
l.startswith("-") and not l.startswith("--- ")): | ||||
lines += 1 | ||||
|
r6348 | |||
|
r6759 | user = util.email(ctx2.user()) | ||
user = amap.get(user, user) # remap | ||||
stats[user] = stats.get(user, 0) + lines | ||||
|
r6955 | ui.debug(_("rev %d: %d lines by %s\n") % (rev, lines, user)) | ||
|
r6348 | |||
if progress: | ||||
|
r6759 | count += 1 | ||
newpct = int(100.0 * count / max(len(revs), 1)) | ||||
if pct < newpct: | ||||
pct = newpct | ||||
|
r6955 | ui.write(_("\rGenerating stats: %d%%") % pct) | ||
|
r6348 | sys.stdout.flush() | ||
if progress: | ||||
ui.write("\r") | ||||
sys.stdout.flush() | ||||
return stats | ||||
def churn(ui, repo, **opts): | ||||
|
r6666 | '''graphs the number of lines changed | ||
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 | |||
|
r6762 | revs = util.sort([int(r) for r in cmdutil.revrange(repo, opts['rev'])]) | ||
|
r6759 | stats = countrevs(ui, repo, amap, revs, opts.get('progress')) | ||
if not stats: | ||||
|
r6348 | return | ||
|
r6762 | stats = util.sort([(-l, u, l) for u,l in stats.items()]) | ||
|
r6759 | maxchurn = float(max(1, stats[0][2])) | ||
maxuser = max([len(u) for k, u, l in stats]) | ||||
|
r6348 | |||
|
r6759 | ttywidth = get_tty_width() | ||
ui.debug(_("assuming %i character terminal\n") % ttywidth) | ||||
width = ttywidth - maxuser - 2 - 6 - 2 - 2 | ||||
|
r6348 | |||
|
r6759 | for k, user, churn in stats: | ||
print "%s %6d %s" % (pad(user, maxuser), churn, | ||||
"*" * int(churn * width / maxchurn)) | ||||
|
r6348 | |||
cmdtable = { | ||||
"churn": | ||||
(churn, | ||||
[('r', 'rev', [], _('limit statistics to the specified revisions')), | ||||
('', 'aliases', '', _('file with email aliases')), | ||||
('', 'progress', None, _('show progress'))], | ||||
|
r6841 | 'hg churn [-r REVISIONS] [--aliases FILE] [--progress]'), | ||
|
r6348 | } | ||