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