/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar
0.147.1 by John Arbash Meinel
Improve the committer matcher tremendously.
1
# Copyright (C) 2006-2010 Canonical Ltd
0.140.26 by Jelmer Vernooij
Add copyright headers.
2
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
7
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
# GNU General Public License for more details.
12
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
0.140.1 by John Arbash Meinel
A simple plugin for generating author statistics, may grow into more.
16
"""A Simple bzr plugin to generate statistics about the history."""
17
0.143.1 by John Arbash Meinel
Make a lot of imports lazy since they may not actually be used.
18
from bzrlib.lazy_import import lazy_import
19
lazy_import(globals(), """
20
from bzrlib import (
21
    branch,
22
    commands,
23
    config,
24
    errors,
0.140.18 by Jelmer Vernooij
Add credits command, test classify code by default, add comments to classify code.
25
    option,
0.147.1 by John Arbash Meinel
Improve the committer matcher tremendously.
26
    trace,
0.143.1 by John Arbash Meinel
Make a lot of imports lazy since they may not actually be used.
27
    tsort,
0.144.1 by Wesley J. Landaker
Added ui to bzrlib lazy imports.
28
    ui,
0.143.1 by John Arbash Meinel
Make a lot of imports lazy since they may not actually be used.
29
    workingtree,
30
    )
0.140.18 by Jelmer Vernooij
Add credits command, test classify code by default, add comments to classify code.
31
from bzrlib.plugins.stats.classify import classify_delta
0.140.36 by John Arbash Meinel
Clean up the test suite infrastructure, add a version tuple
32
""")
33
34
from bzrlib import _format_version_tuple
0.140.18 by Jelmer Vernooij
Add credits command, test classify code by default, add comments to classify code.
35
from itertools import izip
0.140.36 by John Arbash Meinel
Clean up the test suite infrastructure, add a version tuple
36
37
version_info = (0, 1, 0, 'dev', 0)
38
__version__ = _format_version_tuple(version_info)
0.140.3 by John Arbash Meinel
Updated to combine by author name, as well as by email address, and report on multiple names/addresses
39
40
0.147.1 by John Arbash Meinel
Improve the committer matcher tremendously.
41
def collapse_by_person(revisions, canonical_committer):
0.140.16 by Jelmer Vernooij
Rename collapse_by_author -> collapse_by_person since author has an unambigous meaning
42
    """The committers list is sorted by email, fix it up by person.
0.140.3 by John Arbash Meinel
Updated to combine by author name, as well as by email address, and report on multiple names/addresses
43
44
    Some people commit with a similar username, but different email
45
    address. Which makes it hard to sort out when they have multiple
46
    entries. Email is actually more stable, though, since people
47
    frequently forget to set their name properly.
48
49
    So take the most common username for each email address, and
50
    combine them into one new list.
51
    """
0.147.1 by John Arbash Meinel
Improve the committer matcher tremendously.
52
    # Map from canonical committer to
53
    # {committer: ([rev_list], {email: count}, {fname:count})}
54
    committer_to_info = {}
55
    for rev in revisions:
56
        authors = rev.get_apparent_authors()
57
        for author in authors:
58
            username, email = config.parse_username(author)
59
            canon_author = canonical_committer[(username, email)]
60
            info = committer_to_info.setdefault(canon_author, ([], {}, {}))
61
            info[0].append(rev)
62
            info[1][email] = info[1].setdefault(email, 0) + 1
63
            info[2][username] = info[2].setdefault(username, 0) + 1
64
    res = [(len(revs), revs, emails, fnames)
65
           for revs, emails, fnames in committer_to_info.itervalues()]
66
    res.sort(reverse=True)
67
    return res
68
69
70
def collapse_email_and_users(email_users, combo_count):
71
    """Combine the mapping of User Name to email and email to User Name.
72
73
    If a given User Name is used for multiple emails, try to map it all to one
74
    entry.
75
    """
76
    id_to_combos = {}
77
    username_to_id = {}
78
    email_to_id = {}
79
    id_counter = 0
