##// END OF EJS Templates
py3: make sure we pass str to date.strftime()...
Pulkit Goyal -
r38077:543b7b34 default
parent child Browse files
Show More
@@ -1,211 +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(opts['dateformat'])
55 return date.strftime(pycompat.sysstr(opts['dateformat']))
56 else:
56 else:
57 tmpl = opts.get('oldtemplate') or opts.get('template')
57 tmpl = opts.get('oldtemplate') or opts.get('template')
58 tmpl = logcmdutil.maketemplater(ui, repo, tmpl)
58 tmpl = logcmdutil.maketemplater(ui, repo, tmpl)
59 def getkey(ctx):
59 def getkey(ctx):
60 ui.pushbuffer()
60 ui.pushbuffer()
61 tmpl.show(ctx)
61 tmpl.show(ctx)
62 return ui.popbuffer()
62 return ui.popbuffer()
63
63
64 state = {'count': 0}
64 state = {'count': 0}
65 rate = {}
65 rate = {}
66 df = False
66 df = False
67 if opts.get('date'):
67 if opts.get('date'):
68 df = dateutil.matchdate(opts['date'])
68 df = dateutil.matchdate(opts['date'])
69
69
70 m = scmutil.match(repo[None], pats, opts)
70 m = scmutil.match(repo[None], pats, opts)
71 def prep(ctx, fns):
71 def prep(ctx, fns):
72 rev = ctx.rev()
72 rev = ctx.rev()
73 if df and not df(ctx.date()[0]): # doesn't match date format
73 if df and not df(ctx.date()[0]): # doesn't match date format
74 return
74 return
75
75
76 key = getkey(ctx).strip()
76 key = getkey(ctx).strip()
77 key = amap.get(key, key) # alias remap
77 key = amap.get(key, key) # alias remap
78 if opts.get('changesets'):
78 if opts.get('changesets'):
79 rate[key] = (rate.get(key, (0,))[0] + 1, 0)
79 rate[key] = (rate.get(key, (0,))[0] + 1, 0)
80 else:
80 else:
81 parents = ctx.parents()
81 parents = ctx.parents()
82 if len(parents) > 1:
82 if len(parents) > 1:
83 ui.note(_('revision %d is a merge, ignoring...\n') % (rev,))
83 ui.note(_('revision %d is a merge, ignoring...\n') % (rev,))
84 return
84 return
85
85
86 ctx1 = parents[0]
86 ctx1 = parents[0]
87 lines = changedlines(ui, repo, ctx1, ctx, fns)
87 lines = changedlines(ui, repo, ctx1, ctx, fns)
88 rate[key] = [r + l for r, l in zip(rate.get(key, (0, 0)), lines)]
88 rate[key] = [r + l for r, l in zip(rate.get(key, (0, 0)), lines)]
89
89
90 state['count'] += 1
90 state['count'] += 1
91 ui.progress(_('analyzing'), state['count'], total=len(repo),
91 ui.progress(_('analyzing'), state['count'], total=len(repo),
92 unit=_('revisions'))
92 unit=_('revisions'))
93
93
94 for ctx in cmdutil.walkchangerevs(repo, m, opts, prep):
94 for ctx in cmdutil.walkchangerevs(repo, m, opts, prep):
95 continue
95 continue
96
96
97 ui.progress(_('analyzing'), None)
97 ui.progress(_('analyzing'), None)
98
98
99 return rate
99 return rate
100
100
101
101
102 @command('churn',
102 @command('churn',
103 [('r', 'rev', [],
103 [('r', 'rev', [],
104 _('count rate for the specified revision or revset'), _('REV')),
104 _('count rate for the specified revision or revset'), _('REV')),
105 ('d', 'date', '',
105 ('d', 'date', '',
106 _('count rate for revisions matching date spec'), _('DATE')),
106 _('count rate for revisions matching date spec'), _('DATE')),
107 ('t', 'oldtemplate', '',
107 ('t', 'oldtemplate', '',
108 _('template to group changesets (DEPRECATED)'), _('TEMPLATE')),
108 _('template to group changesets (DEPRECATED)'), _('TEMPLATE')),
109 ('T', 'template', '{author|email}',
109 ('T', 'template', '{author|email}',
110 _('template to group changesets'), _('TEMPLATE')),
110 _('template to group changesets'), _('TEMPLATE')),
111 ('f', 'dateformat', '',
111 ('f', 'dateformat', '',
112 _('strftime-compatible format for grouping by date'), _('FORMAT')),
112 _('strftime-compatible format for grouping by date'), _('FORMAT')),
113 ('c', 'changesets', False, _('count rate by number of changesets')),
113 ('c', 'changesets', False, _('count rate by number of changesets')),
114 ('s', 'sort', False, _('sort by key (default: sort by count)')),
114 ('s', 'sort', False, _('sort by key (default: sort by count)')),
115 ('', 'diffstat', False, _('display added/removed lines separately')),
115 ('', 'diffstat', False, _('display added/removed lines separately')),
116 ('', 'aliases', '', _('file with email aliases'), _('FILE')),
116 ('', 'aliases', '', _('file with email aliases'), _('FILE')),
117 ] + cmdutil.walkopts,
117 ] + cmdutil.walkopts,
118 _("hg churn [-d DATE] [-r REV] [--aliases FILE] [FILE]"),
118 _("hg churn [-d DATE] [-r REV] [--aliases FILE] [FILE]"),
119 inferrepo=True)
119 inferrepo=True)
120 def churn(ui, repo, *pats, **opts):
120 def churn(ui, repo, *pats, **opts):
121 '''histogram of changes to the repository
121 '''histogram of changes to the repository
122
122
123 This command will display a histogram representing the number
123 This command will display a histogram representing the number
124 of changed lines or revisions, grouped according to the given
124 of changed lines or revisions, grouped according to the given
125 template. The default template will group changes by author.
125 template. The default template will group changes by author.
126 The --dateformat option may be used to group the results by
126 The --dateformat option may be used to group the results by
127 date instead.
127 date instead.
128
128
129 Statistics are based on the number of changed lines, or
129 Statistics are based on the number of changed lines, or
130 alternatively the number of matching revisions if the
130 alternatively the number of matching revisions if the
131 --changesets option is specified.
131 --changesets option is specified.
132
132
133 Examples::
133 Examples::
134
134
135 # display count of changed lines for every committer
135 # display count of changed lines for every committer
136 hg churn -T "{author|email}"
136 hg churn -T "{author|email}"
137
137
138 # display daily activity graph
138 # display daily activity graph
139 hg churn -f "%H" -s -c
139 hg churn -f "%H" -s -c
140
140
141 # display activity of developers by month
141 # display activity of developers by month
142 hg churn -f "%Y-%m" -s -c
142 hg churn -f "%Y-%m" -s -c
143
143
144 # display count of lines changed in every year
144 # display count of lines changed in every year
145 hg churn -f "%Y" -s
145 hg churn -f "%Y" -s
146
146
147 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
148 by providing a file using the following format::
148 by providing a file using the following format::
149
149
150 <alias email> = <actual email>
150 <alias email> = <actual email>
151
151
152 Such a file may be specified with the --aliases option, otherwise
152 Such a file may be specified with the --aliases option, otherwise
153 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.
154 Aliases will be split from the rightmost "=".
154 Aliases will be split from the rightmost "=".
155 '''
155 '''
156 def pad(s, l):
156 def pad(s, l):
157 return s + " " * (l - encoding.colwidth(s))
157 return s + " " * (l - encoding.colwidth(s))
158
158
159 amap = {}
159 amap = {}
160 aliases = opts.get(r'aliases')
160 aliases = opts.get(r'aliases')
161 if not aliases and os.path.exists(repo.wjoin('.hgchurn')):
161 if not aliases and os.path.exists(repo.wjoin('.hgchurn')):
162 aliases = repo.wjoin('.hgchurn')
162 aliases = repo.wjoin('.hgchurn')
163 if aliases:
163 if aliases:
164 for l in open(aliases, "r"):
164 for l in open(aliases, "r"):
165 try:
165 try:
166 alias, actual = l.rsplit('=' in l and '=' or None, 1)
166 alias, actual = l.rsplit('=' in l and '=' or None, 1)
167 amap[alias.strip()] = actual.strip()
167 amap[alias.strip()] = actual.strip()
168 except ValueError:
168 except ValueError:
169 l = l.strip()
169 l = l.strip()
170 if l:
170 if l:
171 ui.warn(_("skipping malformed alias: %s\n") % l)
171 ui.warn(_("skipping malformed alias: %s\n") % l)
172 continue
172 continue
173
173
174 rate = list(countrate(ui, repo, amap, *pats, **opts).items())
174 rate = list(countrate(ui, repo, amap, *pats, **opts).items())
175 if not rate:
175 if not rate:
176 return
176 return
177
177
178 if opts.get(r'sort'):
178 if opts.get(r'sort'):
179 rate.sort()
179 rate.sort()
180 else:
180 else:
181 rate.sort(key=lambda x: (-sum(x[1]), x))
181 rate.sort(key=lambda x: (-sum(x[1]), x))
182
182
183 # Be careful not to have a zero maxcount (issue833)
183 # Be careful not to have a zero maxcount (issue833)
184 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
185 maxname = max(len(k) for k, v in rate)
185 maxname = max(len(k) for k, v in rate)
186
186
187 ttywidth = ui.termwidth()
187 ttywidth = ui.termwidth()
188 ui.debug("assuming %i character terminal\n" % ttywidth)
188 ui.debug("assuming %i character terminal\n" % ttywidth)
189 width = ttywidth - maxname - 2 - 2 - 2
189 width = ttywidth - maxname - 2 - 2 - 2
190
190
191 if opts.get(r'diffstat'):
191 if opts.get(r'diffstat'):
192 width -= 15
192 width -= 15
193 def format(name, diffstat):
193 def format(name, diffstat):
194 added, removed = diffstat
194 added, removed = diffstat
195 return "%s %15s %s%s\n" % (pad(name, maxname),
195 return "%s %15s %s%s\n" % (pad(name, maxname),
196 '+%d/-%d' % (added, removed),
196 '+%d/-%d' % (added, removed),
197 ui.label('+' * charnum(added),
197 ui.label('+' * charnum(added),
198 'diffstat.inserted'),
198 'diffstat.inserted'),
199 ui.label('-' * charnum(removed),
199 ui.label('-' * charnum(removed),
200 'diffstat.deleted'))
200 'diffstat.deleted'))
201 else:
201 else:
202 width -= 6
202 width -= 6
203 def format(name, count):
203 def format(name, count):
204 return "%s %6d %s\n" % (pad(name, maxname), sum(count),
204 return "%s %6d %s\n" % (pad(name, maxname), sum(count),
205 '*' * charnum(sum(count)))
205 '*' * charnum(sum(count)))
206
206
207 def charnum(count):
207 def charnum(count):
208 return int(round(count * width / maxcount))
208 return int(round(count * width / maxcount))
209
209
210 for name, count in rate:
210 for name, count in rate:
211 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