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