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