##// END OF EJS Templates
churn: fix stack traces on Python 3...
Augie Fackler -
r40276:6f3e7338 default
parent child Browse files
Show More
@@ -1,210 +1,211 b''
1 1 # churn.py - create a graph of revisions count grouped by template
2 2 #
3 3 # Copyright 2006 Josef "Jeff" Sipek <jeffpc@josefsipek.net>
4 4 # Copyright 2008 Alexander Solovyov <piranha@piranha.org.ua>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 '''command to display statistics about repository history'''
10 10
11 11 from __future__ import absolute_import
12 12
13 13 import datetime
14 14 import os
15 15 import time
16 16
17 17 from mercurial.i18n import _
18 18 from mercurial import (
19 19 cmdutil,
20 20 encoding,
21 21 logcmdutil,
22 22 patch,
23 23 pycompat,
24 24 registrar,
25 25 scmutil,
26 26 )
27 27 from mercurial.utils import dateutil
28 28
29 29 cmdtable = {}
30 30 command = registrar.command(cmdtable)
31 31 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
32 32 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
33 33 # be specifying the version(s) of Mercurial they are tested with, or
34 34 # leave the attribute unspecified.
35 35 testedwith = 'ships-with-hg-core'
36 36
37 37 def changedlines(ui, repo, ctx1, ctx2, fns):
38 38 added, removed = 0, 0
39 39 fmatch = scmutil.matchfiles(repo, fns)
40 40 diff = ''.join(patch.diff(repo, ctx1.node(), ctx2.node(), fmatch))
41 41 for l in diff.split('\n'):
42 42 if l.startswith("+") and not l.startswith("+++ "):
43 43 added += 1
44 44 elif l.startswith("-") and not l.startswith("--- "):
45 45 removed += 1
46 46 return (added, removed)
47 47
48 48 def countrate(ui, repo, amap, *pats, **opts):
49 49 """Calculate stats"""
50 50 opts = pycompat.byteskwargs(opts)
51 51 if opts.get('dateformat'):
52 52 def getkey(ctx):
53 53 t, tz = ctx.date()
54 54 date = datetime.datetime(*time.gmtime(float(t) - tz)[:6])
55 return date.strftime(encoding.strfromlocal(opts['dateformat']))
55 return encoding.strtolocal(
56 date.strftime(encoding.strfromlocal(opts['dateformat'])))
56 57 else:
57 58 tmpl = opts.get('oldtemplate') or opts.get('template')
58 59 tmpl = logcmdutil.maketemplater(ui, repo, tmpl)
59 60 def getkey(ctx):
60 61 ui.pushbuffer()
61 62 tmpl.show(ctx)
62 63 return ui.popbuffer()
63 64
64 65 progress = ui.makeprogress(_('analyzing'), unit=_('revisions'),
65 66 total=len(repo))
66 67 rate = {}
67 68 df = False
68 69 if opts.get('date'):
69 70 df = dateutil.matchdate(opts['date'])
70 71
71 72 m = scmutil.match(repo[None], pats, opts)
72 73 def prep(ctx, fns):
73 74 rev = ctx.rev()
74 75 if df and not df(ctx.date()[0]): # doesn't match date format
75 76 return
76 77
77 78 key = getkey(ctx).strip()
78 79 key = amap.get(key, key) # alias remap
79 80 if opts.get('changesets'):
80 81 rate[key] = (rate.get(key, (0,))[0] + 1, 0)
81 82 else:
82 83 parents = ctx.parents()
83 84 if len(parents) > 1:
84 85 ui.note(_('revision %d is a merge, ignoring...\n') % (rev,))
85 86 return
86 87
87 88 ctx1 = parents[0]
88 89 lines = changedlines(ui, repo, ctx1, ctx, fns)
89 90 rate[key] = [r + l for r, l in zip(rate.get(key, (0, 0)), lines)]
90 91
91 92 progress.increment()
92 93
93 94 for ctx in cmdutil.walkchangerevs(repo, m, opts, prep):
94 95 continue
95 96
96 97 progress.complete()
97 98
98 99 return rate
99 100
100 101
101 102 @command('churn',
102 103 [('r', 'rev', [],
103 104 _('count rate for the specified revision or revset'), _('REV')),
104 105 ('d', 'date', '',
105 106 _('count rate for revisions matching date spec'), _('DATE')),
106 107 ('t', 'oldtemplate', '',
107 108 _('template to group changesets (DEPRECATED)'), _('TEMPLATE')),
108 109 ('T', 'template', '{author|email}',
109 110 _('template to group changesets'), _('TEMPLATE')),
110 111 ('f', 'dateformat', '',
111 112 _('strftime-compatible format for grouping by date'), _('FORMAT')),
112 113 ('c', 'changesets', False, _('count rate by number of changesets')),
113 114 ('s', 'sort', False, _('sort by key (default: sort by count)')),
114 115 ('', 'diffstat', False, _('display added/removed lines separately')),
115 116 ('', 'aliases', '', _('file with email aliases'), _('FILE')),
116 117 ] + cmdutil.walkopts,
117 118 _("hg churn [-d DATE] [-r REV] [--aliases FILE] [FILE]"),
118 119 inferrepo=True)
119 120 def churn(ui, repo, *pats, **opts):
120 121 '''histogram of changes to the repository
121 122
122 123 This command will display a histogram representing the number
123 124 of changed lines or revisions, grouped according to the given
124 125 template. The default template will group changes by author.
125 126 The --dateformat option may be used to group the results by
126 127 date instead.
127 128
128 129 Statistics are based on the number of changed lines, or
129 130 alternatively the number of matching revisions if the
130 131 --changesets option is specified.
131 132
132 133 Examples::
133 134
134 135 # display count of changed lines for every committer
135 136 hg churn -T "{author|email}"
136 137
137 138 # display daily activity graph
138 139 hg churn -f "%H" -s -c
139 140
140 141 # display activity of developers by month
141 142 hg churn -f "%Y-%m" -s -c
142 143
143 144 # display count of lines changed in every year
144 145 hg churn -f "%Y" -s
145 146
146 147 It is possible to map alternate email addresses to a main address
147 148 by providing a file using the following format::
148 149
149 150 <alias email> = <actual email>
150 151
151 152 Such a file may be specified with the --aliases option, otherwise
152 153 a .hgchurn file will be looked for in the working directory root.
153 154 Aliases will be split from the rightmost "=".
154 155 '''
155 156 def pad(s, l):
156 157 return s + " " * (l - encoding.colwidth(s))
157 158
158 159 amap = {}
159 160 aliases = opts.get(r'aliases')
160 161 if not aliases and os.path.exists(repo.wjoin('.hgchurn')):
161 162 aliases = repo.wjoin('.hgchurn')
162 163 if aliases:
163 164 for l in open(aliases, "rb"):
164 165 try:
165 166 alias, actual = l.rsplit('=' in l and '=' or None, 1)
166 167 amap[alias.strip()] = actual.strip()
167 168 except ValueError:
168 169 l = l.strip()
169 170 if l:
170 171 ui.warn(_("skipping malformed alias: %s\n") % l)
171 172 continue
172 173
173 174 rate = list(countrate(ui, repo, amap, *pats, **opts).items())
174 175 if not rate:
175 176 return
176 177
177 178 if opts.get(r'sort'):
178 179 rate.sort()
179 180 else:
180 181 rate.sort(key=lambda x: (-sum(x[1]), x))
181 182
182 183 # Be careful not to have a zero maxcount (issue833)
183 184 maxcount = float(max(sum(v) for k, v in rate)) or 1.0
184 185 maxname = max(len(k) for k, v in rate)
185 186
186 187 ttywidth = ui.termwidth()
187 188 ui.debug("assuming %i character terminal\n" % ttywidth)
188 189 width = ttywidth - maxname - 2 - 2 - 2
189 190
190 191 if opts.get(r'diffstat'):
191 192 width -= 15
192 193 def format(name, diffstat):
193 194 added, removed = diffstat
194 195 return "%s %15s %s%s\n" % (pad(name, maxname),
195 196 '+%d/-%d' % (added, removed),
196 197 ui.label('+' * charnum(added),
197 198 'diffstat.inserted'),
198 199 ui.label('-' * charnum(removed),
199 200 'diffstat.deleted'))
200 201 else:
201 202 width -= 6
202 203 def format(name, count):
203 204 return "%s %6d %s\n" % (pad(name, maxname), sum(count),
204 205 '*' * charnum(sum(count)))
205 206
206 207 def charnum(count):
207 208 return int(round(count * width / maxcount))
208 209
209 210 for name, count in rate:
210 211 ui.write(format(name, count))
General Comments 0
You need to be logged in to leave comments. Login now