churn.py
193 lines
| 6.4 KiB
| text/x-python
|
PythonLexer
/ hgext / churn.py
Alexander Solovyov
|
r7065 | # churn.py - create a graph of revisions count grouped by template | ||
Patrick Mezard
|
r6348 | # | ||
# Copyright 2006 Josef "Jeff" Sipek <jeffpc@josefsipek.net> | ||||
Alexander Solovyov
|
r7065 | # Copyright 2008 Alexander Solovyov <piranha@piranha.org.ua> | ||
Patrick Mezard
|
r6348 | # | ||
Martin Geisler
|
r8225 | # This software may be used and distributed according to the terms of the | ||
Matt Mackall
|
r10263 | # GNU General Public License version 2 or any later version. | ||
Martin Geisler
|
r8228 | |||
Dirkjan Ochtman
|
r8934 | '''command to display statistics about repository history''' | ||
Patrick Mezard
|
r6348 | |||
Martin Geisler
|
r7051 | from mercurial.i18n import _ | ||
Alexander Solovyov
|
r7065 | from mercurial import patch, cmdutil, util, templater | ||
Martin Geisler
|
r8254 | import sys, os | ||
Alexander Solovyov
|
r7065 | import time, datetime | ||
Patrick Mezard
|
r6348 | |||
Alexander Solovyov
|
r7065 | def maketemplater(ui, repo, tmpl): | ||
tmpl = templater.parsestring(tmpl, quoted=False) | ||||
try: | ||||
Jim Correia
|
r7762 | t = cmdutil.changeset_templater(ui, repo, False, None, None, False) | ||
Alexander Solovyov
|
r7065 | except SyntaxError, inst: | ||
raise util.Abort(inst.args[0]) | ||||
t.use_template(tmpl) | ||||
return t | ||||
madhu@madhu
|
r7870 | def changedlines(ui, repo, ctx1, ctx2, fns): | ||
Alexander Solovyov
|
r9669 | added, removed = 0, 0 | ||
Brendan Cully
|
r9321 | fmatch = cmdutil.matchfiles(repo, fns) | ||
madhu@madhu
|
r7870 | diff = ''.join(patch.diff(repo, ctx1.node(), ctx2.node(), fmatch)) | ||
Alexander Solovyov
|
r7065 | for l in diff.split('\n'): | ||
Alexander Solovyov
|
r9669 | if l.startswith("+") and not l.startswith("+++ "): | ||
added += 1 | ||||
elif l.startswith("-") and not l.startswith("--- "): | ||||
removed += 1 | ||||
return (added, removed) | ||||
Patrick Mezard
|
r6348 | |||
Alexander Solovyov
|
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() | ||||
Dirkjan Ochtman
|
r7369 | tmpl.show(ctx) | ||
Alexander Solovyov
|
r7065 | return ui.popbuffer() | ||
Alexander Solovyov
|
r9672 | state = {'count': 0, 'pct': 0} | ||
Alexander Solovyov
|
r7065 | rate = {} | ||
df = False | ||||
if opts.get('date'): | ||||
df = util.matchdate(opts['date']) | ||||
Matt Mackall
|
r9652 | m = cmdutil.match(repo, pats, opts) | ||
Matt Mackall
|
r9662 | def prep(ctx, fns): | ||
Matt Mackall
|
r9654 | rev = ctx.rev() | ||
Dirkjan Ochtman
|
r9367 | if df and not df(ctx.date()[0]): # doesn't match date format | ||
Matt Mackall
|
r9662 | return | ||
Patrick Mezard
|
r6348 | |||
Alexander Solovyov
|
r7065 | key = getkey(ctx) | ||
key = amap.get(key, key) # alias remap | ||||
Alexander Solovyov
|
r7070 | if opts.get('changesets'): | ||
Alexander Solovyov
|
r9670 | rate[key] = (rate.get(key, (0,))[0] + 1, 0) | ||
Alexander Solovyov
|
r7070 | else: | ||
Alexander Solovyov
|
r7065 | parents = ctx.parents() | ||
if len(parents) > 1: | ||||
ui.note(_('Revision %d is a merge, ignoring...\n') % (rev,)) | ||||
Matt Mackall
|
r9662 | return | ||
Patrick Mezard
|
r6348 | |||
Alexander Solovyov
|
r7065 | ctx1 = parents[0] | ||
madhu@madhu
|
r7870 | lines = changedlines(ui, repo, ctx1, ctx, fns) | ||
Alexander Solovyov
|
r9669 | rate[key] = [r + l for r, l in zip(rate.get(key, (0, 0)), lines)] | ||
Patrick Mezard
|
r6348 | |||
Alexander Solovyov
|
r7065 | if opts.get('progress'): | ||
Alexander Solovyov
|
r9672 | state['count'] += 1 | ||
newpct = int(100.0 * state['count'] / max(len(repo), 1)) | ||||
if state['pct'] < newpct: | ||||
state['pct'] = newpct | ||||
ui.write("\r" + _("generating stats: %d%%") % state['pct']) | ||||
Patrick Mezard
|
r6348 | sys.stdout.flush() | ||
Matt Mackall
|
r9665 | for ctx in cmdutil.walkchangerevs(repo, m, opts, prep): | ||
Matt Mackall
|
r9662 | continue | ||
Alexander Solovyov
|
r7065 | if opts.get('progress'): | ||
Patrick Mezard
|
r6348 | ui.write("\r") | ||
sys.stdout.flush() | ||||
Alexander Solovyov
|
r7065 | return rate | ||
Alexander Solovyov
|
r7070 | def churn(ui, repo, *pats, **opts): | ||
Cédric Duval
|
r8823 | '''histogram of changes to the repository | ||
Patrick Mezard
|
r6348 | |||
Cédric Duval
|
r8823 | This command will display a histogram representing the number | ||
of changed lines or revisions, grouped according to the given | ||||
template. The default template will group changes by author. | ||||
The --dateformat option may be used to group the results by | ||||
date instead. | ||||
Alexander Solovyov
|
r7065 | |||
Cédric Duval
|
r8823 | Statistics are based on the number of changed lines, or | ||
alternatively the number of matching revisions if the | ||||
--changesets option is specified. | ||||
Alexander Solovyov
|
r7065 | |||
Martin Geisler
|
r9205 | Examples:: | ||
Dirkjan Ochtman
|
r6666 | |||
Alexander Solovyov
|
r7070 | # display count of changed lines for every committer | ||
hg churn -t '{author|email}' | ||||
Alexander Solovyov
|
r7065 | |||
# display daily activity graph | ||||
Alexander Solovyov
|
r7070 | hg churn -f '%H' -s -c | ||
Dirkjan Ochtman
|
r6666 | |||
Alexander Solovyov
|
r7065 | # display activity of developers by month | ||
Alexander Solovyov
|
r7070 | hg churn -f '%Y-%m' -s -c | ||
Patrick Mezard
|
r6348 | |||
Alexander Solovyov
|
r7065 | # display count of lines changed in every year | ||
Alexander Solovyov
|
r7070 | hg churn -f '%Y' -s | ||
Cédric Duval
|
r8823 | It is possible to map alternate email addresses to a main address | ||
Martin Geisler
|
r9254 | by providing a file using the following format:: | ||
Dirkjan Ochtman
|
r8843 | |||
Martin Geisler
|
r9205 | <alias email> <actual email> | ||
Martin Geisler
|
r8254 | |||
Martin Geisler
|
r9254 | Such a file may be specified with the --aliases option, otherwise | ||
a .hgchurn file will be looked for in the working directory root. | ||||
Martin Geisler
|
r8254 | ''' | ||
Patrick Mezard
|
r6348 | def pad(s, l): | ||
Matt Mackall
|
r6759 | return (s + " " * l)[:l] | ||
Patrick Mezard
|
r6348 | |||
amap = {} | ||||
aliases = opts.get('aliases') | ||||
Martin Geisler
|
r8254 | if not aliases and os.path.exists(repo.wjoin('.hgchurn')): | ||
aliases = repo.wjoin('.hgchurn') | ||||
Patrick Mezard
|
r6348 | if aliases: | ||
Matt Mackall
|
r6759 | for l in open(aliases, "r"): | ||
l = l.strip() | ||||
alias, actual = l.split() | ||||
amap[alias] = actual | ||||
Patrick Mezard
|
r6348 | |||
Alexander Solovyov
|
r7065 | rate = countrate(ui, repo, amap, *pats, **opts).items() | ||
if not rate: | ||||
Patrick Mezard
|
r6348 | return | ||
Alexander Solovyov
|
r9669 | sortkey = ((not opts.get('sort')) and (lambda x: -sum(x[1])) or None) | ||
Alejandro Santos
|
r9032 | rate.sort(key=sortkey) | ||
Alexander Solovyov
|
r7065 | |||
Nicolas Dumazet
|
r9388 | # Be careful not to have a zero maxcount (issue833) | ||
Alexander Solovyov
|
r9669 | maxcount = float(max(sum(v) for k, v in rate)) or 1.0 | ||
Nicolas Dumazet
|
r9390 | maxname = max(len(k) for k, v in rate) | ||
Patrick Mezard
|
r6348 | |||
Alexander Solovyov
|
r7547 | ttywidth = util.termwidth() | ||
Martin Geisler
|
r9467 | ui.debug("assuming %i character terminal\n" % ttywidth) | ||
Alexander Solovyov
|
r9669 | width = ttywidth - maxname - 2 - 2 - 2 | ||
Alexander Solovyov
|
r7065 | |||
Alexander Solovyov
|
r9669 | if opts.get('diffstat'): | ||
width -= 15 | ||||
def format(name, (added, removed)): | ||||
return "%s %15s %s%s\n" % (pad(name, maxname), | ||||
'+%d/-%d' % (added, removed), | ||||
'+' * charnum(added), | ||||
'-' * charnum(removed)) | ||||
else: | ||||
width -= 6 | ||||
def format(name, count): | ||||
return "%s %6d %s\n" % (pad(name, maxname), sum(count), | ||||
'*' * charnum(sum(count))) | ||||
def charnum(count): | ||||
Matt Mackall
|
r10282 | return int(round(count * width / maxcount)) | ||
Alexander Solovyov
|
r9669 | |||
for name, count in rate: | ||||
ui.write(format(name, count)) | ||||
Patrick Mezard
|
r6348 | |||
cmdtable = { | ||||
Alexander Solovyov
|
r7070 | "churn": | ||
(churn, | ||||
Alexander Solovyov
|
r7065 | [('r', 'rev', [], _('count rate for the specified revision or range')), | ||
Martin Geisler
|
r8028 | ('d', 'date', '', _('count rate for revisions matching date spec')), | ||
Matt Mackall
|
r10282 | ('t', 'template', '{author|email}', | ||
_('template to group changesets')), | ||||
Alexander Solovyov
|
r7065 | ('f', 'dateformat', '', | ||
_('strftime-compatible format for grouping by date')), | ||||
Alexander Solovyov
|
r7070 | ('c', 'changesets', False, _('count rate by number of changesets')), | ||
Alexander Solovyov
|
r7065 | ('s', 'sort', False, _('sort by key (default: sort by count)')), | ||
Alexander Solovyov
|
r9669 | ('', 'diffstat', False, _('display added/removed lines separately')), | ||
Alexander Solovyov
|
r7065 | ('', 'aliases', '', _('file with email aliases')), | ||
('', 'progress', None, _('show progress'))], | ||||
Martin Geisler
|
r7129 | _("hg churn [-d DATE] [-r REV] [--aliases FILE] [--progress] [FILE]")), | ||
Patrick Mezard
|
r6348 | } | ||