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