80
81
    def collapse_ids(old_id, new_id, new_combos):
82
        old_combos = id_to_combos.pop(old_id)
83
        new_combos.update(old_combos)
84
        for old_user, old_email in old_combos:
85
            if (old_user and old_user != user):
0.140.35 by John Arbash Meinel
Merge Lukas's extra tests, update for the new code.
86
                low_old_user = old_user.lower()
87
                old_user_id = username_to_id[low_old_user]
0.147.1 by John Arbash Meinel
Improve the committer matcher tremendously.
88
                assert old_user_id in (old_id, new_id)
0.140.35 by John Arbash Meinel
Merge Lukas's extra tests, update for the new code.
89
                username_to_id[low_old_user] = new_id
0.147.1 by John Arbash Meinel
Improve the committer matcher tremendously.
90
            if (old_email and old_email != email):
91
                old_email_id = email_to_id[old_email]
92
                assert old_email_id in (old_id, new_id)
93
                email_to_id[old_email] = cur_id
94
    for email, usernames in email_users.iteritems():
95
        assert email not in email_to_id
0.147.2 by John Arbash Meinel
Handle the case where we have a user but no email.
96
        if not email:
97
            # We use a different algorithm for usernames that have no email
98
            # address, we just try to match by username, and not at all by
99
            # email
100
            for user in usernames:
101
                if not user:
102
                    continue # The mysterious ('', '') user
0.140.35 by John Arbash Meinel
Merge Lukas's extra tests, update for the new code.
103
                # When mapping, use case-insensitive names
104
                low_user = user.lower()
105
                user_id = username_to_id.get(low_user)
0.147.2 by John Arbash Meinel
Handle the case where we have a user but no email.
106
                if user_id is None:
107
                    id_counter += 1
108
                    user_id = id_counter
0.140.35 by John Arbash Meinel
Merge Lukas's extra tests, update for the new code.
109
                    username_to_id[low_user] = user_id
0.147.2 by John Arbash Meinel
Handle the case where we have a user but no email.
110
                    id_to_combos[user_id] = id_combos = set()
111
                else:
0.140.35 by John Arbash Meinel
Merge Lukas's extra tests, update for the new code.
112
                    id_combos = id_to_combos[user_id]
0.147.2 by John Arbash Meinel
Handle the case where we have a user but no email.
113
                id_combos.add((user, email))
114
            continue
115
0.147.1 by John Arbash Meinel
Improve the committer matcher tremendously.
116
        id_counter += 1
117
        cur_id = id_counter
118
        id_to_combos[cur_id] = id_combos = set()
0.147.2 by John Arbash Meinel
Handle the case where we have a user but no email.
119
        email_to_id[email] = cur_id
0.147.1 by John Arbash Meinel
Improve the committer matcher tremendously.
120
121
        for user in usernames:
122
            combo = (user, email)
123
            id_combos.add(combo)
0.147.2 by John Arbash Meinel
Handle the case where we have a user but no email.
124
            if not user:
125
                # We don't match on empty usernames
0.147.1 by John Arbash Meinel
Improve the committer matcher tremendously.
126
                continue
0.140.35 by John Arbash Meinel
Merge Lukas's extra tests, update for the new code.
127
            low_user = user.lower()
128
            user_id = username_to_id.get(low_user)
0.147.1 by John Arbash Meinel
Improve the committer matcher tremendously.
129
            if user_id is not None:
130
                # This UserName was matched to an cur_id
131
                if user_id != cur_id:
132
                    # And it is a different identity than the current email
133
                    collapse_ids(user_id, cur_id, id_combos)
0.140.35 by John Arbash Meinel
Merge Lukas's extra tests, update for the new code.
134
            username_to_id[low_user] = cur_id
0.147.1 by John Arbash Meinel
Improve the committer matcher tremendously.
135
    combo_to_best_combo = {}
136
    for cur_id, combos in id_to_combos.iteritems():
137
        best_combo = sorted(combos,
138
                            key=lambda x:combo_count[x],
139
                            reverse=True)[0]
