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