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