# HG changeset patch # User Patrick Mezard # Date 1206205306 -3600 # Node ID f8feaa6653196569ffebc57bf82672590edb0c4c # Parent 3b42f7ac6916909aff76de64f1c15fe96f3b650b Make churn an official extension diff -r 3b42f7ac6916 -r f8feaa665319 contrib/churn.py --- a/contrib/churn.py Sat Mar 22 12:48:15 2008 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,206 +0,0 @@ -# churn.py - create a graph showing who changed the most lines -# -# Copyright 2006 Josef "Jeff" Sipek -# -# This software may be used and distributed according to the terms -# of the GNU General Public License, incorporated herein by reference. -# -# -# Aliases map file format is simple one alias per line in the following -# format: -# -# - -from mercurial.i18n import gettext as _ -from mercurial import mdiff, cmdutil, util, node -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 - -def __gather(ui, repo, node1, node2): - def dirtywork(f, mmap1, mmap2): - lines = 0 - - to = mmap1 and repo.file(f).read(mmap1[f]) or None - tn = mmap2 and repo.file(f).read(mmap2[f]) or None - - diff = mdiff.unidiff(to, "", tn, "", f, f).split("\n") - - for line in diff: - if not line: - continue # skip EOF - if line.startswith(" "): - continue # context line - if line.startswith("--- ") or line.startswith("+++ "): - continue # begining of diff - if line.startswith("@@ "): - continue # info line - - # changed lines - lines += 1 - - return lines - - ## - - lines = 0 - - changes = repo.status(node1, node2, None, util.always)[:5] - - modified, added, removed, deleted, unknown = changes - - who = repo.changelog.read(node2)[1] - who = util.email(who) # get the email of the person - - mmap1 = repo.manifest.read(repo.changelog.read(node1)[0]) - mmap2 = repo.manifest.read(repo.changelog.read(node2)[0]) - for f in modified: - lines += dirtywork(f, mmap1, mmap2) - - for f in added: - lines += dirtywork(f, None, mmap2) - - for f in removed: - lines += dirtywork(f, mmap1, None) - - for f in deleted: - lines += dirtywork(f, mmap1, mmap2) - - for f in unknown: - lines += dirtywork(f, mmap1, mmap2) - - return (who, lines) - -def gather_stats(ui, repo, amap, revs=None, progress=False): - stats = {} - - cl = repo.changelog - - if not revs: - revs = range(0, cl.count()) - - nr_revs = len(revs) - cur_rev = 0 - - for rev in revs: - cur_rev += 1 # next revision - - node2 = cl.node(rev) - node1 = cl.parents(node2)[0] - - if cl.parents(node2)[1] != node.nullid: - ui.note(_('Revision %d is a merge, ignoring...\n') % (rev,)) - continue - - who, lines = __gather(ui, repo, node1, node2) - - # remap the owner if possible - if who in amap: - ui.note("using '%s' alias for '%s'\n" % (amap[who], who)) - who = amap[who] - - if not who in stats: - stats[who] = 0 - stats[who] += lines - - ui.note("rev %d: %d lines by %s\n" % (rev, lines, who)) - - if progress: - nr_revs = max(nr_revs, 1) - if int(100.0*(cur_rev - 1)/nr_revs) < int(100.0*cur_rev/nr_revs): - ui.write("\rGenerating stats: %d%%" % (int(100.0*cur_rev/nr_revs),)) - sys.stdout.flush() - - if progress: - ui.write("\r") - sys.stdout.flush() - - return stats - -def churn(ui, repo, **opts): - "Graphs the number of lines changed" - - def pad(s, l): - if len(s) < l: - return s + " " * (l-len(s)) - return s[0:l] - - def graph(n, maximum, width, char): - maximum = max(1, maximum) - n = int(n * width / float(maximum)) - - return char * (n) - - def get_aliases(f): - aliases = {} - - for l in f.readlines(): - l = l.strip() - alias, actual = l.split(" ") - aliases[alias] = actual - - return aliases - - amap = {} - aliases = opts.get('aliases') - if aliases: - try: - f = open(aliases,"r") - except OSError, e: - print "Error: " + e - return - - amap = get_aliases(f) - f.close() - - revs = [int(r) for r in cmdutil.revrange(repo, opts['rev'])] - revs.sort() - stats = gather_stats(ui, repo, amap, revs, opts.get('progress')) - - # make a list of tuples (name, lines) and sort it in descending order - ordered = stats.items() - if not ordered: - return - ordered.sort(lambda x, y: cmp(y[1], x[1])) - max_churn = ordered[0][1] - - tty_width = get_tty_width() - ui.note(_("assuming %i character terminal\n") % tty_width) - tty_width -= 1 - - max_user_width = max([len(user) for user, churn in ordered]) - - graph_width = tty_width - max_user_width - 1 - 6 - 2 - 2 - - for user, churn in ordered: - print "%s %6d %s" % (pad(user, max_user_width), - churn, - graph(churn, max_churn, graph_width, '*')) - -cmdtable = { - "churn": - (churn, - [('r', 'rev', [], _('limit statistics to the specified revisions')), - ('', 'aliases', '', _('file with email aliases')), - ('', 'progress', None, _('show progress'))], - 'hg churn [-r revision range] [-a file] [--progress]'), -} diff -r 3b42f7ac6916 -r f8feaa665319 hgext/churn.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hgext/churn.py Sat Mar 22 18:01:46 2008 +0100 @@ -0,0 +1,206 @@ +# churn.py - create a graph showing who changed the most lines +# +# Copyright 2006 Josef "Jeff" Sipek +# +# This software may be used and distributed according to the terms +# of the GNU General Public License, incorporated herein by reference. +# +# +# Aliases map file format is simple one alias per line in the following +# format: +# +# + +from mercurial.i18n import gettext as _ +from mercurial import mdiff, cmdutil, util, node +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 + +def __gather(ui, repo, node1, node2): + def dirtywork(f, mmap1, mmap2): + lines = 0 + + to = mmap1 and repo.file(f).read(mmap1[f]) or None + tn = mmap2 and repo.file(f).read(mmap2[f]) or None + + diff = mdiff.unidiff(to, "", tn, "", f, f).split("\n") + + for line in diff: + if not line: + continue # skip EOF + if line.startswith(" "): + continue # context line + if line.startswith("--- ") or line.startswith("+++ "): + continue # begining of diff + if line.startswith("@@ "): + continue # info line + + # changed lines + lines += 1 + + return lines + + ## + + lines = 0 + + changes = repo.status(node1, node2, None, util.always)[:5] + + modified, added, removed, deleted, unknown = changes + + who = repo.changelog.read(node2)[1] + who = util.email(who) # get the email of the person + + mmap1 = repo.manifest.read(repo.changelog.read(node1)[0]) + mmap2 = repo.manifest.read(repo.changelog.read(node2)[0]) + for f in modified: + lines += dirtywork(f, mmap1, mmap2) + + for f in added: + lines += dirtywork(f, None, mmap2) + + for f in removed: + lines += dirtywork(f, mmap1, None) + + for f in deleted: + lines += dirtywork(f, mmap1, mmap2) + + for f in unknown: + lines += dirtywork(f, mmap1, mmap2) + + return (who, lines) + +def gather_stats(ui, repo, amap, revs=None, progress=False): + stats = {} + + cl = repo.changelog + + if not revs: + revs = range(0, cl.count()) + + nr_revs = len(revs) + cur_rev = 0 + + for rev in revs: + cur_rev += 1 # next revision + + node2 = cl.node(rev) + node1 = cl.parents(node2)[0] + + if cl.parents(node2)[1] != node.nullid: + ui.note(_('Revision %d is a merge, ignoring...\n') % (rev,)) + continue + + who, lines = __gather(ui, repo, node1, node2) + + # remap the owner if possible + if who in amap: + ui.note("using '%s' alias for '%s'\n" % (amap[who], who)) + who = amap[who] + + if not who in stats: + stats[who] = 0 + stats[who] += lines + + ui.note("rev %d: %d lines by %s\n" % (rev, lines, who)) + + if progress: + nr_revs = max(nr_revs, 1) + if int(100.0*(cur_rev - 1)/nr_revs) < int(100.0*cur_rev/nr_revs): + ui.write("\rGenerating stats: %d%%" % (int(100.0*cur_rev/nr_revs),)) + sys.stdout.flush() + + if progress: + ui.write("\r") + sys.stdout.flush() + + return stats + +def churn(ui, repo, **opts): + "Graphs the number of lines changed" + + def pad(s, l): + if len(s) < l: + return s + " " * (l-len(s)) + return s[0:l] + + def graph(n, maximum, width, char): + maximum = max(1, maximum) + n = int(n * width / float(maximum)) + + return char * (n) + + def get_aliases(f): + aliases = {} + + for l in f.readlines(): + l = l.strip() + alias, actual = l.split(" ") + aliases[alias] = actual + + return aliases + + amap = {} + aliases = opts.get('aliases') + if aliases: + try: + f = open(aliases,"r") + except OSError, e: + print "Error: " + e + return + + amap = get_aliases(f) + f.close() + + revs = [int(r) for r in cmdutil.revrange(repo, opts['rev'])] + revs.sort() + stats = gather_stats(ui, repo, amap, revs, opts.get('progress')) + + # make a list of tuples (name, lines) and sort it in descending order + ordered = stats.items() + if not ordered: + return + ordered.sort(lambda x, y: cmp(y[1], x[1])) + max_churn = ordered[0][1] + + tty_width = get_tty_width() + ui.note(_("assuming %i character terminal\n") % tty_width) + tty_width -= 1 + + max_user_width = max([len(user) for user, churn in ordered]) + + graph_width = tty_width - max_user_width - 1 - 6 - 2 - 2 + + for user, churn in ordered: + print "%s %6d %s" % (pad(user, max_user_width), + churn, + graph(churn, max_churn, graph_width, '*')) + +cmdtable = { + "churn": + (churn, + [('r', 'rev', [], _('limit statistics to the specified revisions')), + ('', 'aliases', '', _('file with email aliases')), + ('', 'progress', None, _('show progress'))], + 'hg churn [-r revision range] [-a file] [--progress]'), +} diff -r 3b42f7ac6916 -r f8feaa665319 tests/test-churn --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-churn Sat Mar 22 18:01:46 2008 +0100 @@ -0,0 +1,31 @@ +#!/bin/sh + +echo "[extensions]" >> $HGRCPATH +echo "churn=" >> $HGRCPATH + +echo % create test repository +hg init repo +cd repo +echo a > a +hg ci -Am adda -u user1 +echo b >> a +echo b > b +hg ci -Am addb -u user2 +echo c >> a +echo c >> b +echo c > c +hg ci -Am addc -u user3 + +echo % churn all +hg churn +echo % churn up to rev 1 +hg churn -r :1 +echo % churn with aliases +cat > ../aliases <