Mercurial > hg
comparison hgext/churn.py @ 6348:f8feaa665319
Make churn an official extension
author | Patrick Mezard <pmezard@gmail.com> |
---|---|
date | Sat, 22 Mar 2008 18:01:46 +0100 |
parents | contrib/churn.py@bab6c8f2bb1a |
children | ecde0baee570 |
comparison
equal
deleted
inserted
replaced
6347:3b42f7ac6916 | 6348:f8feaa665319 |
---|---|
1 # churn.py - create a graph showing who changed the most lines | |
2 # | |
3 # Copyright 2006 Josef "Jeff" Sipek <jeffpc@josefsipek.net> | |
4 # | |
5 # This software may be used and distributed according to the terms | |
6 # of the GNU General Public License, incorporated herein by reference. | |
7 # | |
8 # | |
9 # Aliases map file format is simple one alias per line in the following | |
10 # format: | |
11 # | |
12 # <alias email> <actual email> | |
13 | |
14 from mercurial.i18n import gettext as _ | |
15 from mercurial import mdiff, cmdutil, util, node | |
16 import os, sys | |
17 | |
18 def get_tty_width(): | |
19 if 'COLUMNS' in os.environ: | |
20 try: | |
21 return int(os.environ['COLUMNS']) | |
22 except ValueError: | |
23 pass | |
24 try: | |
25 import termios, array, fcntl | |
26 for dev in (sys.stdout, sys.stdin): | |
27 try: | |
28 fd = dev.fileno() | |
29 if not os.isatty(fd): | |
30 continue | |
31 arri = fcntl.ioctl(fd, termios.TIOCGWINSZ, '\0' * 8) | |
32 return array.array('h', arri)[1] | |
33 except ValueError: | |
34 pass | |
35 except ImportError: | |
36 pass | |
37 return 80 | |
38 | |
39 def __gather(ui, repo, node1, node2): | |
40 def dirtywork(f, mmap1, mmap2): | |
41 lines = 0 | |
42 | |
43 to = mmap1 and repo.file(f).read(mmap1[f]) or None | |
44 tn = mmap2 and repo.file(f).read(mmap2[f]) or None | |
45 | |
46 diff = mdiff.unidiff(to, "", tn, "", f, f).split("\n") | |
47 | |
48 for line in diff: | |
49 if not line: | |
50 continue # skip EOF | |
51 if line.startswith(" "): | |
52 continue # context line | |
53 if line.startswith("--- ") or line.startswith("+++ "): | |
54 continue # begining of diff | |
55 if line.startswith("@@ "): | |
56 continue # info line | |
57 | |
58 # changed lines | |
59 lines += 1 | |
60 | |
61 return lines | |
62 | |
63 ## | |
64 | |
65 lines = 0 | |
66 | |
67 changes = repo.status(node1, node2, None, util.always)[:5] | |
68 | |
69 modified, added, removed, deleted, unknown = changes | |
70 | |
71 who = repo.changelog.read(node2)[1] | |
72 who = util.email(who) # get the email of the person | |
73 | |
74 mmap1 = repo.manifest.read(repo.changelog.read(node1)[0]) | |
75 mmap2 = repo.manifest.read(repo.changelog.read(node2)[0]) | |
76 for f in modified: | |
77 lines += dirtywork(f, mmap1, mmap2) | |
78 | |
79 for f in added: | |
80 lines += dirtywork(f, None, mmap2) | |
81 | |
82 for f in removed: | |
83 lines += dirtywork(f, mmap1, None) | |
84 | |
85 for f in deleted: | |
86 lines += dirtywork(f, mmap1, mmap2) | |
87 | |
88 for f in unknown: | |
89 lines += dirtywork(f, mmap1, mmap2) | |
90 | |
91 return (who, lines) | |
92 | |
93 def gather_stats(ui, repo, amap, revs=None, progress=False): | |
94 stats = {} | |
95 | |
96 cl = repo.changelog | |
97 | |
98 if not revs: | |
99 revs = range(0, cl.count()) | |
100 | |
101 nr_revs = len(revs) | |
102 cur_rev = 0 | |
103 | |
104 for rev in revs: | |
105 cur_rev += 1 # next revision | |
106 | |
107 node2 = cl.node(rev) | |
108 node1 = cl.parents(node2)[0] | |
109 | |
110 if cl.parents(node2)[1] != node.nullid: | |
111 ui.note(_('Revision %d is a merge, ignoring...\n') % (rev,)) | |
112 continue | |
113 | |
114 who, lines = __gather(ui, repo, node1, node2) | |
115 | |
116 # remap the owner if possible | |
117 if who in amap: | |
118 ui.note("using '%s' alias for '%s'\n" % (amap[who], who)) | |
119 who = amap[who] | |
120 | |
121 if not who in stats: | |
122 stats[who] = 0 | |
123 stats[who] += lines | |
124 | |
125 ui.note("rev %d: %d lines by %s\n" % (rev, lines, who)) | |
126 | |
127 if progress: | |
128 nr_revs = max(nr_revs, 1) | |
129 if int(100.0*(cur_rev - 1)/nr_revs) < int(100.0*cur_rev/nr_revs): | |
130 ui.write("\rGenerating stats: %d%%" % (int(100.0*cur_rev/nr_revs),)) | |
131 sys.stdout.flush() | |
132 | |
133 if progress: | |
134 ui.write("\r") | |
135 sys.stdout.flush() | |
136 | |
137 return stats | |
138 | |
139 def churn(ui, repo, **opts): | |
140 "Graphs the number of lines changed" | |
141 | |
142 def pad(s, l): | |
143 if len(s) < l: | |
144 return s + " " * (l-len(s)) | |
145 return s[0:l] | |
146 | |
147 def graph(n, maximum, width, char): | |
148 maximum = max(1, maximum) | |
149 n = int(n * width / float(maximum)) | |
150 | |
151 return char * (n) | |
152 | |
153 def get_aliases(f): | |
154 aliases = {} | |
155 | |
156 for l in f.readlines(): | |
157 l = l.strip() | |
158 alias, actual = l.split(" ") | |
159 aliases[alias] = actual | |
160 | |
161 return aliases | |
162 | |
163 amap = {} | |
164 aliases = opts.get('aliases') | |
165 if aliases: | |
166 try: | |
167 f = open(aliases,"r") | |
168 except OSError, e: | |
169 print "Error: " + e | |
170 return | |
171 | |
172 amap = get_aliases(f) | |
173 f.close() | |
174 | |
175 revs = [int(r) for r in cmdutil.revrange(repo, opts['rev'])] | |
176 revs.sort() | |
177 stats = gather_stats(ui, repo, amap, revs, opts.get('progress')) | |
178 | |
179 # make a list of tuples (name, lines) and sort it in descending order | |
180 ordered = stats.items() | |
181 if not ordered: | |
182 return | |
183 ordered.sort(lambda x, y: cmp(y[1], x[1])) | |
184 max_churn = ordered[0][1] | |
185 | |
186 tty_width = get_tty_width() | |
187 ui.note(_("assuming %i character terminal\n") % tty_width) | |
188 tty_width -= 1 | |
189 | |
190 max_user_width = max([len(user) for user, churn in ordered]) | |
191 | |
192 graph_width = tty_width - max_user_width - 1 - 6 - 2 - 2 | |
193 | |
194 for user, churn in ordered: | |
195 print "%s %6d %s" % (pad(user, max_user_width), | |
196 churn, | |
197 graph(churn, max_churn, graph_width, '*')) | |
198 | |
199 cmdtable = { | |
200 "churn": | |
201 (churn, | |
202 [('r', 'rev', [], _('limit statistics to the specified revisions')), | |
203 ('', 'aliases', '', _('file with email aliases')), | |
204 ('', 'progress', None, _('show progress'))], | |
205 'hg churn [-r revision range] [-a file] [--progress]'), | |
206 } |