##// END OF EJS Templates
merge with crew-stable
Dirkjan Ochtman -
r9389:7cca9803 merge default
parent child Browse files
Show More
@@ -1,176 +1,177
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, incorporated herein by reference.
7 # GNU General Public License version 2, incorporated herein by reference.
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
12 from mercurial import patch, cmdutil, util, templater
13 import sys, os
13 import sys, 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 lines = 0
26 lines = 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("+++ ") or
30 if (l.startswith("+") and not l.startswith("+++ ") or
31 l.startswith("-") and not l.startswith("--- ")):
31 l.startswith("-") and not l.startswith("--- ")):
32 lines += 1
32 lines += 1
33 return lines
33 return lines
34
34
35 def countrate(ui, repo, amap, *pats, **opts):
35 def countrate(ui, repo, amap, *pats, **opts):
36 """Calculate stats"""
36 """Calculate stats"""
37 if opts.get('dateformat'):
37 if opts.get('dateformat'):
38 def getkey(ctx):
38 def getkey(ctx):
39 t, tz = ctx.date()
39 t, tz = ctx.date()
40 date = datetime.datetime(*time.gmtime(float(t) - tz)[:6])
40 date = datetime.datetime(*time.gmtime(float(t) - tz)[:6])
41 return date.strftime(opts['dateformat'])
41 return date.strftime(opts['dateformat'])
42 else:
42 else:
43 tmpl = opts.get('template', '{author|email}')
43 tmpl = opts.get('template', '{author|email}')
44 tmpl = maketemplater(ui, repo, tmpl)
44 tmpl = maketemplater(ui, repo, tmpl)
45 def getkey(ctx):
45 def getkey(ctx):
46 ui.pushbuffer()
46 ui.pushbuffer()
47 tmpl.show(ctx)
47 tmpl.show(ctx)
48 return ui.popbuffer()
48 return ui.popbuffer()
49
49
50 count = pct = 0
50 count = pct = 0
51 rate = {}
51 rate = {}
52 df = False
52 df = False
53 if opts.get('date'):
53 if opts.get('date'):
54 df = util.matchdate(opts['date'])
54 df = util.matchdate(opts['date'])
55
55
56 get = util.cachefunc(lambda r: repo[r])
56 get = util.cachefunc(lambda r: repo[r])
57 changeiter, matchfn = cmdutil.walkchangerevs(ui, repo, pats, get, opts)
57 changeiter, matchfn = cmdutil.walkchangerevs(ui, repo, pats, get, opts)
58 for st, rev, fns in changeiter:
58 for st, rev, fns in changeiter:
59
59
60 if not st == 'add':
60 if not st == 'add':
61 continue
61 continue
62
62
63 ctx = get(rev)
63 ctx = get(rev)
64 if df and not df(ctx.date()[0]): # doesn't match date format
64 if df and not df(ctx.date()[0]): # doesn't match date format
65 continue
65 continue
66
66
67 key = getkey(ctx)
67 key = getkey(ctx)
68 key = amap.get(key, key) # alias remap
68 key = amap.get(key, key) # alias remap
69 if opts.get('changesets'):
69 if opts.get('changesets'):
70 rate[key] = rate.get(key, 0) + 1
70 rate[key] = rate.get(key, 0) + 1
71 else:
71 else:
72 parents = ctx.parents()
72 parents = ctx.parents()
73 if len(parents) > 1:
73 if len(parents) > 1:
74 ui.note(_('Revision %d is a merge, ignoring...\n') % (rev,))
74 ui.note(_('Revision %d is a merge, ignoring...\n') % (rev,))
75 continue
75 continue
76
76
77 ctx1 = parents[0]
77 ctx1 = parents[0]
78 lines = changedlines(ui, repo, ctx1, ctx, fns)
78 lines = changedlines(ui, repo, ctx1, ctx, fns)
79 rate[key] = rate.get(key, 0) + lines
79 rate[key] = rate.get(key, 0) + lines
80
80
81 if opts.get('progress'):
81 if opts.get('progress'):
82 count += 1
82 count += 1
83 newpct = int(100.0 * count / max(len(repo), 1))
83 newpct = int(100.0 * count / max(len(repo), 1))
84 if pct < newpct:
84 if pct < newpct:
85 pct = newpct
85 pct = newpct
86 ui.write("\r" + _("generating stats: %d%%") % pct)
86 ui.write("\r" + _("generating stats: %d%%") % pct)
87 sys.stdout.flush()
87 sys.stdout.flush()
88
88
89 if opts.get('progress'):
89 if opts.get('progress'):
90 ui.write("\r")
90 ui.write("\r")
91 sys.stdout.flush()
91 sys.stdout.flush()
92
92
93 return rate
93 return rate
94
94
95
95
96 def churn(ui, repo, *pats, **opts):
96 def churn(ui, repo, *pats, **opts):
97 '''histogram of changes to the repository
97 '''histogram of changes to the repository
98
98
99 This command will display a histogram representing the number
99 This command will display a histogram representing the number
100 of changed lines or revisions, grouped according to the given
100 of changed lines or revisions, grouped according to the given
101 template. The default template will group changes by author.
101 template. The default template will group changes by author.
102 The --dateformat option may be used to group the results by
102 The --dateformat option may be used to group the results by
103 date instead.
103 date instead.
104
104
105 Statistics are based on the number of changed lines, or
105 Statistics are based on the number of changed lines, or
106 alternatively the number of matching revisions if the
106 alternatively the number of matching revisions if the
107 --changesets option is specified.
107 --changesets option is specified.
108
108
109 Examples::
109 Examples::
110
110
111 # display count of changed lines for every committer
111 # display count of changed lines for every committer
112 hg churn -t '{author|email}'
112 hg churn -t '{author|email}'
113
113
114 # display daily activity graph
114 # display daily activity graph
115 hg churn -f '%H' -s -c
115 hg churn -f '%H' -s -c
116
116
117 # display activity of developers by month
117 # display activity of developers by month
118 hg churn -f '%Y-%m' -s -c
118 hg churn -f '%Y-%m' -s -c
119
119
120 # display count of lines changed in every year
120 # display count of lines changed in every year
121 hg churn -f '%Y' -s
121 hg churn -f '%Y' -s
122
122
123 It is possible to map alternate email addresses to a main address
123 It is possible to map alternate email addresses to a main address
124 by providing a file using the following format::
124 by providing a file using the following format::
125
125
126 <alias email> <actual email>
126 <alias email> <actual email>
127
127
128 Such a file may be specified with the --aliases option, otherwise
128 Such a file may be specified with the --aliases option, otherwise
129 a .hgchurn file will be looked for in the working directory root.
129 a .hgchurn file will be looked for in the working directory root.
130 '''
130 '''
131 def pad(s, l):
131 def pad(s, l):
132 return (s + " " * l)[:l]
132 return (s + " " * l)[:l]
133
133
134 amap = {}
134 amap = {}
135 aliases = opts.get('aliases')
135 aliases = opts.get('aliases')
136 if not aliases and os.path.exists(repo.wjoin('.hgchurn')):
136 if not aliases and os.path.exists(repo.wjoin('.hgchurn')):
137 aliases = repo.wjoin('.hgchurn')
137 aliases = repo.wjoin('.hgchurn')
138 if aliases:
138 if aliases:
139 for l in open(aliases, "r"):
139 for l in open(aliases, "r"):
140 l = l.strip()
140 l = l.strip()
141 alias, actual = l.split()
141 alias, actual = l.split()
142 amap[alias] = actual
142 amap[alias] = actual
143
143
144 rate = countrate(ui, repo, amap, *pats, **opts).items()
144 rate = countrate(ui, repo, amap, *pats, **opts).items()
145 if not rate:
145 if not rate:
146 return
146 return
147
147
148 sortkey = ((not opts.get('sort')) and (lambda x: -x[1]) or None)
148 sortkey = ((not opts.get('sort')) and (lambda x: -x[1]) or None)
149 rate.sort(key=sortkey)
149 rate.sort(key=sortkey)
150
150
151 maxcount = float(max([v for k, v in rate]))
151 # Be careful not to have a zero maxcount (issue833)
152 maxcount = float(max([v for k, v in rate])) or 1.0
152 maxname = max([len(k) for k, v in rate])
153 maxname = max([len(k) for k, v in rate])
153
154
154 ttywidth = util.termwidth()
155 ttywidth = util.termwidth()
155 ui.debug(_("assuming %i character terminal\n") % ttywidth)
156 ui.debug(_("assuming %i character terminal\n") % ttywidth)
156 width = ttywidth - maxname - 2 - 6 - 2 - 2
157 width = ttywidth - maxname - 2 - 6 - 2 - 2
157
158
158 for date, count in rate:
159 for date, count in rate:
159 print "%s %6d %s" % (pad(date, maxname), count,
160 print "%s %6d %s" % (pad(date, maxname), count,
160 "*" * int(count * width / maxcount))
161 "*" * int(count * width / maxcount))
161
162
162
163
163 cmdtable = {
164 cmdtable = {
164 "churn":
165 "churn":
165 (churn,
166 (churn,
166 [('r', 'rev', [], _('count rate for the specified revision or range')),
167 [('r', 'rev', [], _('count rate for the specified revision or range')),
167 ('d', 'date', '', _('count rate for revisions matching date spec')),
168 ('d', 'date', '', _('count rate for revisions matching date spec')),
168 ('t', 'template', '{author|email}', _('template to group changesets')),
169 ('t', 'template', '{author|email}', _('template to group changesets')),
169 ('f', 'dateformat', '',
170 ('f', 'dateformat', '',
170 _('strftime-compatible format for grouping by date')),
171 _('strftime-compatible format for grouping by date')),
171 ('c', 'changesets', False, _('count rate by number of changesets')),
172 ('c', 'changesets', False, _('count rate by number of changesets')),
172 ('s', 'sort', False, _('sort by key (default: sort by count)')),
173 ('s', 'sort', False, _('sort by key (default: sort by count)')),
173 ('', 'aliases', '', _('file with email aliases')),
174 ('', 'aliases', '', _('file with email aliases')),
174 ('', 'progress', None, _('show progress'))],
175 ('', 'progress', None, _('show progress'))],
175 _("hg churn [-d DATE] [-r REV] [--aliases FILE] [--progress] [FILE]")),
176 _("hg churn [-d DATE] [-r REV] [--aliases FILE] [--progress] [FILE]")),
176 }
177 }
@@ -1,52 +1,62
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 COLUMNS=80; export COLUMNS
6 COLUMNS=80; export COLUMNS
7
7
8 echo % create test repository
8 echo % create test repository
9 hg init repo
9 hg init repo
10 cd repo
10 cd repo
11 echo a > a
11 echo a > a
12 hg ci -Am adda -u user1 -d 6:00
12 hg ci -Am adda -u user1 -d 6:00
13 echo b >> a
13 echo b >> a
14 echo b > b
14 echo b > b
15 hg ci -m changeba -u user2 -d 9:00 a
15 hg ci -m changeba -u user2 -d 9:00 a
16 hg ci -Am addb -u user2 -d 9:30
16 hg ci -Am addb -u user2 -d 9:30
17 echo c >> a
17 echo c >> a
18 echo c >> b
18 echo c >> b
19 echo c > c
19 echo c > c
20 hg ci -m changeca -u user3 -d 12:00 a
20 hg ci -m changeca -u user3 -d 12:00 a
21 hg ci -m changecb -u user3 -d 12:15 b
21 hg ci -m changecb -u user3 -d 12:15 b
22 hg ci -Am addc -u user3 -d 12:30
22 hg ci -Am addc -u user3 -d 12:30
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 echo % churn separate directories
30 echo % churn separate directories
31 cd d
31 cd d
32 hg churn e
32 hg churn e
33 echo % churn all
33 echo % churn all
34 hg churn
34 hg churn
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 user3 alias3
41 user3 alias3
42 EOF
42 EOF
43 hg churn --aliases ../aliases
43 hg churn --aliases ../aliases
44 echo % churn with .hgchurn
44 echo % churn with .hgchurn
45 mv ../aliases .hgchurn
45 mv ../aliases .hgchurn
46 hg churn
46 hg churn
47 rm .hgchurn
47 rm .hgchurn
48 echo % churn with column specifier
48 echo % churn with column specifier
49 COLUMNS=40 hg churn
49 COLUMNS=40 hg churn
50 echo % churn by hour
50 echo % churn by hour
51 hg churn -f '%H' -s
51 hg churn -f '%H' -s
52
52
53 cd ..
54
55 # issue 833: ZeroDivisionError
56 hg init issue-833
57 cd issue-833
58 touch foo
59 hg ci -Am foo
60 # this was failing with a ZeroDivisionError
61 hg churn
62 cd ..
@@ -1,30 +1,32
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 up to rev 2
11 % churn up to rev 2
12 user2 2 ***************************************************************
12 user2 2 ***************************************************************
13 user1 1 *******************************
13 user1 1 *******************************
14 % churn with aliases
14 % churn with aliases
15 alias3 3 **************************************************************
15 alias3 3 **************************************************************
16 alias1 3 **************************************************************
16 alias1 3 **************************************************************
17 user2 2 *****************************************
17 user2 2 *****************************************
18 % churn with .hgchurn
18 % churn with .hgchurn
19 alias3 3 **************************************************************
19 alias3 3 **************************************************************
20 alias1 3 **************************************************************
20 alias1 3 **************************************************************
21 user2 2 *****************************************
21 user2 2 *****************************************
22 % churn with column specifier
22 % churn with column specifier
23 user3 3 ***********************
23 user3 3 ***********************
24 user1 3 ***********************
24 user1 3 ***********************
25 user2 2 ***************
25 user2 2 ***************
26 % churn by hour
26 % churn by hour
27 06 1 ****************
27 06 1 ****************
28 09 2 *********************************
28 09 2 *********************************
29 12 4 ******************************************************************
29 12 4 ******************************************************************
30 13 1 ****************
30 13 1 ****************
31 adding foo
32 test 0
General Comments 0
You need to be logged in to leave comments. Login now