/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
7211.8.2 by Jelmer Vernooij
Use operator.
20
import operator
21
6645.1.1 by Jelmer Vernooij
Bundle the stats plugin.
22
from ... import (
0.143.1 by John Arbash Meinel
Make a lot of imports lazy since they may not actually be used.
23
    branch,
24
    commands,
25
    config,
26
    errors,
0.140.18 by Jelmer Vernooij
Add credits command, test classify code by default, add comments to classify code.
27
    option,
0.147.1 by John Arbash Meinel
Improve the committer matcher tremendously.
28
    trace,
0.143.1 by John Arbash Meinel
Make a lot of imports lazy since they may not actually be used.
29
    tsort,
0.144.1 by Wesley J. Landaker
Added ui to bzrlib lazy imports.
30
    ui,
0.143.1 by John Arbash Meinel
Make a lot of imports lazy since they may not actually be used.
31
    workingtree,
32
    )
6645.1.1 by Jelmer Vernooij
Bundle the stats plugin.
33
from ...revision import NULL_REVISION
34
from .classify import classify_delta
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)
6656.1.1 by Martin
Apply 2to3 dict fixer and clean up resulting mess using view helpers
63
           for revs, emails, fnames in committer_to_info.values()]
7211.8.2 by Jelmer Vernooij
Use operator.
64
    res.sort(reverse=True, key=operator.itemgetter(0))
0.147.1 by John Arbash Meinel
Improve the committer matcher tremendously.
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
6656.1.1 by Martin
Apply 2to3 dict fixer and clean up resulting mess using view helpers
92
    for email, usernames in email_users.items():
0.147.1 by John Arbash Meinel
Improve the committer matcher tremendously.
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:
7143.15.2 by Jelmer Vernooij
Run autopep8.
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 = {}
6656.1.1 by Martin
Apply 2to3 dict fixer and clean up resulting mess using view helpers
134
    for cur_id, combos in id_to_combos.items():