140
        for combo in combos:
141
            combo_to_best_combo[combo] = best_combo
142
    return combo_to_best_combo
143
144
145
def get_revisions_and_committers(a_repo, revids):
146
    """Get the Revision information, and the best-match for committer."""
147
148
    email_users = {} # user@email.com => User Name
149
    combo_count = {}
0.144.1 by Wesley J. Landaker
Added ui to bzrlib lazy imports.
150
    pb = ui.ui_factory.nested_progress_bar()
0.142.2 by Jelmer Vernooij
Split out functionality that sorts revids by commmitter.
151
    try:
0.147.1 by John Arbash Meinel
Improve the committer matcher tremendously.
152
        trace.note('getting revisions')
0.142.2 by Jelmer Vernooij
Split out functionality that sorts revids by commmitter.
153
        revisions = a_repo.get_revisions(revids)
154
        for count, rev in enumerate(revisions):
155
            pb.update('checking', count, len(revids))
0.140.29 by Jelmer Vernooij
Remove some uses of get_apparent_author.
156
            for author in rev.get_apparent_authors():
0.147.1 by John Arbash Meinel
Improve the committer matcher tremendously.
157
                # XXX: There is a chance sometimes with svn imports that the
158
                #      full name and email can BOTH be blank.
159
                username, email = config.parse_username(author)
160
                email_users.setdefault(email, set()).add(username)
161
                combo = (username, email)
162
                combo_count[combo] = combo_count.setdefault(combo, 0) + 1
0.142.2 by Jelmer Vernooij
Split out functionality that sorts revids by commmitter.
163
    finally:
164
        pb.finished()
0.147.1 by John Arbash Meinel
Improve the committer matcher tremendously.
165
    return revisions, collapse_email_and_users(email_users, combo_count)
0.142.2 by Jelmer Vernooij
Split out functionality that sorts revids by commmitter.
166
167
0.140.6 by John Arbash Meinel
refactor in preparation for supporting 2 revision specs
168
def get_info(a_repo, revision):
169
    """Get all of the information for a particular revision"""
0.144.1 by Wesley J. Landaker
Added ui to bzrlib lazy imports.
170
    pb = ui.ui_factory.nested_progress_bar()
0.140.6 by John Arbash Meinel
refactor in preparation for supporting 2 revision specs
171
    a_repo.lock_read()
172
    try:
0.147.1 by John Arbash Meinel
Improve the committer matcher tremendously.
173
        trace.note('getting ancestry')
0.140.6 by John Arbash Meinel
refactor in preparation for supporting 2 revision specs
174
        ancestry = a_repo.get_ancestry(revision)[1:]
0.147.1 by John Arbash Meinel
Improve the committer matcher tremendously.
175
        revs, canonical_committer = get_revisions_and_committers(a_repo, ancestry)
0.140.6 by John Arbash Meinel
refactor in preparation for supporting 2 revision specs
176
    finally:
177
        a_repo.unlock()
178
        pb.finished()
179
0.147.1 by John Arbash Meinel
Improve the committer matcher tremendously.
180
    return collapse_by_person(revs, canonical_committer)
0.140.6 by John Arbash Meinel
refactor in preparation for supporting 2 revision specs
181
182
0.140.7 by John Arbash Meinel
Compute the revisions using a difference check
183
def get_diff_info(a_repo, start_rev, end_rev):
184
    """Get only the info for new revisions between the two revisions
0.150.3 by Lukáš Lalinský
Reuse sort_by_committer in get_diff_info
185
0.140.7 by John Arbash Meinel
Compute the revisions using a difference check
186
    This lets us figure out what has actually changed between 2 revisions.
187
    """
0.144.1 by Wesley J. Landaker
Added ui to bzrlib lazy imports.
188
    pb = ui.ui_factory.nested_progress_bar()
0.140.7 by John Arbash Meinel
Compute the revisions using a difference check
189
    a_repo.lock_read()
190
    try:
191
        pb.note('getting ancestry 1')
192
        start_ancestry = set(a_repo.get_ancestry(start_rev))
