##// END OF EJS Templates
churn: do not crash on malformed lines in alias file
Martin Geisler -
r12069:7c3c4441 stable
parent child Browse files
Show More
@@ -1,192 +1,196 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 mercurial.i18n import _
11 from mercurial.i18n import _
12 from mercurial import patch, cmdutil, util, templater, commands
12 from mercurial import patch, cmdutil, util, templater, commands
13 import os
13 import os
14 import time, datetime
14 import time, datetime
15
15
16 def maketemplater(ui, repo, tmpl):
16 def maketemplater(ui, repo, tmpl):
17 tmpl = templater.parsestring(tmpl, quoted=False)
17 tmpl = templater.parsestring(tmpl, quoted=False)
18 try:
18 try:
19 t = cmdutil.changeset_templater(ui, repo, False, None, None, False)
19 t = cmdutil.changeset_templater(ui, repo, False, None, None, False)
20 except SyntaxError, inst:
20 except SyntaxError, inst:
21 raise util.Abort(inst.args[0])
21 raise util.Abort(inst.args[0])
22 t.use_template(tmpl)
22 t.use_template(tmpl)
23 return t
23 return t
24
24
25 def changedlines(ui, repo, ctx1, ctx2, fns):
25 def changedlines(ui, repo, ctx1, ctx2, fns):
26 added, removed = 0, 0
26 added, removed = 0, 0
27 fmatch = cmdutil.matchfiles(repo, fns)
27 fmatch = cmdutil.matchfiles(repo, fns)
28 diff = ''.join(patch.diff(repo, ctx1.node(), ctx2.node(), fmatch))
28 diff = ''.join(patch.diff(repo, ctx1.node(), ctx2.node(), fmatch))
29 for l in diff.split('\n'):
29 for l in diff.split('\n'):
30 if l.startswith("+") and not l.startswith("+++ "):
30 if l.startswith("+") and not l.startswith("+++ "):
31 added += 1
31 added += 1
32 elif l.startswith("-") and not l.startswith("--- "):
32 elif l.startswith("-") and not l.startswith("--- "):
33 removed += 1
33 removed += 1
34 return (added, removed)
34 return (added, removed)
35
35
36 def countrate(ui, repo, amap, *pats, **opts):
36 def countrate(ui, repo, amap, *pats, **opts):
37 """Calculate stats"""
37 """Calculate stats"""
38 if opts.get('dateformat'):
38 if opts.get('dateformat'):
39 def getkey(ctx):
39 def getkey(ctx):
40 t, tz = ctx.date()
40 t, tz = ctx.date()
41 date = datetime.datetime(*time.gmtime(float(t) - tz)[:6])
41 date = datetime.datetime(*time.gmtime(float(t) - tz)[:6])
42 return date.strftime(opts['dateformat'])
42 return date.strftime(opts['dateformat'])
43 else:
43 else:
44 tmpl = opts.get('template', '{author|email}')
44 tmpl = opts.get('template', '{author|email}')
45 tmpl = maketemplater(ui, repo, tmpl)
45 tmpl = maketemplater(ui, repo, tmpl)
46 def getkey(ctx):
46 def getkey(ctx):
47 ui.pushbuffer()
47 ui.pushbuffer()
48 tmpl.show(ctx)
48 tmpl.show(ctx)
49 return ui.popbuffer()
49 return ui.popbuffer()
50
50
51 state = {'count': 0}
51 state = {'count': 0}
52 rate = {}
52 rate = {}
53 df = False
53 df = False
54 if opts.get('date'):
54 if opts.get('date'):
55 df = util.matchdate(opts['date'])
55 df = util.matchdate(opts['date'])
56
56
57 m = cmdutil.match(repo, pats, opts)
57 m = cmdutil.match(repo, pats, opts)
58 def prep(ctx, fns):
58 def prep(ctx, fns):
59 rev = ctx.rev()
59 rev = ctx.rev()
60 if df and not df(ctx.date()[0]): # doesn't match date format
60 if df and not df(ctx.date()[0]): # doesn't match date format
61 return
61 return
62
62
63 key = getkey(ctx)
63 key = getkey(ctx)
64 key = amap.get(key, key) # alias remap
64 key = amap.get(key, key) # alias remap
65 if opts.get('changesets'):
65 if opts.get('changesets'):
66 rate[key] = (rate.get(key, (0,))[0] + 1, 0)
66 rate[key] = (rate.get(key, (0,))[0] + 1, 0)
67 else:
67 else:
68 parents = ctx.parents()
68 parents = ctx.parents()
69 if len(parents) > 1:
69 if len(parents) > 1:
70 ui.note(_('Revision %d is a merge, ignoring...\n') % (rev,))
70 ui.note(_('Revision %d is a merge, ignoring...\n') % (rev,))
71 return
71 return
72
72
73 ctx1 = parents[0]
73 ctx1 = parents[0]
74 lines = changedlines(ui, repo, ctx1, ctx, fns)
74 lines = changedlines(ui, repo, ctx1, ctx, fns)
75 rate[key] = [r + l for r, l in zip(rate.get(key, (0, 0)), lines)]
75 rate[key] = [r + l for r, l in zip(rate.get(key, (0, 0)), lines)]
76
76
77 state['count'] += 1
77 state['count'] += 1
78 ui.progress(_('analyzing'), state['count'], total=len(repo))
78 ui.progress(_('analyzing'), state['count'], total=len(repo))
79
79
80 for ctx in cmdutil.walkchangerevs(repo, m, opts, prep):
80 for ctx in cmdutil.walkchangerevs(repo, m, opts, prep):
81 continue
81 continue
82
82
83 ui.progress(_('analyzing'), None)
83 ui.progress(_('analyzing'), None)
84
84
85 return rate
85 return rate
86
86
87
87
88 def churn(ui, repo, *pats, **opts):
88 def churn(ui, repo, *pats, **opts):
89 '''histogram of changes to the repository
89 '''histogram of changes to the repository
90
90
91 This command will display a histogram representing the number
91 This command will display a histogram representing the number
92 of changed lines or revisions, grouped according to the given
92 of changed lines or revisions, grouped according to the given
93 template. The default template will group changes by author.
93 template. The default template will group changes by author.
94 The --dateformat option may be used to group the results by
94 The --dateformat option may be used to group the results by
95 date instead.
95 date instead.
96
96
97 Statistics are based on the number of changed lines, or
97 Statistics are based on the number of changed lines, or
98 alternatively the number of matching revisions if the
98 alternatively the number of matching revisions if the
99 --changesets option is specified.
99 --changesets option is specified.
100
100
101 Examples::
101 Examples::
102
102
103 # display count of changed lines for every committer
103 # display count of changed lines for every committer
104 hg churn -t '{author|email}'
104 hg churn -t '{author|email}'
105
105
106 # display daily activity graph
106 # display daily activity graph
107 hg churn -f '%H' -s -c
107 hg churn -f '%H' -s -c
108
108
109 # display activity of developers by month
109 # display activity of developers by month
110 hg churn -f '%Y-%m' -s -c
110 hg churn -f '%Y-%m' -s -c
111
111
112 # display count of lines changed in every year
112 # display count of lines changed in every year
113 hg churn -f '%Y' -s
113 hg churn -f '%Y' -s
114
114
115 It is possible to map alternate email addresses to a main address
115 It is possible to map alternate email addresses to a main address
116 by providing a file using the following format::
116 by providing a file using the following format::
117
117
118 <alias email> = <actual email>
118 <alias email> = <actual email>
119
119
120 Such a file may be specified with the --aliases option, otherwise
120 Such a file may be specified with the --aliases option, otherwise
121 a .hgchurn file will be looked for in the working directory root.
121 a .hgchurn file will be looked for in the working directory root.
122 '''
122 '''
123 def pad(s, l):
123 def pad(s, l):
124 return (s + " " * l)[:l]
124 return (s + " " * l)[:l]
125
125
126 amap = {}
126 amap = {}
127 aliases = opts.get('aliases')
127 aliases = opts.get('aliases')
128 if not aliases and os.path.exists(repo.wjoin('.hgchurn')):
128 if not aliases and os.path.exists(repo.wjoin('.hgchurn')):
129 aliases = repo.wjoin('.hgchurn')
129 aliases = repo.wjoin('.hgchurn')
130 if aliases:
130 if aliases:
131 for l in open(aliases, "r"):
131 for l in open(aliases, "r"):
132 if not l.strip():
132 try:
133 alias, actual = l.split('=' in l and '=' or None, 1)
134 amap[alias.strip()] = actual.strip()
135 except ValueError:
136 l = l.strip()
137 if l:
138 ui.warn(_("skipping malformed alias: %s\n" % l))
133 continue
139 continue
134 alias, actual = l.split('=' in l and '=' or None, 1)
135 amap[alias.strip()] = actual.strip()
136
140
137 rate = countrate(ui, repo, amap, *pats, **opts).items()
141 rate = countrate(ui, repo, amap, *pats, **opts).items()
138 if not rate:
142 if not rate:
139 return
143 return
140
144
141 sortkey = ((not opts.get('sort')) and (lambda x: -sum(x[1])) or None)
145 sortkey = ((not opts.get('sort')) and (lambda x: -sum(x[1])) or None)
142 rate.sort(key=sortkey)
146 rate.sort(key=sortkey)
143
147
144 # Be careful not to have a zero maxcount (issue833)
148 # Be careful not to have a zero maxcount (issue833)
145 maxcount = float(max(sum(v) for k, v in rate)) or 1.0
149 maxcount = float(max(sum(v) for k, v in rate)) or 1.0
146 maxname = max(len(k) for k, v in rate)
150 maxname = max(len(k) for k, v in rate)
147
151
148 ttywidth = util.termwidth()
152 ttywidth = util.termwidth()
149 ui.debug("assuming %i character terminal\n" % ttywidth)
153 ui.debug("assuming %i character terminal\n" % ttywidth)
150 width = ttywidth - maxname - 2 - 2 - 2
154 width = ttywidth - maxname - 2 - 2 - 2
151
155
152 if opts.get('diffstat'):
156 if opts.get('diffstat'):
153 width -= 15
157 width -= 15
154 def format(name, (added, removed)):
158 def format(name, (added, removed)):
155 return "%s %15s %s%s\n" % (pad(name, maxname),
159 return "%s %15s %s%s\n" % (pad(name, maxname),
156 '+%d/-%d' % (added, removed),
160 '+%d/-%d' % (added, removed),
157 ui.label('+' * charnum(added),
161 ui.label('+' * charnum(added),
158 'diffstat.inserted'),
162 'diffstat.inserted'),
159 ui.label('-' * charnum(removed),
163 ui.label('-' * charnum(removed),
160 'diffstat.deleted'))
164 'diffstat.deleted'))
161 else:
165 else:
162 width -= 6
166 width -= 6
163 def format(name, count):
167 def format(name, count):
164 return "%s %6d %s\n" % (pad(name, maxname), sum(count),
168 return "%s %6d %s\n" % (pad(name, maxname), sum(count),
165 '*' * charnum(sum(count)))
169 '*' * charnum(sum(count)))
166
170
167 def charnum(count):
171 def charnum(count):
168 return int(round(count * width / maxcount))
172 return int(round(count * width / maxcount))
169
173
170 for name, count in rate:
174 for name, count in rate:
171 ui.write(format(name, count))
175 ui.write(format(name, count))
172
176
173
177
174 cmdtable = {
178 cmdtable = {
175 "churn":
179 "churn":
176 (churn,
180 (churn,
177 [('r', 'rev', [],
181 [('r', 'rev', [],
178 _('count rate for the specified revision or range'), _('REV')),
182 _('count rate for the specified revision or range'), _('REV')),
179 ('d', 'date', '',
183 ('d', 'date', '',
180 _('count rate for revisions matching date spec'), _('DATE')),
184 _('count rate for revisions matching date spec'), _('DATE')),
181 ('t', 'template', '{author|email}',
185 ('t', 'template', '{author|email}',
182 _('template to group changesets'), _('TEMPLATE')),
186 _('template to group changesets'), _('TEMPLATE')),
183 ('f', 'dateformat', '',
187 ('f', 'dateformat', '',
184 _('strftime-compatible format for grouping by date'), _('FORMAT')),
188 _('strftime-compatible format for grouping by date'), _('FORMAT')),
185 ('c', 'changesets', False, _('count rate by number of changesets')),
189 ('c', 'changesets', False, _('count rate by number of changesets')),
186 ('s', 'sort', False, _('sort by key (default: sort by count)')),
190 ('s', 'sort', False, _('sort by key (default: sort by count)')),
187 ('', 'diffstat', False, _('display added/removed lines separately')),
191 ('', 'diffstat', False, _('display added/removed lines separately')),
188 ('', 'aliases', '',
192 ('', 'aliases', '',
189 _('file with email aliases'), _('FILE')),
193 _('file with email aliases'), _('FILE')),
190 ] + commands.walkopts,
194 ] + commands.walkopts,
191 _("hg churn [-d DATE] [-r REV] [--aliases FILE] [FILE]")),
195 _("hg churn [-d DATE] [-r REV] [--aliases FILE] [FILE]")),
192 }
196 }
@@ -1,80 +1,81 b''
1 #!/bin/sh
1 #!/bin/sh
2
2
3 echo "[extensions]" >> $HGRCPATH
3 echo "[extensions]" >> $HGRCPATH
4 echo "churn=" >> $HGRCPATH
4 echo "churn=" >> $HGRCPATH
5
5
6 echo % create test repository
6 echo % create test repository
7 hg init repo
7 hg init repo
8 cd repo
8 cd repo
9 echo a > a
9 echo a > a
10 hg ci -Am adda -u user1 -d 6:00
10 hg ci -Am adda -u user1 -d 6:00
11 echo b >> a
11 echo b >> a
12 echo b > b
12 echo b > b
13 hg ci -m changeba -u user2 -d 9:00 a
13 hg ci -m changeba -u user2 -d 9:00 a
14 hg ci -Am addb -u user2 -d 9:30
14 hg ci -Am addb -u user2 -d 9:30
15 echo c >> a
15 echo c >> a
16 echo c >> b
16 echo c >> b
17 echo c > c
17 echo c > c
18 hg ci -m changeca -u user3 -d 12:00 a
18 hg ci -m changeca -u user3 -d 12:00 a
19 hg ci -m changecb -u user3 -d 12:15 b
19 hg ci -m changecb -u user3 -d 12:15 b
20 hg ci -Am addc -u user3 -d 12:30
20 hg ci -Am addc -u user3 -d 12:30
21 mkdir -p d/e
21 mkdir -p d/e
22 echo abc > d/e/f1.txt
22 echo abc > d/e/f1.txt
23 hg ci -Am "add d/e/f1.txt" -u user1 -d 12:45 d/e/f1.txt
23 hg ci -Am "add d/e/f1.txt" -u user1 -d 12:45 d/e/f1.txt
24 mkdir -p d/g
24 mkdir -p d/g
25 echo def > d/g/f2.txt
25 echo def > d/g/f2.txt
26 hg ci -Am "add d/g/f2.txt" -u user1 -d 13:00 d/g/f2.txt
26 hg ci -Am "add d/g/f2.txt" -u user1 -d 13:00 d/g/f2.txt
27
27
28 echo % churn separate directories
28 echo % churn separate directories
29 cd d
29 cd d
30 hg churn e
30 hg churn e
31 echo % churn all
31 echo % churn all
32 hg churn
32 hg churn
33 echo % churn excluding one dir
33 echo % churn excluding one dir
34 hg churn -X e
34 hg churn -X e
35 echo % churn up to rev 2
35 echo % churn up to rev 2
36 hg churn -r :2
36 hg churn -r :2
37 cd ..
37 cd ..
38 echo % churn with aliases
38 echo % churn with aliases
39 cat > ../aliases <<EOF
39 cat > ../aliases <<EOF
40 user1 alias1
40 user1 alias1
41
41
42 user3 alias3
42 user3 alias3
43 not-an-alias
43 EOF
44 EOF
44 hg churn --aliases ../aliases
45 hg churn --aliases ../aliases
45 echo % churn with .hgchurn
46 echo % churn with .hgchurn
46 mv ../aliases .hgchurn
47 mv ../aliases .hgchurn
47 hg churn
48 hg churn
48 rm .hgchurn
49 rm .hgchurn
49 echo % churn with column specifier
50 echo % churn with column specifier
50 COLUMNS=40 hg churn
51 COLUMNS=40 hg churn
51 echo % churn by hour
52 echo % churn by hour
52 hg churn -f '%H' -s
53 hg churn -f '%H' -s
53
54
54 echo % churn with separated added/removed lines
55 echo % churn with separated added/removed lines
55 hg rm d/g/f2.txt
56 hg rm d/g/f2.txt
56 hg ci -Am "removed d/g/f2.txt" -u user1 -d 14:00 d/g/f2.txt
57 hg ci -Am "removed d/g/f2.txt" -u user1 -d 14:00 d/g/f2.txt
57 hg churn --diffstat
58 hg churn --diffstat
58 echo % churn --diffstat with color
59 echo % churn --diffstat with color
59 hg --config extensions.color= churn --config color.mode=ansi \
60 hg --config extensions.color= churn --config color.mode=ansi \
60 --diffstat --color=always
61 --diffstat --color=always
61
62
62 echo % changeset number churn
63 echo % changeset number churn
63 hg churn -c
64 hg churn -c
64
65
65 echo 'with space = no-space' >> ../aliases
66 echo 'with space = no-space' >> ../aliases
66 echo a >> a
67 echo a >> a
67 hg commit -m a -u 'with space' -d 15:00
68 hg commit -m a -u 'with space' -d 15:00
68 echo % churn with space in alias
69 echo % churn with space in alias
69 hg churn --aliases ../aliases -r tip
70 hg churn --aliases ../aliases -r tip
70
71
71 cd ..
72 cd ..
72
73
73 # issue 833: ZeroDivisionError
74 # issue 833: ZeroDivisionError
74 hg init issue-833
75 hg init issue-833
75 cd issue-833
76 cd issue-833
76 touch foo
77 touch foo
77 hg ci -Am foo
78 hg ci -Am foo
78 # this was failing with a ZeroDivisionError
79 # this was failing with a ZeroDivisionError
79 hg churn
80 hg churn
80 cd ..
81 cd ..
@@ -1,50 +1,52 b''
1 % create test repository
1 % create test repository
2 adding a
2 adding a
3 adding b
3 adding b
4 adding c
4 adding c
5 % churn separate directories
5 % churn separate directories
6 user1 1 ***************************************************************
6 user1 1 ***************************************************************
7 % churn all
7 % churn all
8 user3 3 ***************************************************************
8 user3 3 ***************************************************************
9 user1 3 ***************************************************************
9 user1 3 ***************************************************************
10 user2 2 ******************************************
10 user2 2 ******************************************
11 % churn excluding one dir
11 % churn excluding one dir
12 user3 3 ***************************************************************
12 user3 3 ***************************************************************
13 user2 2 ******************************************
13 user2 2 ******************************************
14 user1 2 ******************************************
14 user1 2 ******************************************
15 % churn up to rev 2
15 % churn up to rev 2
16 user2 2 ***************************************************************
16 user2 2 ***************************************************************
17 user1 1 ********************************
17 user1 1 ********************************
18 % churn with aliases
18 % churn with aliases
19 skipping malformed alias: not-an-alias
19 alias3 3 **************************************************************
20 alias3 3 **************************************************************
20 alias1 3 **************************************************************
21 alias1 3 **************************************************************
21 user2 2 *****************************************
22 user2 2 *****************************************
22 % churn with .hgchurn
23 % churn with .hgchurn
24 skipping malformed alias: not-an-alias
23 alias3 3 **************************************************************
25 alias3 3 **************************************************************
24 alias1 3 **************************************************************
26 alias1 3 **************************************************************
25 user2 2 *****************************************
27 user2 2 *****************************************
26 % churn with column specifier
28 % churn with column specifier
27 user3 3 ***********************
29 user3 3 ***********************
28 user1 3 ***********************
30 user1 3 ***********************
29 user2 2 ***************
31 user2 2 ***************
30 % churn by hour
32 % churn by hour
31 06 1 *****************
33 06 1 *****************
32 09 2 *********************************
34 09 2 *********************************
33 12 4 ******************************************************************
35 12 4 ******************************************************************
34 13 1 *****************
36 13 1 *****************
35 % churn with separated added/removed lines
37 % churn with separated added/removed lines
36 user1 +3/-1 +++++++++++++++++++++++++++++++++++++++++--------------
38 user1 +3/-1 +++++++++++++++++++++++++++++++++++++++++--------------
37 user3 +3/-0 +++++++++++++++++++++++++++++++++++++++++
39 user3 +3/-0 +++++++++++++++++++++++++++++++++++++++++
38 user2 +2/-0 +++++++++++++++++++++++++++
40 user2 +2/-0 +++++++++++++++++++++++++++
39 % churn --diffstat with color
41 % churn --diffstat with color
40 user1 +3/-1 +++++++++++++++++++++++++++++++++++++++++--------------
42 user1 +3/-1 +++++++++++++++++++++++++++++++++++++++++--------------
41 user3 +3/-0 +++++++++++++++++++++++++++++++++++++++++
43 user3 +3/-0 +++++++++++++++++++++++++++++++++++++++++
42 user2 +2/-0 +++++++++++++++++++++++++++
44 user2 +2/-0 +++++++++++++++++++++++++++
43 % changeset number churn
45 % changeset number churn
44 user1 4 ***************************************************************
46 user1 4 ***************************************************************
45 user3 3 ***********************************************
47 user3 3 ***********************************************
46 user2 2 ********************************
48 user2 2 ********************************
47 % churn with space in alias
49 % churn with space in alias
48 no-space 1 ************************************************************
50 no-space 1 ************************************************************
49 adding foo
51 adding foo
50 test 0
52 test 0
General Comments 0
You need to be logged in to leave comments. Login now