0.147.1 by John Arbash Meinel
Improve the committer matcher tremendously.
135
        best_combo = sorted(combos,
7143.15.2 by Jelmer Vernooij
Run autopep8.
136
                            key=lambda x: combo_count[x],
0.147.1 by John Arbash Meinel
Improve the committer matcher tremendously.
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
7143.15.2 by Jelmer Vernooij
Run autopep8.
146
    email_users = {}  # user@email.com => User Name
0.147.1 by John Arbash Meinel
Improve the committer matcher tremendously.
147
    combo_count = {}
6861.4.1 by Jelmer Vernooij
Make progress bars context managers.
148
    with ui.ui_factory.nested_progress_bar() as pb:
0.147.1 by John Arbash Meinel
Improve the committer matcher tremendously.
149
        trace.note('getting revisions')
6883.6.4 by Jelmer Vernooij
Fix tests.
150
        revisions = list(a_repo.iter_revisions(revids))
151
        for count, (revid, rev) in enumerate(revisions):
0.142.2 by Jelmer Vernooij
Split out functionality that sorts revids by commmitter.
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
6883.6.4 by Jelmer Vernooij
Fix tests.
160
    return ((rev for (revid, rev) in revisions),
6883.6.2 by Jelmer Vernooij
Use itertools magic.
161
            collapse_email_and_users(email_users, combo_count))
0.142.2 by Jelmer Vernooij
Split out functionality that sorts revids by commmitter.
162
163
0.140.6 by John Arbash Meinel
refactor in preparation for supporting 2 revision specs
164
def get_info(a_repo, revision):
165
    """Get all of the information for a particular revision"""
6861.4.1 by Jelmer Vernooij
Make progress bars context managers.
166
    with ui.ui_factory.nested_progress_bar() as pb, a_repo.lock_read():
0.147.1 by John Arbash Meinel
Improve the committer matcher tremendously.
167
        trace.note('getting ancestry')
0.151.2 by Jelmer Vernooij
Remove another use of get_ancestry.
168
        graph = a_repo.get_graph()
169
        ancestry = [
170
            r for (r, ps) in graph.iter_ancestry([revision])
171
            if ps is not None and r != NULL_REVISION]
7143.15.2 by Jelmer Vernooij
Run autopep8.
172
        revs, canonical_committer = get_revisions_and_committers(
173
            a_repo, ancestry)
0.140.6 by John Arbash Meinel
refactor in preparation for supporting 2 revision specs
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
    """
6861.4.1 by Jelmer Vernooij
Make progress bars context managers.
183
    with ui.ui_factory.nested_progress_bar() as pb, a_repo.lock_read():
0.151.3 by Jelmer Vernooij
Avoid use of get_ancestry.
184
        graph = a_repo.get_graph()
185
        trace.note('getting ancestry diff')
186
        ancestry = graph.find_difference(start_rev, end_rev)[1]
7143.15.2 by Jelmer Vernooij
Run autopep8.
187
        revs, canonical_committer = get_revisions_and_committers(
188
            a_repo, ancestry)
0.140.7 by John Arbash Meinel
Compute the revisions using a difference check
189
0.147.1 by John Arbash Meinel
Improve the committer matcher tremendously.
190
    return collapse_by_person(revs, canonical_committer)
0.140.7 by John Arbash Meinel
Compute the revisions using a difference check
191
0.140.16 by Jelmer Vernooij
Rename collapse_by_author -> collapse_by_person since author has an unambigous meaning
192
0.140.20 by Jelmer Vernooij
Add --show-class argument to stats command.
193
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
194
    """Write out the information"""
195
196
    for count, revs, emails, fullnames in info:
197
        # Get the most common email name
198
        sorted_emails = sorted(((count, email)
7143.15.2 by Jelmer Vernooij
Run autopep8.
199
                                for email, count in emails.items()),
0.140.6 by John Arbash Meinel
refactor in preparation for supporting 2 revision specs
200
                               reverse=True)
201
        sorted_fullnames = sorted(((count, fullname)
7143.15.2 by Jelmer Vernooij
Run autopep8.
202
                                   for fullname, count in fullnames.items()),
0.140.6 by John Arbash Meinel
refactor in preparation for supporting 2 revision specs
203
                                  reverse=True)
0.147.2 by John Arbash Meinel
Handle the case where we have a user but no email.
204
        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
205
            to_file.write('%4d %s\n'
206
                          % (count, 'Unknown'))
207
        else:
208
            to_file.write('%4d %s <%s>\n'
209
                          % (count, sorted_fullnames[0][1],
210
                             sorted_emails[0][1]))
0.140.6 by John Arbash Meinel
refactor in preparation for supporting 2 revision specs
211
        if len(sorted_fullnames) > 1:
0.147.1 by John Arbash Meinel
Improve the committer matcher tremendously.
212
            to_file.write('     Other names:\n')
213
            for count, fname in sorted_fullnames:
0.140.6 by John Arbash Meinel
refactor in preparation for supporting 2 revision specs
214
                to_file.write('     %4d ' % (count,))
215
                if fname == '':
216
                    to_file.write("''\n")
217
                else:
218
                    to_file.write("%s\n" % (fname,))
219
        if len(sorted_emails) > 1:
0.147.1 by John Arbash Meinel
Improve the committer matcher tremendously.
220
            to_file.write('     Other email addresses:\n')
0.140.6 by John Arbash Meinel
refactor in preparation for supporting 2 revision specs
221
            for count, email in sorted_emails:
222
                to_file.write('     %4d ' % (count,))
223
                if email == '':
224
                    to_file.write("''\n")
225
                else:
226
                    to_file.write("%s\n" % (email,))
0.140.20 by Jelmer Vernooij
Add --show-class argument to stats command.
227
        if gather_class_stats is not None:
0.140.39 by Jelmer Vernooij
Eliminate print command.
228
            to_file.write('     Contributions:\n')
0.140.20 by Jelmer Vernooij
Add --show-class argument to stats command.
229
            classes, total = gather_class_stats(revs)
6656.1.1 by Martin
Apply 2to3 dict fixer and clean up resulting mess using view helpers
230
            for name, count in sorted(classes.items(), key=classify_key):
0.140.24 by Jelmer Vernooij
Remove 2.5ism.
231
                if name is None:
232
                    name = "Unknown"
7143.15.2 by Jelmer Vernooij
Run autopep8.
233
                to_file.write("     %4.0f%% %s\n" %
234
                              ((float(count) / total) * 100.0, name))
0.140.6 by John Arbash Meinel
refactor in preparation for supporting 2 revision specs
235
236
0.140.14 by Jelmer Vernooij
Merge upstream.
237
class cmd_committer_statistics(commands.Command):
0.140.1 by John Arbash Meinel
A simple plugin for generating author statistics, may grow into more.
238
    """Generate statistics for LOCATION."""
239
0.140.12 by Jelmer Vernooij
Change name to committer-stats, to allow for other sorts of stats too.
240
    aliases = ['stats', 'committer-stats']
0.140.1 by John Arbash Meinel
A simple plugin for generating author statistics, may grow into more.
241
    takes_args = ['location?']
7143.15.2 by Jelmer Vernooij
Run autopep8.
242
    takes_options = ['revision',
243
                     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.
244
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
245
    encoding_type = 'replace'
246
0.140.20 by Jelmer Vernooij
Add --show-class argument to stats command.
247
    def run(self, location='.', revision=None, show_class=False):
0.140.6 by John Arbash Meinel
refactor in preparation for supporting 2 revision specs
248
        alternate_rev = None
0.140.1 by John Arbash Meinel
A simple plugin for generating author statistics, may grow into more.
249
        try:
0.143.1 by John Arbash Meinel
Make a lot of imports lazy since they may not actually be used.
250
            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.
251
        except errors.NoWorkingTree:
0.143.1 by John Arbash Meinel
Make a lot of imports lazy since they may not actually be used.
252
            a_branch = branch.Branch.open(location)
0.140.6 by John Arbash Meinel
refactor in preparation for supporting 2 revision specs
253
            last_rev = a_branch.last_revision()
0.140.1 by John Arbash Meinel
A simple plugin for generating author statistics, may grow into more.
254
        else:
0.140.6 by John Arbash Meinel
refactor in preparation for supporting 2 revision specs
255
            a_branch = wt.branch
0.140.1 by John Arbash Meinel
A simple plugin for generating author statistics, may grow into more.
256
            last_rev = wt.last_revision()
0.140.7 by John Arbash Meinel
Compute the revisions using a difference check
257
0.140.8 by John Arbash Meinel
Allow branch: to work, which needs a write lock
258
        if revision is not None:
259
            last_rev = revision[0].in_history(a_branch).rev_id
260
            if len(revision) > 1:
261
                alternate_rev = revision[1].in_history(a_branch).rev_id
262
6754.8.4 by Jelmer Vernooij
Use new context stuff.
263
        with a_branch.lock_read():
0.140.7 by John Arbash Meinel
Compute the revisions using a difference check
264
            if alternate_rev:
265
                info = get_diff_info(a_branch.repository, last_rev,
266
                                     alternate_rev)
267
            else:
268
                info = get_info(a_branch.repository, last_rev)
0.140.25 by Jelmer Vernooij
Merge support for Python2.4.
269
        if show_class:
270
            def fetch_class_stats(revs):
271
                return gather_class_stats(a_branch.repository, revs)
272
        else:
273
            fetch_class_stats = None
0.145.1 by Russ Brown
Made to work with python 2.4
274
        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.
275
276
0.143.1 by John Arbash Meinel
Make a lot of imports lazy since they may not actually be used.
277
class cmd_ancestor_growth(commands.Command):
0.140.4 by John Arbash Meinel
added ancestry_growth to generate a csv of ancestors.
278
    """Figure out the ancestor graph for LOCATION"""
279
280
    takes_args = ['location?']
281
282
    encoding_type = 'replace'
283
6951.1.1 by Jelmer Vernooij
Hide the ancestor-growth command.
284
    hidden = True
285
0.140.4 by John Arbash Meinel
added ancestry_growth to generate a csv of ancestors.
286
    def run(self, location='.'):
287
        try:
0.143.1 by John Arbash Meinel
Make a lot of imports lazy since they may not actually be used.
288
            wt = workingtree.WorkingTree.open_containing(location)[0]
0.140.4 by John Arbash Meinel
added ancestry_growth to generate a csv of ancestors.
289
        except errors.NoWorkingTree:
0.143.1 by John Arbash Meinel
Make a lot of imports lazy since they may not actually be used.
290
            a_branch = branch.Branch.open(location)
0.140.6 by John Arbash Meinel
refactor in preparation for supporting 2 revision specs
291
            last_rev = a_branch.last_revision()
0.140.4 by John Arbash Meinel
added ancestry_growth to generate a csv of ancestors.
292
        else:
0.140.6 by John Arbash Meinel
refactor in preparation for supporting 2 revision specs
293
            a_branch = wt.branch
0.140.4 by John Arbash Meinel
added ancestry_growth to generate a csv of ancestors.
294
            last_rev = wt.last_revision()
295
6754.8.4 by Jelmer Vernooij
Use new context stuff.
296
        with a_branch.lock_read():
0.140.38 by Jelmer Vernooij
Avoid using Repository.get_revision_graph().
297
            graph = a_branch.repository.get_graph()
298
            revno = 0
299
            cur_parents = 0
300
            sorted_graph = tsort.merge_sort(graph.iter_ancestry([last_rev]),
301
                                            last_rev)
302
            for num, node_name, depth, isend in reversed(sorted_graph):
303
                cur_parents += 1
304
                if depth == 0:
305
                    revno += 1
306
                    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.
307
308
0.140.18 by Jelmer Vernooij
Add credits command, test classify code by default, add comments to classify code.
309
def gather_class_stats(repository, revs):
310
    ret = {}
311
    total = 0
6861.4.1 by Jelmer Vernooij
Make progress bars context managers.
312
    with ui.ui_factory.nested_progress_bar() as pb:
6754.8.4 by Jelmer Vernooij
Use new context stuff.
313
        with repository.lock_read():
0.140.18 by Jelmer Vernooij
Add credits command, test classify code by default, add comments to classify code.
314
            i = 0
315
            for delta in repository.get_deltas_for_revisions(revs):
316
                pb.update("classifying commits", i, len(revs))
317
                for c in classify_delta(delta):
7183.3.1 by Martin
Fix E71* lint errors
318
                    if c not in ret:
0.140.18 by Jelmer Vernooij
Add credits command, test classify code by default, add comments to classify code.
319
                        ret[c] = 0
320
                    ret[c] += 1
321
                    total += 1
322
                i += 1
323
    return ret, total
324
325
6656.1.1 by Martin
Apply 2to3 dict fixer and clean up resulting mess using view helpers
326
def classify_key(item):
327
    """Sort key for item of (author, count) from classify_delta."""
328
    return -item[1], item[0]
329
330
0.140.37 by John Arbash Meinel
Update display_credits to handle unprintable chars.
331
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.
332
    (coders, documenters, artists, translators) = credits
7143.15.2 by Jelmer Vernooij
Run autopep8.
333
0.140.18 by Jelmer Vernooij
Add credits command, test classify code by default, add comments to classify code.
334
    def print_section(name, lst):
335
        if len(lst) == 0:
336
            return
0.140.37 by John Arbash Meinel
Update display_credits to handle unprintable chars.
337
        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.
338
        for name in lst:
0.140.37 by John Arbash Meinel
Update display_credits to handle unprintable chars.
339
            to_file.write("%s\n" % name)
340
        to_file.write('\n')
0.140.18 by Jelmer Vernooij
Add credits command, test classify code by default, add comments to classify code.
341
    print_section("Code", coders)
342
    print_section("Documentation", documenters)
343
    print_section("Art", artists)
344
    print_section("Translations", translators)
345
346
347
def find_credits(repository, revid):
348
    """Find the credits of the contributors to a revision.
349
350
    :return: tuple with (authors, documenters, artists, translators)
351
    """
352
    ret = {"documentation": {},
353
           "code": {},
354
           "art": {},
355
           "translation": {},
356
           None: {}
357
           }
6754.8.4 by Jelmer Vernooij
Use new context stuff.
358
    with repository.lock_read():
0.151.1 by Jelmer Vernooij
Remove use of .get_ancestry.
359
        graph = repository.get_graph()
0.140.54 by Jelmer Vernooij
Filter out NULL_REVISION.
360
        ancestry = [r for (r, ps) in graph.iter_ancestry([revid])
361
                    if ps is not None and r != NULL_REVISION]
0.140.23 by Jelmer Vernooij
Add another progress bar.
362
        revs = repository.get_revisions(ancestry)
6861.4.1 by Jelmer Vernooij
Make progress bars context managers.
363
        with ui.ui_factory.nested_progress_bar() as pb:
6754.5.1 by Jelmer Vernooij
Fix some python3 compatibility issues that break 'make check-nodocs3' for me.
364
            iterator = zip(revs, repository.get_deltas_for_revisions(revs))
6809.1.1 by Martin
Apply 2to3 ws_comma fixer
365
            for i, (rev, delta) in enumerate(iterator):
0.140.23 by Jelmer Vernooij
Add another progress bar.
366
                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.
367
                # Don't count merges
368
                if len(rev.parent_ids) > 1:
369
                    continue
370
                for c in set(classify_delta(delta)):
0.140.29 by Jelmer Vernooij
Remove some uses of get_apparent_author.
371
                    for author in rev.get_apparent_authors():
7183.3.1 by Martin
Fix E71* lint errors
372
                        if author not in ret[c]:
0.140.29 by Jelmer Vernooij
Remove some uses of get_apparent_author.
373
                            ret[c][author] = 0
374
                        ret[c][author] += 1
7143.15.2 by Jelmer Vernooij
Run autopep8.
375
0.140.18 by Jelmer Vernooij
Add credits command, test classify code by default, add comments to classify code.
376
    def sort_class(name):
6656.1.1 by Martin
Apply 2to3 dict fixer and clean up resulting mess using view helpers
377
        return [author
7143.15.2 by Jelmer Vernooij
Run autopep8.
378
                for author, _ in sorted(ret[name].items(), key=classify_key)]
0.140.18 by Jelmer Vernooij
Add credits command, test classify code by default, add comments to classify code.
379
    return (sort_class("code"), sort_class("documentation"), sort_class("art"), sort_class("translation"))
380
381
382
class cmd_credits(commands.Command):
383
    """Determine credits for LOCATION."""
384
385
    takes_args = ['location?']
386
    takes_options = ['revision']
387
388
    encoding_type = 'replace'
389
390
    def run(self, location='.', revision=None):
391
        try:
392
            wt = workingtree.WorkingTree.open_containing(location)[0]
393
        except errors.NoWorkingTree:
394
            a_branch = branch.Branch.open(location)
395
            last_rev = a_branch.last_revision()
396
        else:
397
            a_branch = wt.branch
398
            last_rev = wt.last_revision()
399
400
        if revision is not None:
401
            last_rev = revision[0].in_history(a_branch).rev_id
402
6754.8.4 by Jelmer Vernooij
Use new context stuff.
403
        with a_branch.lock_read():
0.140.18 by Jelmer Vernooij
Add credits command, test classify code by default, add comments to classify code.
404
            credits = find_credits(a_branch.repository, last_rev)
0.140.37 by John Arbash Meinel
Update display_credits to handle unprintable chars.
405
            display_credits(credits, self.outf)