193
        pb.note('getting ancestry 2')
194
        ancestry = a_repo.get_ancestry(end_rev)[1:]
195
        ancestry = [rev for rev in ancestry if rev not in start_ancestry]
0.140.35 by John Arbash Meinel
Merge Lukas's extra tests, update for the new code.
196
        revs, canonical_committer = get_revisions_and_committers(a_repo, ancestry)
0.140.7 by John Arbash Meinel
Compute the revisions using a difference check
197
    finally:
198
        a_repo.unlock()
199
        pb.finished()
200
0.147.1 by John Arbash Meinel
Improve the committer matcher tremendously.
201
    return collapse_by_person(revs, canonical_committer)
0.140.7 by John Arbash Meinel
Compute the revisions using a difference check
202
0.140.16 by Jelmer Vernooij
Rename collapse_by_author -> collapse_by_person since author has an unambigous meaning
203
0.140.20 by Jelmer Vernooij
Add --show-class argument to stats command.
204
def display_info(info, to_file, gather_class_stats=None):
0.140.6 by John Arbash Meinel
refactor in preparation for supporting 2 revision specs
205
    """Write out the information"""
206
207
    for count, revs, emails, fullnames in info:
208
        # Get the most common email name
209
        sorted_emails = sorted(((count, email)
210
                               for email,count in emails.iteritems()),
211
                               reverse=True)
212
        sorted_fullnames = sorted(((count, fullname)
213
                                  for fullname,count in fullnames.iteritems()),
214
                                  reverse=True)
0.147.2 by John Arbash Meinel
Handle the case where we have a user but no email.
215
        if sorted_fullnames[0][1] == '' and sorted_emails[0][1] == '':
0.146.1 by Paul Hummer
Revisions with missing emails are no longer all attributed to the same person
216
            to_file.write('%4d %s\n'
217
                          % (count, 'Unknown'))
218
        else:
219
            to_file.write('%4d %s <%s>\n'
220
                          % (count, sorted_fullnames[0][1],
221
                             sorted_emails[0][1]))
0.140.6 by John Arbash Meinel
refactor in preparation for supporting 2 revision specs
222
        if len(sorted_fullnames) > 1:
0.147.1 by John Arbash Meinel
Improve the committer matcher tremendously.
223
            to_file.write('     Other names:\n')
224
            for count, fname in sorted_fullnames:
0.140.6 by John Arbash Meinel
refactor in preparation for supporting 2 revision specs
225
                to_file.write('     %4d ' % (count,))
226
                if fname == '':
227
                    to_file.write("''\n")
228
                else:
229
                    to_file.write("%s\n" % (fname,))
230
        if len(sorted_emails) > 1:
0.147.1 by John Arbash Meinel
Improve the committer matcher tremendously.
231
            to_file.write('     Other email addresses:\n')
0.140.6 by John Arbash Meinel
refactor in preparation for supporting 2 revision specs
232
            for count, email in sorted_emails:
233
                to_file.write('     %4d ' % (count,))
234
                if email == '':
235
                    to_file.write("''\n")
236
                else:
237
                    to_file.write("%s\n" % (email,))
0.140.20 by Jelmer Vernooij
Add --show-class argument to stats command.
238
        if gather_class_stats is not None:
239
            print '     Contributions:'
240
            classes, total = gather_class_stats(revs)
241
            for name,count in sorted(classes.items(), lambda x,y: cmp((x[1], x[0]), (y[1], y[0]))):
0.140.24 by Jelmer Vernooij
Remove 2.5ism.
242
                if name is None:
243
                    name = "Unknown"
244
                to_file.write("     %4.0f%% %s\n" % ((float(count) / total) * 100.0, name))
0.140.6 by John Arbash Meinel
refactor in preparation for supporting 2 revision specs
245
246
0.140.14 by Jelmer Vernooij
Merge upstream.
247
class cmd_committer_statistics(commands.Command):
0.140.1 by John Arbash Meinel
A simple plugin for generating author statistics, may grow into more.
248
    """Generate statistics for LOCATION."""
