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