sapling/edenscm/hgext/churn.py
Jun Wu 9dc21f8d0b codemod: import from the edenscm package
Summary:
D13853115 adds `edenscm/` to `sys.path` and code still uses `import mercurial`.
That has nasty problems if both `import mercurial` and
`import edenscm.mercurial` are used, because Python would think `mercurial.foo`
and `edenscm.mercurial.foo` are different modules so code like
`try: ... except mercurial.error.Foo: ...`, or `isinstance(x, mercurial.foo.Bar)`
would fail to handle the `edenscm.mercurial` version. There are also some
module-level states (ex. `extensions._extensions`) that would cause trouble if
they have multiple versions in a single process.

Change imports to use the `edenscm` so ideally the `mercurial` is no longer
imported at all. Add checks in extensions.py to catch unexpected extensions
importing modules from the old (wrong) locations when running tests.

Reviewed By: phillco

Differential Revision: D13868981

fbshipit-source-id: f4e2513766957fd81d85407994f7521a08e4de48
2019-01-29 17:25:32 -08:00

246 lines
7.1 KiB
Python

# churn.py - create a graph of revisions count grouped by template
#
# Copyright 2006 Josef "Jeff" Sipek <jeffpc@josefsipek.net>
# Copyright 2008 Alexander Solovyov <piranha@piranha.org.ua>
#
# 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
import datetime
import os
import time
from edenscm.mercurial import (
cmdutil,
encoding,
patch,
progress,
pycompat,
registrar,
scmutil,
util,
)
from edenscm.mercurial.i18n import _
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 = "ships-with-hg-core"
def changedlines(ui, repo, ctx1, ctx2, fns):
added, removed = 0, 0
fmatch = scmutil.matchfiles(repo, fns)
diff = "".join(patch.diff(repo, ctx1.node(), ctx2.node(), fmatch))
for l in diff.split("\n"):
if l.startswith("+") and not l.startswith("+++ "):
added += 1
elif l.startswith("-") and not l.startswith("--- "):
removed += 1
return (added, removed)
def countrate(ui, repo, amap, *pats, **opts):
"""Calculate stats"""
opts = pycompat.byteskwargs(opts)
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("oldtemplate") or opts.get("template")
tmpl = cmdutil.makelogtemplater(ui, repo, tmpl)
def getkey(ctx):
ui.pushbuffer()
tmpl.show(ctx)
return ui.popbuffer()
rate = {}
df = False
if opts.get("date"):
df = util.matchdate(opts["date"])
prog = progress.bar(ui, _("analyzing"), _("revisions"), len(repo))
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("changesets"):
rate[key] = (rate.get(key, (0,))[0] + 1, 0)
else:
parents = ctx.parents()
if len(parents) > 1:
ui.note(_("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)]
prog.value += 1
with prog:
for ctx in cmdutil.walkchangerevs(repo, m, opts, prep):
continue
return rate
@command(
"churn",
[
(
"r",
"rev",
[],
_("count rate for the specified revision or revset"),
_("REV"),
),
("d", "date", "", _("count rate for revisions matching date spec"), _("DATE")),
(
"t",
"oldtemplate",
"",
_("template to group changesets (DEPRECATED)"),
_("TEMPLATE"),
),
(
"T",
"template",
"{author|email}",
_("template to group changesets"),
_("TEMPLATE"),
),
(
"f",
"dateformat",
"",
_("strftime-compatible format for grouping by date"),
_("FORMAT"),
),
("c", "changesets", False, _("count rate by number of changesets")),
("s", "sort", False, _("sort by key (default: sort by count)")),
("", "diffstat", False, _("display added/removed lines separately")),
("", "aliases", "", _("file with email aliases"), _("FILE")),
]
+ cmdutil.walkopts,
_("hg churn [-d DATE] [-r REV] [--aliases FILE] [FILE]"),
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
It is possible to map alternate email addresses to a main address
by providing a file using the following format::
<alias email> = <actual email>
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 + " " * (l - encoding.colwidth(s))
amap = {}
aliases = opts.get(r"aliases")
if not aliases and os.path.exists(repo.wjoin(".hgchurn")):
aliases = repo.wjoin(".hgchurn")
if aliases:
for l in open(aliases, "r"):
try:
alias, actual = l.rsplit("=" in l and "=" or None, 1)
amap[alias.strip()] = actual.strip()
except ValueError:
l = l.strip()
if l:
ui.warn(_("skipping malformed alias: %s\n") % l)
continue
rate = countrate(ui, repo, amap, *pats, **opts).items()
if not rate:
return
if opts.get(r"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("assuming %i character terminal\n" % ttywidth)
width = ttywidth - maxname - 2 - 2 - 2
if opts.get(r"diffstat"):
width -= 15
def format(name, diffstat):
added, removed = diffstat
return "%s %15s %s%s\n" % (
pad(name, maxname),
"+%d/-%d" % (added, removed),
ui.label("+" * charnum(added), "diffstat.inserted"),
ui.label("-" * charnum(removed), "diffstat.deleted"),
)
else:
width -= 6
def format(name, count):
return "%s %6d %s\n" % (
pad(name, maxname),
sum(count),
"*" * charnum(sum(count)),
)
def charnum(count):
return int(round(count * width / maxcount))
for name, count in rate:
ui.write(format(name, count))