|
1 # churn.py - create a graph of revisions count grouped by template |
|
2 # |
|
3 # Copyright 2006 Josef "Jeff" Sipek <jeffpc@josefsipek.net> |
|
4 # Copyright 2008 Alexander Solovyov <piranha@piranha.org.ua> |
|
5 # |
|
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. |
|
8 |
|
9 '''command to display statistics about repository history''' |
|
10 |
|
11 from mercurial.i18n import _ |
|
12 from mercurial import patch, cmdutil, util, templater, commands |
|
13 import os |
|
14 import time, datetime |
|
15 |
|
16 def maketemplater(ui, repo, tmpl): |
|
17 tmpl = templater.parsestring(tmpl, quoted=False) |
|
18 try: |
|
19 t = cmdutil.changeset_templater(ui, repo, False, None, None, False) |
|
20 except SyntaxError, inst: |
|
21 raise util.Abort(inst.args[0]) |
|
22 t.use_template(tmpl) |
|
23 return t |
|
24 |
|
25 def changedlines(ui, repo, ctx1, ctx2, fns): |
|
26 added, removed = 0, 0 |
|
27 fmatch = cmdutil.matchfiles(repo, fns) |
|
28 diff = ''.join(patch.diff(repo, ctx1.node(), ctx2.node(), fmatch)) |
|
29 for l in diff.split('\n'): |
|
30 if l.startswith("+") and not l.startswith("+++ "): |
|
31 added += 1 |
|
32 elif l.startswith("-") and not l.startswith("--- "): |
|
33 removed += 1 |
|
34 return (added, removed) |
|
35 |
|
36 def countrate(ui, repo, amap, *pats, **opts): |
|
37 """Calculate stats""" |
|
38 if opts.get('dateformat'): |
|
39 def getkey(ctx): |
|
40 t, tz = ctx.date() |
|
41 date = datetime.datetime(*time.gmtime(float(t) - tz)[:6]) |
|
42 return date.strftime(opts['dateformat']) |
|
43 else: |
|
44 tmpl = opts.get('template', '{author|email}') |
|
45 tmpl = maketemplater(ui, repo, tmpl) |
|
46 def getkey(ctx): |
|
47 ui.pushbuffer() |
|
48 tmpl.show(ctx) |
|
49 return ui.popbuffer() |
|
50 |
|
51 state = {'count': 0} |
|
52 rate = {} |
|
53 df = False |
|
54 if opts.get('date'): |
|
55 df = util.matchdate(opts['date']) |
|
56 |
|
57 m = cmdutil.match(repo, pats, opts) |
|
58 def prep(ctx, fns): |
|
59 rev = ctx.rev() |
|
60 if df and not df(ctx.date()[0]): # doesn't match date format |
|
61 return |
|
62 |
|
63 key = getkey(ctx) |
|
64 key = amap.get(key, key) # alias remap |
|
65 key = key.strip() # ignore leading and trailing spaces |
|
66 if opts.get('changesets'): |
|
67 rate[key] = (rate.get(key, (0,))[0] + 1, 0) |
|
68 else: |
|
69 parents = ctx.parents() |
|
70 if len(parents) > 1: |
|
71 ui.note(_('Revision %d is a merge, ignoring...\n') % (rev,)) |
|
72 return |
|
73 |
|
74 ctx1 = parents[0] |
|
75 lines = changedlines(ui, repo, ctx1, ctx, fns) |
|
76 rate[key] = [r + l for r, l in zip(rate.get(key, (0, 0)), lines)] |
|
77 |
|
78 state['count'] += 1 |
|
79 ui.progress(_('analyzing'), state['count'], total=len(repo)) |
|
80 |
|
81 for ctx in cmdutil.walkchangerevs(repo, m, opts, prep): |
|
82 continue |
|
83 |
|
84 ui.progress(_('analyzing'), None) |
|
85 |
|
86 return rate |
|
87 |
|
88 |
|
89 def churn(ui, repo, *pats, **opts): |
|
90 '''histogram of changes to the repository |
|
91 |
|
92 This command will display a histogram representing the number |
|
93 of changed lines or revisions, grouped according to the given |
|
94 template. The default template will group changes by author. |
|
95 The --dateformat option may be used to group the results by |
|
96 date instead. |
|
97 |
|
98 Statistics are based on the number of changed lines, or |
|
99 alternatively the number of matching revisions if the |
|
100 --changesets option is specified. |
|
101 |
|
102 Examples:: |
|
103 |
|
104 # display count of changed lines for every committer |
|
105 hg churn -t '{author|email}' |
|
106 |
|
107 # display daily activity graph |
|
108 hg churn -f '%H' -s -c |
|
109 |
|
110 # display activity of developers by month |
|
111 hg churn -f '%Y-%m' -s -c |
|
112 |
|
113 # display count of lines changed in every year |
|
114 hg churn -f '%Y' -s |
|
115 |
|
116 It is possible to map alternate email addresses to a main address |
|
117 by providing a file using the following format:: |
|
118 |
|
119 <alias email> = <actual email> |
|
120 |
|
121 Such a file may be specified with the --aliases option, otherwise |
|
122 a .hgchurn file will be looked for in the working directory root. |
|
123 ''' |
|
124 def pad(s, l): |
|
125 return (s + " " * l)[:l] |
|
126 |
|
127 amap = {} |
|
128 aliases = opts.get('aliases') |
|
129 if not aliases and os.path.exists(repo.wjoin('.hgchurn')): |
|
130 aliases = repo.wjoin('.hgchurn') |
|
131 if aliases: |
|
132 for l in open(aliases, "r"): |
|
133 try: |
|
134 alias, actual = l.split('=' in l and '=' or None, 1) |
|
135 amap[alias.strip()] = actual.strip() |
|
136 except ValueError: |
|
137 l = l.strip() |
|
138 if l: |
|
139 ui.warn(_("skipping malformed alias: %s\n" % l)) |
|
140 continue |
|
141 |
|
142 rate = countrate(ui, repo, amap, *pats, **opts).items() |
|
143 if not rate: |
|
144 return |
|
145 |
|
146 sortkey = ((not opts.get('sort')) and (lambda x: -sum(x[1])) or None) |
|
147 rate.sort(key=sortkey) |
|
148 |
|
149 # Be careful not to have a zero maxcount (issue833) |
|
150 maxcount = float(max(sum(v) for k, v in rate)) or 1.0 |
|
151 maxname = max(len(k) for k, v in rate) |
|
152 |
|
153 ttywidth = ui.termwidth() |
|
154 ui.debug("assuming %i character terminal\n" % ttywidth) |
|
155 width = ttywidth - maxname - 2 - 2 - 2 |
|
156 |
|
157 if opts.get('diffstat'): |
|
158 width -= 15 |
|
159 def format(name, diffstat): |
|
160 added, removed = diffstat |
|
161 return "%s %15s %s%s\n" % (pad(name, maxname), |
|
162 '+%d/-%d' % (added, removed), |
|
163 ui.label('+' * charnum(added), |
|
164 'diffstat.inserted'), |
|
165 ui.label('-' * charnum(removed), |
|
166 'diffstat.deleted')) |
|
167 else: |
|
168 width -= 6 |
|
169 def format(name, count): |
|
170 return "%s %6d %s\n" % (pad(name, maxname), sum(count), |
|
171 '*' * charnum(sum(count))) |
|
172 |
|
173 def charnum(count): |
|
174 return int(round(count * width / maxcount)) |
|
175 |
|
176 for name, count in rate: |
|
177 ui.write(format(name, count)) |
|
178 |
|
179 |
|
180 cmdtable = { |
|
181 "churn": |
|
182 (churn, |
|
183 [('r', 'rev', [], |
|
184 _('count rate for the specified revision or range'), _('REV')), |
|
185 ('d', 'date', '', |
|
186 _('count rate for revisions matching date spec'), _('DATE')), |
|
187 ('t', 'template', '{author|email}', |
|
188 _('template to group changesets'), _('TEMPLATE')), |
|
189 ('f', 'dateformat', '', |
|
190 _('strftime-compatible format for grouping by date'), _('FORMAT')), |
|
191 ('c', 'changesets', False, _('count rate by number of changesets')), |
|
192 ('s', 'sort', False, _('sort by key (default: sort by count)')), |
|
193 ('', 'diffstat', False, _('display added/removed lines separately')), |
|
194 ('', 'aliases', '', |
|
195 _('file with email aliases'), _('FILE')), |
|
196 ] + commands.walkopts, |
|
197 _("hg churn [-d DATE] [-r REV] [--aliases FILE] [FILE]")), |
|
198 } |