# churn.py - create a graph of revisions count grouped by template # # Copyright 2006 Josef "Jeff" Sipek # Copyright 2008 Alexander Solovyov # # This software may be used and distributed according to the terms of the # GNU General Public License version 2 or any later version. '''command to display statistics about repository history''' from __future__ import absolute_import, division import datetime import os import time from mercurial.i18n import _ from mercurial.pycompat import open from mercurial import ( cmdutil, encoding, logcmdutil, patch, pycompat, registrar, scmutil, ) from mercurial.utils import dateutil cmdtable = {} command = registrar.command(cmdtable) # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for # 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. testedwith = b'ships-with-hg-core' def changedlines(ui, repo, ctx1, ctx2, fns): added, removed = 0, 0 fmatch = scmutil.matchfiles(repo, fns) 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"+++ "): added += 1 elif l.startswith(b"-") and not l.startswith(b"--- "): removed += 1 return (added, removed) def countrate(ui, repo, amap, *pats, **opts): """Calculate stats""" opts = pycompat.byteskwargs(opts) if opts.get(b'dateformat'): def getkey(ctx): t, tz = ctx.date() date = datetime.datetime(*time.gmtime(float(t) - tz)[:6]) return encoding.strtolocal( date.strftime(encoding.strfromlocal(opts[b'dateformat'])) ) else: tmpl = opts.get(b'oldtemplate') or opts.get(b'template') tmpl = logcmdutil.maketemplater(ui, repo, tmpl) def getkey(ctx): ui.pushbuffer() tmpl.show(ctx) return ui.popbuffer() progress = ui.makeprogress( _(b'analyzing'), unit=_(b'revisions'), total=len(repo) ) rate = {} df = False if opts.get(b'date'): df = dateutil.matchdate(opts[b'date']) m = scmutil.match(repo[None], pats, opts) def prep(ctx, fns): rev = ctx.rev() if df and not df(ctx.date()[0]): # doesn't match date format return key = getkey(ctx).strip() key = amap.get(key, key) # alias remap if opts.get(b'changesets'): rate[key] = (rate.get(key, (0,))[0] + 1, 0) else: parents = ctx.parents() if len(parents) > 1: ui.note(_(b'revision %d is a merge, ignoring...\n') % (rev,)) return ctx1 = parents[0] lines = changedlines(ui, repo, ctx1, ctx, fns) rate[key] = [r + l for r, l in zip(rate.get(key, (0, 0)), lines)] progress.increment() for ctx in cmdutil.walkchangerevs(repo, m, opts, prep): continue progress.complete() return rate @command( b'churn', [ ( b'r', b'rev', [], _(b'count rate for the specified revision or revset'), _(b'REV'), ), ( b'd', b'date', b'', _(b'count rate for revisions matching date spec'), _(b'DATE'), ), ( b't', b'oldtemplate', b'', _(b'template to group changesets (DEPRECATED)'), _(b'TEMPLATE'), ), ( b'T', b'template', b'{author|email}', _(b'template to group changesets'), _(b'TEMPLATE'), ), ( b'f', b'dateformat', b'', _(b'strftime-compatible format for grouping by date'), _(b'FORMAT'), ), (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')), ] + cmdutil.walkopts, _(b"hg churn [-d DATE] [-r REV] [--aliases FILE] [FILE]"), helpcategory=command.CATEGORY_MAINTENANCE, inferrepo=True, ) def churn(ui, repo, *pats, **opts): '''histogram of changes to the repository 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. Statistics are based on the number of changed lines, or alternatively the number of matching revisions if the --changesets option is specified. Examples:: # display count of changed lines for every committer hg churn -T "{author|email}" # display daily activity graph hg churn -f "%H" -s -c # display activity of developers by month hg churn -f "%Y-%m" -s -c # display count of lines changed in every year hg churn -f "%Y" -s # display count of lines changed in a time range hg churn -d "2020-04 to 2020-09" It is possible to map alternate email addresses to a main address by providing a file using the following format:: = Such a file may be specified with the --aliases option, otherwise a .hgchurn file will be looked for in the working directory root. Aliases will be split from the rightmost "=". ''' def pad(s, l): return s + b" " * (l - encoding.colwidth(s)) amap = {} aliases = opts.get('aliases') if not aliases and os.path.exists(repo.wjoin(b'.hgchurn')): aliases = repo.wjoin(b'.hgchurn') if aliases: for l in open(aliases, b"rb"): try: alias, actual = l.rsplit(b'=' in l and b'=' or None, 1) amap[alias.strip()] = actual.strip() except ValueError: l = l.strip() if l: ui.warn(_(b"skipping malformed alias: %s\n") % l) continue rate = list(countrate(ui, repo, amap, *pats, **opts).items()) if not rate: return if opts.get('sort'): rate.sort() else: rate.sort(key=lambda x: (-sum(x[1]), x)) # Be careful not to have a zero maxcount (issue833) maxcount = float(max(sum(v) for k, v in rate)) or 1.0 maxname = max(len(k) for k, v in rate) ttywidth = ui.termwidth() ui.debug(b"assuming %i character terminal\n" % ttywidth) width = ttywidth - maxname - 2 - 2 - 2 if opts.get('diffstat'): width -= 15 def format(name, diffstat): added, removed = diffstat return b"%s %15s %s%s\n" % ( pad(name, maxname), b'+%d/-%d' % (added, removed), ui.label(b'+' * charnum(added), b'diffstat.inserted'), ui.label(b'-' * charnum(removed), b'diffstat.deleted'), ) else: width -= 6 def format(name, count): return b"%s %6d %s\n" % ( pad(name, maxname), sum(count), b'*' * charnum(sum(count)), ) def charnum(count): return int(count * width // maxcount) for name, count in rate: ui.write(format(name, count))