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