249
0.140.12 by Jelmer Vernooij
Change name to committer-stats, to allow for other sorts of stats too.
250
    aliases = ['stats', 'committer-stats']
0.140.1 by John Arbash Meinel
A simple plugin for generating author statistics, may grow into more.
251
    takes_args = ['location?']
0.140.20 by Jelmer Vernooij
Add --show-class argument to stats command.
252
    takes_options = ['revision', 
0.148.1 by Petr Viktorin
Typo fix
253
            option.Option('show-class', help="Show the class of contributions.")]
0.140.1 by John Arbash Meinel
A simple plugin for generating author statistics, may grow into more.
254
0.140.3 by John Arbash Meinel
Updated to combine by author name, as well as by email address, and report on multiple names/addresses
255
    encoding_type = 'replace'
256
0.140.20 by Jelmer Vernooij
Add --show-class argument to stats command.
257
    def run(self, location='.', revision=None, show_class=False):
0.140.6 by John Arbash Meinel
refactor in preparation for supporting 2 revision specs
258
        alternate_rev = None
0.140.1 by John Arbash Meinel
A simple plugin for generating author statistics, may grow into more.
259
        try:
0.143.1 by John Arbash Meinel
Make a lot of imports lazy since they may not actually be used.
260
            wt = workingtree.WorkingTree.open_containing(location)[0]
0.140.1 by John Arbash Meinel
A simple plugin for generating author statistics, may grow into more.
261
        except errors.NoWorkingTree:
0.143.1 by John Arbash Meinel
Make a lot of imports lazy since they may not actually be used.
262
            a_branch = branch.Branch.open(location)
0.140.6 by John Arbash Meinel
refactor in preparation for supporting 2 revision specs
263
            last_rev = a_branch.last_revision()
0.140.1 by John Arbash Meinel
A simple plugin for generating author statistics, may grow into more.
264
        else:
0.140.6 by John Arbash Meinel
refactor in preparation for supporting 2 revision specs
265
            a_branch = wt.branch
0.140.1 by John Arbash Meinel
A simple plugin for generating author statistics, may grow into more.
266
            last_rev = wt.last_revision()
0.140.7 by John Arbash Meinel
Compute the revisions using a difference check
267
0.140.8 by John Arbash Meinel
Allow branch: to work, which needs a write lock
268
        if revision is not None:
269
            last_rev = revision[0].in_history(a_branch).rev_id
270
            if len(revision) > 1:
271
                alternate_rev = revision[1].in_history(a_branch).rev_id
272
0.140.7 by John Arbash Meinel
Compute the revisions using a difference check
273
        a_branch.lock_read()
274
        try:
275
            if alternate_rev:
276
                info = get_diff_info(a_branch.repository, last_rev,
277
                                     alternate_rev)
278
            else:
279
                info = get_info(a_branch.repository, last_rev)
280
        finally:
281
            a_branch.unlock()
0.140.25 by Jelmer Vernooij
Merge support for Python2.4.
282
        if show_class:
283
            def fetch_class_stats(revs):
284
                return gather_class_stats(a_branch.repository, revs)
285
        else:
286
            fetch_class_stats = None
0.145.1 by Russ Brown
Made to work with python 2.4
287
        display_info(info, self.outf, fetch_class_stats)
0.140.1 by John Arbash Meinel
A simple plugin for generating author statistics, may grow into more.
288
289
0.140.14 by Jelmer Vernooij
Merge upstream.
290
commands.register_command(cmd_committer_statistics)
0.143.1 by John Arbash Meinel
Make a lot of imports lazy since they may not actually be used.
291
292
293
class cmd_ancestor_growth(commands.Command):
0.140.4 by John Arbash Meinel
added ancestry_growth to generate a csv of ancestors.
294
    """Figure out the ancestor graph for LOCATION"""
295
296
    takes_args = ['location?']
297
298
    encoding_type = 'replace'
299
300
    def run(self, location='.'):
301
        try:
