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