##// END OF EJS Templates
churn: count lines that look like diff headers but are not...
Aay Jay Chan -
r47158:b84c3d43 default
parent child Browse files
Show More
@@ -1,259 +1,264 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 scmutil,
27 27 )
28 28
29 29 cmdtable = {}
30 30 command = registrar.command(cmdtable)
31 31 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
32 32 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
33 33 # be specifying the version(s) of Mercurial they are tested with, or
34 34 # leave the attribute unspecified.
35 35 testedwith = b'ships-with-hg-core'
36 36
37 37
38 38 def changedlines(ui, repo, ctx1, ctx2, fmatch):
39 39 added, removed = 0, 0
40 40 diff = b''.join(patch.diff(repo, ctx1.node(), ctx2.node(), fmatch))
41 inhunk = False
41 42 for l in diff.split(b'\n'):
42 if l.startswith(b"+") and not l.startswith(b"+++ "):
43 if inhunk and l.startswith(b"+"):
43 44 added += 1
44 elif l.startswith(b"-") and not l.startswith(b"--- "):
45 elif inhunk and l.startswith(b"-"):
45 46 removed += 1
47 elif l.startswith(b"@"):
48 inhunk = True
49 elif l.startswith(b"d"):
50 inhunk = False
46 51 return (added, removed)
47 52
48 53
49 54 def countrate(ui, repo, amap, *pats, **opts):
50 55 """Calculate stats"""
51 56 opts = pycompat.byteskwargs(opts)
52 57 if opts.get(b'dateformat'):
53 58
54 59 def getkey(ctx):
55 60 t, tz = ctx.date()
56 61 date = datetime.datetime(*time.gmtime(float(t) - tz)[:6])
57 62 return encoding.strtolocal(
58 63 date.strftime(encoding.strfromlocal(opts[b'dateformat']))
59 64 )
60 65
61 66 else:
62 67 tmpl = opts.get(b'oldtemplate') or opts.get(b'template')
63 68 tmpl = logcmdutil.maketemplater(ui, repo, tmpl)
64 69
65 70 def getkey(ctx):
66 71 ui.pushbuffer()
67 72 tmpl.show(ctx)
68 73 return ui.popbuffer()
69 74
70 75 progress = ui.makeprogress(
71 76 _(b'analyzing'), unit=_(b'revisions'), total=len(repo)
72 77 )
73 78 rate = {}
74 79
75 80 def prep(ctx, fmatch):
76 81 rev = ctx.rev()
77 82 key = getkey(ctx).strip()
78 83 key = amap.get(key, key) # alias remap
79 84 if opts.get(b'changesets'):
80 85 rate[key] = (rate.get(key, (0,))[0] + 1, 0)
81 86 else:
82 87 parents = ctx.parents()
83 88 if len(parents) > 1:
84 89 ui.note(_(b'revision %d is a merge, ignoring...\n') % (rev,))
85 90 return
86 91
87 92 ctx1 = parents[0]
88 93 lines = changedlines(ui, repo, ctx1, ctx, fmatch)
89 94 rate[key] = [r + l for r, l in zip(rate.get(key, (0, 0)), lines)]
90 95
91 96 progress.increment()
92 97
93 98 wopts = logcmdutil.walkopts(
94 99 pats=pats,
95 100 opts=opts,
96 101 revspec=opts[b'rev'],
97 102 date=opts[b'date'],
98 103 include_pats=opts[b'include'],
99 104 exclude_pats=opts[b'exclude'],
100 105 )
101 106 revs, makefilematcher = logcmdutil.makewalker(repo, wopts)
102 107 for ctx in scmutil.walkchangerevs(repo, revs, makefilematcher, prep):
103 108 continue
104 109
105 110 progress.complete()
106 111
107 112 return rate
108 113
109 114
110 115 @command(
111 116 b'churn',
112 117 [
113 118 (
114 119 b'r',
115 120 b'rev',
116 121 [],
117 122 _(b'count rate for the specified revision or revset'),
118 123 _(b'REV'),
119 124 ),
120 125 (
121 126 b'd',
122 127 b'date',
123 128 b'',
124 129 _(b'count rate for revisions matching date spec'),
125 130 _(b'DATE'),
126 131 ),
127 132 (
128 133 b't',
129 134 b'oldtemplate',
130 135 b'',
131 136 _(b'template to group changesets (DEPRECATED)'),
132 137 _(b'TEMPLATE'),
133 138 ),
134 139 (
135 140 b'T',
136 141 b'template',
137 142 b'{author|email}',
138 143 _(b'template to group changesets'),
139 144 _(b'TEMPLATE'),
140 145 ),
141 146 (
142 147 b'f',
143 148 b'dateformat',
144 149 b'',
145 150 _(b'strftime-compatible format for grouping by date'),
146 151 _(b'FORMAT'),
147 152 ),
148 153 (b'c', b'changesets', False, _(b'count rate by number of changesets')),
149 154 (b's', b'sort', False, _(b'sort by key (default: sort by count)')),
150 155 (b'', b'diffstat', False, _(b'display added/removed lines separately')),
151 156 (b'', b'aliases', b'', _(b'file with email aliases'), _(b'FILE')),
152 157 ]
153 158 + cmdutil.walkopts,
154 159 _(b"hg churn [-d DATE] [-r REV] [--aliases FILE] [FILE]"),
155 160 helpcategory=command.CATEGORY_MAINTENANCE,
156 161 inferrepo=True,
157 162 )
158 163 def churn(ui, repo, *pats, **opts):
159 164 """histogram of changes to the repository
160 165
161 166 This command will display a histogram representing the number
162 167 of changed lines or revisions, grouped according to the given
163 168 template. The default template will group changes by author.
164 169 The --dateformat option may be used to group the results by
165 170 date instead.
166 171
167 172 Statistics are based on the number of changed lines, or
168 173 alternatively the number of matching revisions if the
169 174 --changesets option is specified.
170 175
171 176 Examples::
172 177
173 178 # display count of changed lines for every committer
174 179 hg churn -T "{author|email}"
175 180
176 181 # display daily activity graph
177 182 hg churn -f "%H" -s -c
178 183
179 184 # display activity of developers by month
180 185 hg churn -f "%Y-%m" -s -c
181 186
182 187 # display count of lines changed in every year
183 188 hg churn -f "%Y" -s
184 189
185 190 # display count of lines changed in a time range
186 191 hg churn -d "2020-04 to 2020-09"
187 192
188 193 It is possible to map alternate email addresses to a main address
189 194 by providing a file using the following format::
190 195
191 196 <alias email> = <actual email>
192 197
193 198 Such a file may be specified with the --aliases option, otherwise
194 199 a .hgchurn file will be looked for in the working directory root.
195 200 Aliases will be split from the rightmost "=".
196 201 """
197 202
198 203 def pad(s, l):
199 204 return s + b" " * (l - encoding.colwidth(s))
200 205
201 206 amap = {}
202 207 aliases = opts.get('aliases')
203 208 if not aliases and os.path.exists(repo.wjoin(b'.hgchurn')):
204 209 aliases = repo.wjoin(b'.hgchurn')
205 210 if aliases:
206 211 for l in open(aliases, b"rb"):
207 212 try:
208 213 alias, actual = l.rsplit(b'=' in l and b'=' or None, 1)
209 214 amap[alias.strip()] = actual.strip()
210 215 except ValueError:
211 216 l = l.strip()
212 217 if l:
213 218 ui.warn(_(b"skipping malformed alias: %s\n") % l)
214 219 continue
215 220
216 221 rate = list(countrate(ui, repo, amap, *pats, **opts).items())
217 222 if not rate:
218 223 return
219 224
220 225 if opts.get('sort'):
221 226 rate.sort()
222 227 else:
223 228 rate.sort(key=lambda x: (-sum(x[1]), x))
224 229
225 230 # Be careful not to have a zero maxcount (issue833)
226 231 maxcount = float(max(sum(v) for k, v in rate)) or 1.0
227 232 maxname = max(len(k) for k, v in rate)
228 233
229 234 ttywidth = ui.termwidth()
230 235 ui.debug(b"assuming %i character terminal\n" % ttywidth)
231 236 width = ttywidth - maxname - 2 - 2 - 2
232 237
233 238 if opts.get('diffstat'):
234 239 width -= 15
235 240
236 241 def format(name, diffstat):
237 242 added, removed = diffstat
238 243 return b"%s %15s %s%s\n" % (
239 244 pad(name, maxname),
240 245 b'+%d/-%d' % (added, removed),
241 246 ui.label(b'+' * charnum(added), b'diffstat.inserted'),
242 247 ui.label(b'-' * charnum(removed), b'diffstat.deleted'),
243 248 )
244 249
245 250 else:
246 251 width -= 6
247 252
248 253 def format(name, count):
249 254 return b"%s %6d %s\n" % (
250 255 pad(name, maxname),
251 256 sum(count),
252 257 b'*' * charnum(sum(count)),
253 258 )
254 259
255 260 def charnum(count):
256 261 return int(count * width // maxcount)
257 262
258 263 for name, count in rate:
259 264 ui.write(format(name, count))
@@ -1,197 +1,216 b''
1 1 $ echo "[extensions]" >> $HGRCPATH
2 2 $ echo "churn=" >> $HGRCPATH
3 3
4 4 create test repository
5 5
6 6 $ hg init repo
7 7 $ cd repo
8 8 $ echo a > a
9 9 $ hg ci -Am adda -u user1 -d 6:00
10 10 adding a
11 11 $ echo b >> a
12 12 $ echo b > b
13 13 $ hg ci -m changeba -u user2 -d 9:00 a
14 14 $ hg ci -Am addb -u user2 -d 9:30
15 15 adding b
16 16 $ echo c >> a
17 17 $ echo c >> b
18 18 $ echo c > c
19 19 $ hg ci -m changeca -u user3 -d 12:00 a
20 20 $ hg ci -m changecb -u user3 -d 12:15 b
21 21 $ hg ci -Am addc -u user3 -d 12:30
22 22 adding c
23 23 $ mkdir -p d/e
24 24 $ echo abc > d/e/f1.txt
25 25 $ hg ci -Am "add d/e/f1.txt" -u user1 -d 12:45 d/e/f1.txt
26 26 $ mkdir -p d/g
27 27 $ echo def > d/g/f2.txt
28 28 $ hg ci -Am "add d/g/f2.txt" -u user1 -d 13:00 d/g/f2.txt
29 29
30 30
31 31 churn separate directories
32 32
33 33 $ cd d
34 34 $ hg churn e
35 35 user1 1 ***************************************************************
36 36
37 37 churn all
38 38
39 39 $ hg churn
40 40 user1 3 ***************************************************************
41 41 user3 3 ***************************************************************
42 42 user2 2 ******************************************
43 43
44 44 churn excluding one dir
45 45
46 46 $ hg churn -X e
47 47 user3 3 ***************************************************************
48 48 user1 2 ******************************************
49 49 user2 2 ******************************************
50 50
51 51 churn up to rev 2
52 52
53 53 $ hg churn -r :2
54 54 user2 2 ***************************************************************
55 55 user1 1 *******************************
56 56 $ cd ..
57 57
58 58 churn with aliases
59 59
60 60 $ cat > ../aliases <<EOF
61 61 > user1 alias1
62 62 > user3 alias3
63 63 > not-an-alias
64 64 > EOF
65 65
66 66 churn with .hgchurn
67 67
68 68 $ mv ../aliases .hgchurn
69 69 $ hg churn
70 70 skipping malformed alias: not-an-alias
71 71 alias1 3 **************************************************************
72 72 alias3 3 **************************************************************
73 73 user2 2 *****************************************
74 74 $ rm .hgchurn
75 75
76 76 churn with column specifier
77 77
78 78 $ COLUMNS=40 hg churn
79 79 user1 3 ***********************
80 80 user3 3 ***********************
81 81 user2 2 ***************
82 82
83 83 churn by hour
84 84
85 85 $ hg churn -f '%H' -s
86 86 06 1 ****************
87 87 09 2 *********************************
88 88 12 4 ******************************************************************
89 89 13 1 ****************
90 90
91 91
92 92 churn with separated added/removed lines
93 93
94 94 $ hg rm d/g/f2.txt
95 95 $ hg ci -Am "removed d/g/f2.txt" -u user1 -d 14:00 d/g/f2.txt
96 96 $ hg churn --diffstat
97 97 user1 +3/-1 ++++++++++++++++++++++++++++++++++++++++-------------
98 98 user3 +3/-0 ++++++++++++++++++++++++++++++++++++++++
99 99 user2 +2/-0 +++++++++++++++++++++++++++
100 100
101 101 churn --diffstat with color
102 102
103 103 $ hg --config extensions.color= churn --config color.mode=ansi \
104 104 > --diffstat --color=always
105 105 user1 +3/-1 \x1b[0;32m++++++++++++++++++++++++++++++++++++++++\x1b[0m\x1b[0;31m-------------\x1b[0m (esc)
106 106 user3 +3/-0 \x1b[0;32m++++++++++++++++++++++++++++++++++++++++\x1b[0m (esc)
107 107 user2 +2/-0 \x1b[0;32m+++++++++++++++++++++++++++\x1b[0m (esc)
108 108
109 109
110 110 changeset number churn
111 111
112 112 $ hg churn -c
113 113 user1 4 ***************************************************************
114 114 user3 3 ***********************************************
115 115 user2 2 *******************************
116 116
117 117 $ echo 'with space = no-space' >> ../aliases
118 118 $ echo a >> a
119 119 $ hg commit -m a -u 'with space' -d 15:00
120 120
121 121 churn with space in alias
122 122
123 123 $ hg churn --aliases ../aliases -r tip
124 124 no-space 1 ************************************************************
125 125
126 126 $ cd ..
127 127
128 128
129 129 Issue833: ZeroDivisionError
130 130
131 131 $ hg init issue-833
132 132 $ cd issue-833
133 133 $ touch foo
134 134 $ hg ci -Am foo
135 135 adding foo
136 136
137 137 this was failing with a ZeroDivisionError
138 138
139 139 $ hg churn
140 140 test 0
141 141 $ cd ..
142 142
143 143 Ignore trailing or leading spaces in emails
144 144
145 145 $ cd repo
146 146 $ touch bar
147 147 $ hg ci -Am'bar' -u 'user4 <user4@x.com>'
148 148 adding bar
149 149 $ touch foo
150 150 $ hg ci -Am'foo' -u 'user4 < user4@x.com >'
151 151 adding foo
152 152 $ hg log -l2 --template '[{author|email}]\n'
153 153 [ user4@x.com ]
154 154 [user4@x.com]
155 155 $ hg churn -c
156 156 user1 4 *********************************************************
157 157 user3 3 ******************************************
158 158 user2 2 ****************************
159 159 user4@x.com 2 ****************************
160 160 with space 1 **************
161 161
162 162 Test multibyte sequences in names
163 163
164 164 $ echo bar >> bar
165 165 $ hg --encoding utf-8 ci -m'changed bar' -u 'El NiΓ±o <nino@x.com>'
166 166 $ hg --encoding utf-8 churn -ct '{author|person}'
167 167 user1 4 **********************************************************
168 168 user3 3 *******************************************
169 169 user2 2 *****************************
170 170 user4 2 *****************************
171 171 El Ni\xc3\xb1o 1 ************** (esc)
172 172 with space 1 **************
173 173
174 174 Test --template argument, with backwards compatibility
175 175
176 176 $ hg churn -t '{author|user}'
177 177 user1 4 ***************************************************************
178 178 user3 3 ***********************************************
179 179 user2 2 *******************************
180 180 nino 1 ***************
181 181 with 1 ***************
182 182 0
183 183 user4 0
184 184 $ hg churn -T '{author|user}'
185 185 user1 4 ***************************************************************
186 186 user3 3 ***********************************************
187 187 user2 2 *******************************
188 188 nino 1 ***************
189 189 with 1 ***************
190 190 0
191 191 user4 0
192 192 $ hg churn -t 'alltogether'
193 193 alltogether 11 *********************************************************
194 194 $ hg churn -T 'alltogether'
195 195 alltogether 11 *********************************************************
196 196
197 197 $ cd ..
198
199 count lines that look like headings but are not
200
201 $ hg init not-headers
202 $ cd not-headers
203 $ cat > a <<EOF
204 > diff
205 > @@ -195,3 +195,21 @@
206 > -- a/tests/test-churn.t
207 > ++ b/tests/test-churn.t
208 > EOF
209 $ hg ci -Am adda -u user1
210 adding a
211 $ hg churn --diffstat
212 user1 +4/-0 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
213 $ hg rm a
214 $ hg ci -Am removea -u user1
215 $ hg churn --diffstat
216 user1 +4/-4 +++++++++++++++++++++++++++---------------------------
General Comments 0
You need to be logged in to leave comments. Login now