0.143.1 by John Arbash Meinel
Make a lot of imports lazy since they may not actually be used.
302
            wt = workingtree.WorkingTree.open_containing(location)[0]
0.140.4 by John Arbash Meinel
added ancestry_growth to generate a csv of ancestors.
303
        except errors.NoWorkingTree:
0.143.1 by John Arbash Meinel
Make a lot of imports lazy since they may not actually be used.
304
            a_branch = branch.Branch.open(location)
0.140.6 by John Arbash Meinel
refactor in preparation for supporting 2 revision specs
305
            last_rev = a_branch.last_revision()
0.140.4 by John Arbash Meinel
added ancestry_growth to generate a csv of ancestors.
306
        else:
0.140.6 by John Arbash Meinel
refactor in preparation for supporting 2 revision specs
307
            a_branch = wt.branch
0.140.4 by John Arbash Meinel
added ancestry_growth to generate a csv of ancestors.
308
            last_rev = wt.last_revision()
309
0.140.6 by John Arbash Meinel
refactor in preparation for supporting 2 revision specs
310
        a_branch.lock_read()
0.140.4 by John Arbash Meinel
added ancestry_growth to generate a csv of ancestors.
311
        try:
0.140.6 by John Arbash Meinel
refactor in preparation for supporting 2 revision specs
312
            graph = a_branch.repository.get_revision_graph(last_rev)
0.140.4 by John Arbash Meinel
added ancestry_growth to generate a csv of ancestors.
313
        finally:
0.140.6 by John Arbash Meinel
refactor in preparation for supporting 2 revision specs
314
            a_branch.unlock()
0.140.4 by John Arbash Meinel
added ancestry_growth to generate a csv of ancestors.
315
316
        revno = 0
317
        cur_parents = 0
318
        sorted_graph = tsort.merge_sort(graph.iteritems(), last_rev)
319
        for num, node_name, depth, isend in reversed(sorted_graph):
320
            cur_parents += 1
321
            if depth == 0:
322
                revno += 1
323
                self.outf.write('%4d, %4d\n' % (revno, cur_parents))
324
325
0.143.1 by John Arbash Meinel
Make a lot of imports lazy since they may not actually be used.
326
commands.register_command(cmd_ancestor_growth)
0.141.1 by Jelmer Vernooij
Add some simple tests for extract_fullname.
327
0.140.10 by John Arbash Meinel
Minor whitespace cleanup
328
0.140.18 by Jelmer Vernooij
Add credits command, test classify code by default, add comments to classify code.
329
def gather_class_stats(repository, revs):
330
    ret = {}
331
    total = 0
332
    pb = ui.ui_factory.nested_progress_bar()
333
    try:
334
        repository.lock_read()
335
        try:
336
            i = 0
337
            for delta in repository.get_deltas_for_revisions(revs):
338
                pb.update("classifying commits", i, len(revs))
339
                for c in classify_delta(delta):
340
                    if not c in ret:
341
                        ret[c] = 0
342
                    ret[c] += 1
343
                    total += 1
344
                i += 1
345
        finally:
346
            repository.unlock()
347
    finally:
348
        pb.finished()
349
    return ret, total
350
351
0.140.37 by John Arbash Meinel
Update display_credits to handle unprintable chars.
352
def display_credits(credits, to_file):
0.140.18 by Jelmer Vernooij
Add credits command, test classify code by default, add comments to classify code.
353
    (coders, documenters, artists, translators) = credits
354
    def print_section(name, lst):
355
        if len(lst) == 0:
356
            return
0.140.37 by John Arbash Meinel
Update display_credits to handle unprintable chars.
357
        to_file.write("%s:\n" % name)
0.140.18 by Jelmer Vernooij
Add credits command, test classify code by default, add comments to classify code.
358
        for name in lst:
0.140.37 by John Arbash Meinel
Update display_credits to handle unprintable chars.
359
            to_file.write("%s\n" % name)
360
        to_file.write('\n')
0.140.18 by Jelmer Vernooij
Add credits command, test classify code by default, add comments to classify code.
361
    print_section("Code", coders)
362
    print_section("Documentation", documenters)
