churn.py
260 lines
| 7.5 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 | |||
Augie Fackler
|
r40277 | from __future__ import absolute_import, division | ||
Gregory Szorc
|
r28094 | |||
import datetime | ||||
import os | ||||
import time | ||||
Martin Geisler
|
r7051 | from mercurial.i18n import _ | ||
Gregory Szorc
|
r43355 | from mercurial.pycompat import open | ||
Gregory Szorc
|
r28094 | from mercurial import ( | ||
cmdutil, | ||||
encoding, | ||||
Yuya Nishihara
|
r35906 | logcmdutil, | ||
Gregory Szorc
|
r28094 | patch, | ||
Pulkit Goyal
|
r34975 | pycompat, | ||
Yuya Nishihara
|
r32337 | registrar, | ||
Gregory Szorc
|
r28094 | scmutil, | ||
) | ||||
Boris Feld
|
r36625 | from mercurial.utils import dateutil | ||
Patrick Mezard
|
r6348 | |||
Gregory Szorc
|
r21245 | cmdtable = {} | ||
Yuya Nishihara
|
r32337 | command = registrar.command(cmdtable) | ||
Augie Fackler
|
r29841 | # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for | ||
Augie Fackler
|
r25186 | # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should | ||
# be specifying the version(s) of Mercurial they are tested with, or | ||||
# leave the attribute unspecified. | ||||
Augie Fackler
|
r43347 | testedwith = b'ships-with-hg-core' | ||
Augie Fackler
|
r16743 | |||
Augie Fackler
|
r43346 | |||
madhu@madhu
|
r7870 | def changedlines(ui, repo, ctx1, ctx2, fns): | ||
Alexander Solovyov
|
r9669 | added, removed = 0, 0 | ||
Matt Mackall
|
r14322 | fmatch = scmutil.matchfiles(repo, fns) | ||
Augie Fackler
|
r43347 | diff = b''.join(patch.diff(repo, ctx1.node(), ctx2.node(), fmatch)) | ||
for l in diff.split(b'\n'): | ||||
if l.startswith(b"+") and not l.startswith(b"+++ "): | ||||
Alexander Solovyov
|
r9669 | added += 1 | ||
Augie Fackler
|
r43347 | elif l.startswith(b"-") and not l.startswith(b"--- "): | ||
Alexander Solovyov
|
r9669 | removed += 1 | ||
return (added, removed) | ||||
Patrick Mezard
|
r6348 | |||
Augie Fackler
|
r43346 | |||
Alexander Solovyov
|
r7065 | def countrate(ui, repo, amap, *pats, **opts): | ||
"""Calculate stats""" | ||||
Pulkit Goyal
|
r34975 | opts = pycompat.byteskwargs(opts) | ||
Augie Fackler
|
r43347 | if opts.get(b'dateformat'): | ||
Augie Fackler
|
r43346 | |||
Alexander Solovyov
|
r7065 | def getkey(ctx): | ||
t, tz = ctx.date() | ||||
date = datetime.datetime(*time.gmtime(float(t) - tz)[:6]) | ||||
Augie Fackler
|
r40276 | return encoding.strtolocal( | ||
Augie Fackler
|
r43347 | date.strftime(encoding.strfromlocal(opts[b'dateformat'])) | ||
Augie Fackler
|
r43346 | ) | ||
Alexander Solovyov
|
r7065 | else: | ||
Augie Fackler
|
r43347 | tmpl = opts.get(b'oldtemplate') or opts.get(b'template') | ||
Yuya Nishihara
|
r35906 | tmpl = logcmdutil.maketemplater(ui, repo, tmpl) | ||
Augie Fackler
|
r43346 | |||
Alexander Solovyov
|
r7065 | def getkey(ctx): | ||
ui.pushbuffer() | ||||
Dirkjan Ochtman
|
r7369 | tmpl.show(ctx) | ||
Alexander Solovyov
|
r7065 | return ui.popbuffer() | ||
Augie Fackler
|
r43346 | progress = ui.makeprogress( | ||
Augie Fackler
|
r43347 | _(b'analyzing'), unit=_(b'revisions'), total=len(repo) | ||
Augie Fackler
|
r43346 | ) | ||
Alexander Solovyov
|
r7065 | rate = {} | ||
df = False | ||||
Augie Fackler
|
r43347 | if opts.get(b'date'): | ||
df = dateutil.matchdate(opts[b'date']) | ||||
Alexander Solovyov
|
r7065 | |||
Matt Mackall
|
r14671 | m = scmutil.match(repo[None], pats, opts) | ||
Augie Fackler
|
r43346 | |||
Matt Mackall
|
r9662 | def prep(ctx, fns): | ||
Matt Mackall
|
r9654 | rev = ctx.rev() | ||
Augie Fackler
|
r43346 | if df and not df(ctx.date()[0]): # doesn't match date format | ||
Matt Mackall
|
r9662 | return | ||
Patrick Mezard
|
r6348 | |||
Alexander Solovyov
|
r14040 | key = getkey(ctx).strip() | ||
Augie Fackler
|
r43346 | key = amap.get(key, key) # alias remap | ||
Augie Fackler
|
r43347 | if opts.get(b'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: | ||||
Augie Fackler
|
r43347 | ui.note(_(b'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 | |||
Martin von Zweigbergk
|
r38420 | progress.increment() | ||
Patrick Mezard
|
r6348 | |||
Matt Mackall
|
r9665 | for ctx in cmdutil.walkchangerevs(repo, m, opts, prep): | ||
Matt Mackall
|
r9662 | continue | ||
Martin von Zweigbergk
|
r38420 | progress.complete() | ||
Patrick Mezard
|
r6348 | |||
Alexander Solovyov
|
r7065 | return rate | ||
Augie Fackler
|
r43346 | @command( | ||
Augie Fackler
|
r43347 | b'churn', | ||
Augie Fackler
|
r43346 | [ | ||
( | ||||
Augie Fackler
|
r43347 | b'r', | ||
b'rev', | ||||
Augie Fackler
|
r43346 | [], | ||
Augie Fackler
|
r43347 | _(b'count rate for the specified revision or revset'), | ||
_(b'REV'), | ||||
Augie Fackler
|
r43346 | ), | ||
( | ||||
Augie Fackler
|
r43347 | b'd', | ||
b'date', | ||||
b'', | ||||
_(b'count rate for revisions matching date spec'), | ||||
_(b'DATE'), | ||||
Augie Fackler
|
r43346 | ), | ||
( | ||||
Augie Fackler
|
r43347 | b't', | ||
b'oldtemplate', | ||||
b'', | ||||
_(b'template to group changesets (DEPRECATED)'), | ||||
_(b'TEMPLATE'), | ||||
Augie Fackler
|
r43346 | ), | ||
( | ||||
Augie Fackler
|
r43347 | b'T', | ||
b'template', | ||||
b'{author|email}', | ||||
_(b'template to group changesets'), | ||||
_(b'TEMPLATE'), | ||||
Augie Fackler
|
r43346 | ), | ||
( | ||||
Augie Fackler
|
r43347 | b'f', | ||
b'dateformat', | ||||
b'', | ||||
_(b'strftime-compatible format for grouping by date'), | ||||
_(b'FORMAT'), | ||||
Augie Fackler
|
r43346 | ), | ||
Augie Fackler
|
r43347 | (b'c', b'changesets', False, _(b'count rate by number of changesets')), | ||
(b's', b'sort', False, _(b'sort by key (default: sort by count)')), | ||||
(b'', b'diffstat', False, _(b'display added/removed lines separately')), | ||||
(b'', b'aliases', b'', _(b'file with email aliases'), _(b'FILE')), | ||||
Augie Fackler
|
r43346 | ] | ||
+ cmdutil.walkopts, | ||||
Augie Fackler
|
r43347 | _(b"hg churn [-d DATE] [-r REV] [--aliases FILE] [FILE]"), | ||
rdamazio@google.com
|
r40329 | helpcategory=command.CATEGORY_MAINTENANCE, | ||
Augie Fackler
|
r43346 | inferrepo=True, | ||
) | ||||
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 | ||
Matt Harbison
|
r32229 | hg churn -T "{author|email}" | ||
Alexander Solovyov
|
r7065 | |||
# display daily activity graph | ||||
FUJIWARA Katsunori
|
r19959 | hg churn -f "%H" -s -c | ||
Dirkjan Ochtman
|
r6666 | |||
Alexander Solovyov
|
r7065 | # display activity of developers by month | ||
FUJIWARA Katsunori
|
r19959 | hg churn -f "%Y-%m" -s -c | ||
Patrick Mezard
|
r6348 | |||
Alexander Solovyov
|
r7065 | # display count of lines changed in every year | ||
FUJIWARA Katsunori
|
r19959 | hg churn -f "%Y" -s | ||
Alexander Solovyov
|
r7070 | |||
"Stephane"
|
r46047 | # display count of lines changed in a time range | ||
hg churn -d "2020-04 to 2020-09" | ||||
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 | |||
Alexander Solovyov
|
r11264 | <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. | ||||
Matthew Turk
|
r19464 | Aliases will be split from the rightmost "=". | ||
Martin Geisler
|
r8254 | ''' | ||
Augie Fackler
|
r43346 | |||
Patrick Mezard
|
r6348 | def pad(s, l): | ||
Augie Fackler
|
r43347 | return s + b" " * (l - encoding.colwidth(s)) | ||
Patrick Mezard
|
r6348 | |||
amap = {} | ||||
Augie Fackler
|
r43906 | aliases = opts.get('aliases') | ||
Augie Fackler
|
r43347 | if not aliases and os.path.exists(repo.wjoin(b'.hgchurn')): | ||
aliases = repo.wjoin(b'.hgchurn') | ||||
Patrick Mezard
|
r6348 | if aliases: | ||
Augie Fackler
|
r43347 | for l in open(aliases, b"rb"): | ||
Martin Geisler
|
r12069 | try: | ||
Augie Fackler
|
r43347 | alias, actual = l.rsplit(b'=' in l and b'=' or None, 1) | ||
Martin Geisler
|
r12069 | amap[alias.strip()] = actual.strip() | ||
except ValueError: | ||||
l = l.strip() | ||||
if l: | ||||
Augie Fackler
|
r43347 | ui.warn(_(b"skipping malformed alias: %s\n") % l) | ||
Ronny Pfannschmidt
|
r12068 | continue | ||
Patrick Mezard
|
r6348 | |||
Pulkit Goyal
|
r36410 | rate = list(countrate(ui, repo, amap, *pats, **opts).items()) | ||
Alexander Solovyov
|
r7065 | if not rate: | ||
Patrick Mezard
|
r6348 | return | ||
Augie Fackler
|
r43906 | if opts.get('sort'): | ||
Mads Kiilerich
|
r18369 | rate.sort() | ||
else: | ||||
rate.sort(key=lambda x: (-sum(x[1]), x)) | ||||
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 | |||
Augie Fackler
|
r12689 | ttywidth = ui.termwidth() | ||
Augie Fackler
|
r43347 | ui.debug(b"assuming %i character terminal\n" % ttywidth) | ||
Alexander Solovyov
|
r9669 | width = ttywidth - maxname - 2 - 2 - 2 | ||
Alexander Solovyov
|
r7065 | |||
Augie Fackler
|
r43906 | if opts.get('diffstat'): | ||
Alexander Solovyov
|
r9669 | width -= 15 | ||
Augie Fackler
|
r43346 | |||
Renato Cunha
|
r11501 | def format(name, diffstat): | ||
added, removed = diffstat | ||||
Augie Fackler
|
r43347 | return b"%s %15s %s%s\n" % ( | ||
Augie Fackler
|
r43346 | pad(name, maxname), | ||
Augie Fackler
|
r43347 | b'+%d/-%d' % (added, removed), | ||
ui.label(b'+' * charnum(added), b'diffstat.inserted'), | ||||
ui.label(b'-' * charnum(removed), b'diffstat.deleted'), | ||||
Augie Fackler
|
r43346 | ) | ||
Alexander Solovyov
|
r9669 | else: | ||
width -= 6 | ||||
Augie Fackler
|
r43346 | |||
Alexander Solovyov
|
r9669 | def format(name, count): | ||
Augie Fackler
|
r43347 | return b"%s %6d %s\n" % ( | ||
Augie Fackler
|
r43346 | pad(name, maxname), | ||
sum(count), | ||||
Augie Fackler
|
r43347 | b'*' * charnum(sum(count)), | ||
Augie Fackler
|
r43346 | ) | ||
Alexander Solovyov
|
r9669 | |||
def charnum(count): | ||||
Augie Fackler
|
r40300 | return int(count * width // maxcount) | ||
Alexander Solovyov
|
r9669 | |||
for name, count in rate: | ||||
Steve Borho
|
r11310 | ui.write(format(name, count)) | ||