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