hgext/churn.py
changeset 43077 687b865b95ad
parent 43076 2372284d9457
child 43085 eef9a2d67051
equal deleted inserted replaced
43076:2372284d9457 43077:687b865b95ad
    30 command = registrar.command(cmdtable)
    30 command = registrar.command(cmdtable)
    31 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
    31 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
    32 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
    32 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
    33 # be specifying the version(s) of Mercurial they are tested with, or
    33 # be specifying the version(s) of Mercurial they are tested with, or
    34 # leave the attribute unspecified.
    34 # leave the attribute unspecified.
    35 testedwith = 'ships-with-hg-core'
    35 testedwith = b'ships-with-hg-core'
    36 
    36 
    37 
    37 
    38 def changedlines(ui, repo, ctx1, ctx2, fns):
    38 def changedlines(ui, repo, ctx1, ctx2, fns):
    39     added, removed = 0, 0
    39     added, removed = 0, 0
    40     fmatch = scmutil.matchfiles(repo, fns)
    40     fmatch = scmutil.matchfiles(repo, fns)
    41     diff = ''.join(patch.diff(repo, ctx1.node(), ctx2.node(), fmatch))
    41     diff = b''.join(patch.diff(repo, ctx1.node(), ctx2.node(), fmatch))
    42     for l in diff.split('\n'):
    42     for l in diff.split(b'\n'):
    43         if l.startswith("+") and not l.startswith("+++ "):
    43         if l.startswith(b"+") and not l.startswith(b"+++ "):
    44             added += 1
    44             added += 1
    45         elif l.startswith("-") and not l.startswith("--- "):
    45         elif l.startswith(b"-") and not l.startswith(b"--- "):
    46             removed += 1
    46             removed += 1
    47     return (added, removed)
    47     return (added, removed)
    48 
    48 
    49 
    49 
    50 def countrate(ui, repo, amap, *pats, **opts):
    50 def countrate(ui, repo, amap, *pats, **opts):
    51     """Calculate stats"""
    51     """Calculate stats"""
    52     opts = pycompat.byteskwargs(opts)
    52     opts = pycompat.byteskwargs(opts)
    53     if opts.get('dateformat'):
    53     if opts.get(b'dateformat'):
    54 
    54 
    55         def getkey(ctx):
    55         def getkey(ctx):
    56             t, tz = ctx.date()
    56             t, tz = ctx.date()
    57             date = datetime.datetime(*time.gmtime(float(t) - tz)[:6])
    57             date = datetime.datetime(*time.gmtime(float(t) - tz)[:6])
    58             return encoding.strtolocal(
    58             return encoding.strtolocal(
    59                 date.strftime(encoding.strfromlocal(opts['dateformat']))
    59                 date.strftime(encoding.strfromlocal(opts[b'dateformat']))
    60             )
    60             )
    61 
    61 
    62     else:
    62     else:
    63         tmpl = opts.get('oldtemplate') or opts.get('template')
    63         tmpl = opts.get(b'oldtemplate') or opts.get(b'template')
    64         tmpl = logcmdutil.maketemplater(ui, repo, tmpl)
    64         tmpl = logcmdutil.maketemplater(ui, repo, tmpl)
    65 
    65 
    66         def getkey(ctx):
    66         def getkey(ctx):
    67             ui.pushbuffer()
    67             ui.pushbuffer()
    68             tmpl.show(ctx)
    68             tmpl.show(ctx)
    69             return ui.popbuffer()
    69             return ui.popbuffer()
    70 
    70 
    71     progress = ui.makeprogress(
    71     progress = ui.makeprogress(
    72         _('analyzing'), unit=_('revisions'), total=len(repo)
    72         _(b'analyzing'), unit=_(b'revisions'), total=len(repo)
    73     )
    73     )
    74     rate = {}
    74     rate = {}
    75     df = False
    75     df = False
    76     if opts.get('date'):
    76     if opts.get(b'date'):
    77         df = dateutil.matchdate(opts['date'])
    77         df = dateutil.matchdate(opts[b'date'])
    78 
    78 
    79     m = scmutil.match(repo[None], pats, opts)
    79     m = scmutil.match(repo[None], pats, opts)
    80 
    80 
    81     def prep(ctx, fns):
    81     def prep(ctx, fns):
    82         rev = ctx.rev()
    82         rev = ctx.rev()
    83         if df and not df(ctx.date()[0]):  # doesn't match date format
    83         if df and not df(ctx.date()[0]):  # doesn't match date format
    84             return
    84             return
    85 
    85 
    86         key = getkey(ctx).strip()
    86         key = getkey(ctx).strip()
    87         key = amap.get(key, key)  # alias remap
    87         key = amap.get(key, key)  # alias remap
    88         if opts.get('changesets'):
    88         if opts.get(b'changesets'):
    89             rate[key] = (rate.get(key, (0,))[0] + 1, 0)
    89             rate[key] = (rate.get(key, (0,))[0] + 1, 0)
    90         else:
    90         else:
    91             parents = ctx.parents()
    91             parents = ctx.parents()
    92             if len(parents) > 1:
    92             if len(parents) > 1:
    93                 ui.note(_('revision %d is a merge, ignoring...\n') % (rev,))
    93                 ui.note(_(b'revision %d is a merge, ignoring...\n') % (rev,))
    94                 return
    94                 return
    95 
    95 
    96             ctx1 = parents[0]
    96             ctx1 = parents[0]
    97             lines = changedlines(ui, repo, ctx1, ctx, fns)
    97             lines = changedlines(ui, repo, ctx1, ctx, fns)
    98             rate[key] = [r + l for r, l in zip(rate.get(key, (0, 0)), lines)]
    98             rate[key] = [r + l for r, l in zip(rate.get(key, (0, 0)), lines)]
   106 
   106 
   107     return rate
   107     return rate
   108 
   108 
   109 
   109 
   110 @command(
   110 @command(
   111     'churn',
   111     b'churn',
   112     [
   112     [
   113         (
   113         (
   114             'r',
   114             b'r',
   115             'rev',
   115             b'rev',
   116             [],
   116             [],
   117             _('count rate for the specified revision or revset'),
   117             _(b'count rate for the specified revision or revset'),
   118             _('REV'),
   118             _(b'REV'),
   119         ),
   119         ),
   120         (
   120         (
   121             'd',
   121             b'd',
   122             'date',
   122             b'date',
   123             '',
   123             b'',
   124             _('count rate for revisions matching date spec'),
   124             _(b'count rate for revisions matching date spec'),
   125             _('DATE'),
   125             _(b'DATE'),
   126         ),
   126         ),
   127         (
   127         (
   128             't',
   128             b't',
   129             'oldtemplate',
   129             b'oldtemplate',
   130             '',
   130             b'',
   131             _('template to group changesets (DEPRECATED)'),
   131             _(b'template to group changesets (DEPRECATED)'),
   132             _('TEMPLATE'),
   132             _(b'TEMPLATE'),
   133         ),
   133         ),
   134         (
   134         (
   135             'T',
   135             b'T',
   136             'template',
   136             b'template',
   137             '{author|email}',
   137             b'{author|email}',
   138             _('template to group changesets'),
   138             _(b'template to group changesets'),
   139             _('TEMPLATE'),
   139             _(b'TEMPLATE'),
   140         ),
   140         ),
   141         (
   141         (
   142             'f',
   142             b'f',
   143             'dateformat',
   143             b'dateformat',
   144             '',
   144             b'',
   145             _('strftime-compatible format for grouping by date'),
   145             _(b'strftime-compatible format for grouping by date'),
   146             _('FORMAT'),
   146             _(b'FORMAT'),
   147         ),
   147         ),
   148         ('c', 'changesets', False, _('count rate by number of changesets')),
   148         (b'c', b'changesets', False, _(b'count rate by number of changesets')),
   149         ('s', 'sort', False, _('sort by key (default: sort by count)')),
   149         (b's', b'sort', False, _(b'sort by key (default: sort by count)')),
   150         ('', 'diffstat', False, _('display added/removed lines separately')),
   150         (b'', b'diffstat', False, _(b'display added/removed lines separately')),
   151         ('', 'aliases', '', _('file with email aliases'), _('FILE')),
   151         (b'', b'aliases', b'', _(b'file with email aliases'), _(b'FILE')),
   152     ]
   152     ]
   153     + cmdutil.walkopts,
   153     + cmdutil.walkopts,
   154     _("hg churn [-d DATE] [-r REV] [--aliases FILE] [FILE]"),
   154     _(b"hg churn [-d DATE] [-r REV] [--aliases FILE] [FILE]"),
   155     helpcategory=command.CATEGORY_MAINTENANCE,
   155     helpcategory=command.CATEGORY_MAINTENANCE,
   156     inferrepo=True,
   156     inferrepo=True,
   157 )
   157 )
   158 def churn(ui, repo, *pats, **opts):
   158 def churn(ui, repo, *pats, **opts):
   159     '''histogram of changes to the repository
   159     '''histogram of changes to the repository
   191     a .hgchurn file will be looked for in the working directory root.
   191     a .hgchurn file will be looked for in the working directory root.
   192     Aliases will be split from the rightmost "=".
   192     Aliases will be split from the rightmost "=".
   193     '''
   193     '''
   194 
   194 
   195     def pad(s, l):
   195     def pad(s, l):
   196         return s + " " * (l - encoding.colwidth(s))
   196         return s + b" " * (l - encoding.colwidth(s))
   197 
   197 
   198     amap = {}
   198     amap = {}
   199     aliases = opts.get(r'aliases')
   199     aliases = opts.get(r'aliases')
   200     if not aliases and os.path.exists(repo.wjoin('.hgchurn')):
   200     if not aliases and os.path.exists(repo.wjoin(b'.hgchurn')):
   201         aliases = repo.wjoin('.hgchurn')
   201         aliases = repo.wjoin(b'.hgchurn')
   202     if aliases:
   202     if aliases:
   203         for l in open(aliases, "rb"):
   203         for l in open(aliases, b"rb"):
   204             try:
   204             try:
   205                 alias, actual = l.rsplit('=' in l and '=' or None, 1)
   205                 alias, actual = l.rsplit(b'=' in l and b'=' or None, 1)
   206                 amap[alias.strip()] = actual.strip()
   206                 amap[alias.strip()] = actual.strip()
   207             except ValueError:
   207             except ValueError:
   208                 l = l.strip()
   208                 l = l.strip()
   209                 if l:
   209                 if l:
   210                     ui.warn(_("skipping malformed alias: %s\n") % l)
   210                     ui.warn(_(b"skipping malformed alias: %s\n") % l)
   211                 continue
   211                 continue
   212 
   212 
   213     rate = list(countrate(ui, repo, amap, *pats, **opts).items())
   213     rate = list(countrate(ui, repo, amap, *pats, **opts).items())
   214     if not rate:
   214     if not rate:
   215         return
   215         return
   222     # Be careful not to have a zero maxcount (issue833)
   222     # Be careful not to have a zero maxcount (issue833)
   223     maxcount = float(max(sum(v) for k, v in rate)) or 1.0
   223     maxcount = float(max(sum(v) for k, v in rate)) or 1.0
   224     maxname = max(len(k) for k, v in rate)
   224     maxname = max(len(k) for k, v in rate)
   225 
   225 
   226     ttywidth = ui.termwidth()
   226     ttywidth = ui.termwidth()
   227     ui.debug("assuming %i character terminal\n" % ttywidth)
   227     ui.debug(b"assuming %i character terminal\n" % ttywidth)
   228     width = ttywidth - maxname - 2 - 2 - 2
   228     width = ttywidth - maxname - 2 - 2 - 2
   229 
   229 
   230     if opts.get(r'diffstat'):
   230     if opts.get(r'diffstat'):
   231         width -= 15
   231         width -= 15
   232 
   232 
   233         def format(name, diffstat):
   233         def format(name, diffstat):
   234             added, removed = diffstat
   234             added, removed = diffstat
   235             return "%s %15s %s%s\n" % (
   235             return b"%s %15s %s%s\n" % (
   236                 pad(name, maxname),
   236                 pad(name, maxname),
   237                 '+%d/-%d' % (added, removed),
   237                 b'+%d/-%d' % (added, removed),
   238                 ui.label('+' * charnum(added), 'diffstat.inserted'),
   238                 ui.label(b'+' * charnum(added), b'diffstat.inserted'),
   239                 ui.label('-' * charnum(removed), 'diffstat.deleted'),
   239                 ui.label(b'-' * charnum(removed), b'diffstat.deleted'),
   240             )
   240             )
   241 
   241 
   242     else:
   242     else:
   243         width -= 6
   243         width -= 6
   244 
   244 
   245         def format(name, count):
   245         def format(name, count):
   246             return "%s %6d %s\n" % (
   246             return b"%s %6d %s\n" % (
   247                 pad(name, maxname),
   247                 pad(name, maxname),
   248                 sum(count),
   248                 sum(count),
   249                 '*' * charnum(sum(count)),
   249                 b'*' * charnum(sum(count)),
   250             )
   250             )
   251 
   251 
   252     def charnum(count):
   252     def charnum(count):
   253         return int(count * width // maxcount)
   253         return int(count * width // maxcount)
   254 
   254