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