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