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