363
    print_section("Art", artists)
364
    print_section("Translations", translators)
365
366
367
def find_credits(repository, revid):
368
    """Find the credits of the contributors to a revision.
369
370
    :return: tuple with (authors, documenters, artists, translators)
371
    """
372
    ret = {"documentation": {},
373
           "code": {},
374
           "art": {},
375
           "translation": {},
376
           None: {}
377
           }
378
    repository.lock_read()
379
    try:
380
        ancestry = filter(lambda x: x is not None, repository.get_ancestry(revid))
0.140.23 by Jelmer Vernooij
Add another progress bar.
381
        revs = repository.get_revisions(ancestry)
0.140.18 by Jelmer Vernooij
Add credits command, test classify code by default, add comments to classify code.
382
        pb = ui.ui_factory.nested_progress_bar()
383
        try:
0.140.36 by John Arbash Meinel
Clean up the test suite infrastructure, add a version tuple
384
            iterator = izip(revs, repository.get_deltas_for_revisions(revs))
385
            for i, (rev,delta) in enumerate(iterator):
0.140.23 by Jelmer Vernooij
Add another progress bar.
386
                pb.update("analysing revisions", i, len(revs))
0.140.18 by Jelmer Vernooij
Add credits command, test classify code by default, add comments to classify code.
387
                # Don't count merges
388
                if len(rev.parent_ids) > 1:
389
                    continue
390
                for c in set(classify_delta(delta)):
0.140.29 by Jelmer Vernooij
Remove some uses of get_apparent_author.
391
                    for author in rev.get_apparent_authors():
392
                        if not author in ret[c]:
393
                            ret[c][author] = 0
394
                        ret[c][author] += 1
0.140.18 by Jelmer Vernooij
Add credits command, test classify code by default, add comments to classify code.
395
        finally:
396
            pb.finished()
397
    finally:
398
        repository.unlock()
399
    def sort_class(name):
0.140.19 by Jelmer Vernooij
List contributors with more contributions first.
400
        return map(lambda (x,y): x, 
401
               sorted(ret[name].items(), lambda x,y: cmp((x[1], x[0]), (y[1], y[0])), reverse=True))
0.140.18 by Jelmer Vernooij
Add credits command, test classify code by default, add comments to classify code.
402
    return (sort_class("code"), sort_class("documentation"), sort_class("art"), sort_class("translation"))
403
404
405
class cmd_credits(commands.Command):
406
    """Determine credits for LOCATION."""
407
408
    takes_args = ['location?']
409
    takes_options = ['revision']
410
411
    encoding_type = 'replace'
412
413
    def run(self, location='.', revision=None):
414
        try:
415
            wt = workingtree.WorkingTree.open_containing(location)[0]
416
        except errors.NoWorkingTree:
417
            a_branch = branch.Branch.open(location)
418
            last_rev = a_branch.last_revision()
419
        else:
420
            a_branch = wt.branch
421
            last_rev = wt.last_revision()
422
423
        if revision is not None:
424
            last_rev = revision[0].in_history(a_branch).rev_id
425
426
        a_branch.lock_read()
427
        try:
428
            credits = find_credits(a_branch.repository, last_rev)
0.140.37 by John Arbash Meinel
Update display_credits to handle unprintable chars.
429
            display_credits(credits, self.outf)
0.140.18 by Jelmer Vernooij
Add credits command, test classify code by default, add comments to classify code.
430
        finally:
431
            a_branch.unlock()
432
433
434
commands.register_command(cmd_credits)
435
436
0.140.36 by John Arbash Meinel
Clean up the test suite infrastructure, add a version tuple
437
def load_tests(basic_tests, module, loader):
438
    testmod_names = [__name__ + '.' + x for x in [
439
        'test_classify',
440
        'test_stats',
441
        ]]
442
    suite = loader.suiteClass()
443
    suite.addTest(loader.loadTestsFromModuleNames(testmod_names))
0.141.1 by Jelmer Vernooij
Add some simple tests for extract_fullname.
444
    return suite
445