~launchpad-pqm/launchpad/devel

9373.1.1 by Karl Fogel
Add utilities/community-contributions.py (descended from lpcc.py in
1
#!/usr/bin/python
2
# -*- coding: utf-8 -*-
3
#
4
# Copyright 2009 Canonical Ltd.  This software is licensed under the
5
# GNU Affero General Public License version 3 (see the file LICENSE).
6
9373.1.3 by Karl Fogel
* utilities/community-contributions.py: Update doc string to start
7
"""Show what Launchpad community contributors have done.
8
9
Trawl a Launchpad branch's history to detect contributions by non-Canonical
10
developers, then update https://dev.launchpad.net/Contributions accordingly.
9373.1.1 by Karl Fogel
Add utilities/community-contributions.py (descended from lpcc.py in
11
10456.1.10 by William Grant
Take devel and db-devel paths as options instead of arguments, to make ordering more obvious.
12
Usage: community-contributions.py [options] --devel=PATH --db-devel=DB_PATH
9373.1.1 by Karl Fogel
Add utilities/community-contributions.py (descended from lpcc.py in
13
14
Requirements:
10456.1.4 by William Grant
Fully support multiple branches -- they will now be linked properly, and their revisions will be labeled.
15
       You need both the 'devel' and 'db-devel' branches of Launchpad
16
       available locally (see https://dev.launchpad.net/Getting),
17
       your ~/.moin_ids file must be set up correctly, and you need
18
       editmoin.py (if you don't have it, the error message will tell
19
       you where to get it).
9373.1.1 by Karl Fogel
Add utilities/community-contributions.py (descended from lpcc.py in
20
21
Options:
10456.1.10 by William Grant
Take devel and db-devel paths as options instead of arguments, to make ordering more obvious.
22
  -q               Print no non-essential messages.
23
  -h, --help       Print this help.
24
  --dry-run        Don't update the wiki, just print the new page to stdout.
25
  --draft-run      Update the wiki "/Draft" page instead of the real page.
26
  --devel=PATH     Specify the filesystem path to the 'devel' branch.
27
  --db-devel=PATH  Specify the filesystem path to the 'db-devel' branch.
9373.1.1 by Karl Fogel
Add utilities/community-contributions.py (descended from lpcc.py in
28
"""
29
30
# General notes:
31
#
32
# The Right Way to do this would probably be to output some kind of
33
# XML format, and then have a separate converter script transform that
34
# to wiki syntax and update the wiki page.  But as the wiki is our
35
# only consumer right now, we just output wiki syntax and update the
36
# wiki page directly, premature generalization being the root of all
37
# evil.
38
#
39
# For understanding the code, you may find it helpful to see
40
# bzrlib/log.py and http://bazaar-vcs.org/Integrating_with_Bazaar.
41
9373.1.16 by Karl Fogel
* utilities/community-contributions.py: Sort imports, as per jml's review.
42
import getopt
9373.1.1 by Karl Fogel
Add utilities/community-contributions.py (descended from lpcc.py in
43
import re
44
import sys
9373.1.16 by Karl Fogel
* utilities/community-contributions.py: Sort imports, as per jml's review.
45
46
from bzrlib import log
9373.1.1 by Karl Fogel
Add utilities/community-contributions.py (descended from lpcc.py in
47
from bzrlib.branch import Branch
48
from bzrlib.osutils import format_date
49
50
try:
51
    from editmoin import editshortcut
52
except:
10281.1.2 by Jamal Fanaian
Fixed lint messages regardling line length
53
    sys.stderr.write("""ERROR: Unable to import from 'editmoin'. How to solve:
9521.1.1 by Karl Fogel
In utilities/community-contributions.py, update error message to give
54
Get editmoin.py from launchpadlib's "contrib/" directory:
55
56
  http://bazaar.launchpad.net/~lazr-developers/launchpadlib/trunk/annotate/head%3A/contrib/editmoin.py
57
58
(Put it in the same directory as this script and everything should work.)
9373.1.1 by Karl Fogel
Add utilities/community-contributions.py (descended from lpcc.py in
59
""")
60
    sys.exit(1)
61
62
10301.1.13 by Karl Fogel
Various tweaks, based on Jonathan Lange's review.
63
def wiki_encode(x):
64
    """Encode a Unicode string for display on the wiki."""
65
    return x.encode('utf-8', 'xmlcharrefreplace')
66
67
68
10301.1.4 by Karl Fogel
Show Canonical contributors from outside the Launchpad team.
69
# The output contains two classes of contributors: people who don't
70
# work for Canonical at all, and people who do work for Canonical but
71
# not on the Launchpad team.
9373.1.1 by Karl Fogel
Add utilities/community-contributions.py (descended from lpcc.py in
72
#
10301.1.13 by Karl Fogel
Various tweaks, based on Jonathan Lange's review.
73
# People who used to work for Canonical on the Launchpad team are not
74
# shown in the output, since they don't help us from a "contributions
75
# from outside the team" perspective, so they are listed as known
76
# Canonical Launchpad developers even though they aren't actually on
77
# the team anymore.  There may be a few former Canonicalites who
78
# didn't work on the Launchpad team but who still contributed to
79
# Launchpad; most of them would have done so before Launchpad was open
80
# sourced in July 2009, though, and since this script is really about
81
# showing things that have happened since Launchpad was open sourced,
82
# they may be listed as Launchpad team members anyway just to ensure
83
# they don't appear in the output.
84
#
85
# (As time goes on, that assumption will be less and less correct, of
86
# course, and eventually we may wish to do something about it.  Also,
87
# there are some people, e.g. Jelmer Vernooij, who made contributions
88
# to Launchpad before working at Canonical, but who now work on the
89
# Launchpad team at Canonical.  Ideally, each potentially listable
90
# contributor could have a set of roles, and a date range associated
91
# with each role... but that would be overkill for this script.  That
92
# last 2% of correctness would cost way too much to achieve.)
93
#
10224.15.1 by Karl Fogel
* utilities/community-contributions.py
94
# XXX: Karl Fogel 2009-09-10 bug=513608: We should use launchpadlib
10301.1.4 by Karl Fogel
Show Canonical contributors from outside the Launchpad team.
95
# to consult Launchpad itself to find out who's a Canonical developer,
96
# and within that who's a Launchpad developer.
97
10301.1.13 by Karl Fogel
Various tweaks, based on Jonathan Lange's review.
98
99
# If a contributor's address contains this, then they are or were a
100
# Canonical developer -- maybe on the Launchpad team, maybe not.
101
CANONICAL_ADDR = wiki_encode(u" {_AT_} canonical.com")
102
10301.1.4 by Karl Fogel
Show Canonical contributors from outside the Launchpad team.
103
# People on the Canonical Launchpad team.
104
known_canonical_lp_devs = \
10301.1.13 by Karl Fogel
Various tweaks, based on Jonathan Lange's review.
105
    [wiki_encode(x) for x in (u'Aaron Bentley',
106
                              u'Abel Deuring',
107
                              u'Andrew Bennetts',
108
                              u'Barry Warsaw',
109
                              u'Bjorn Tillenius',
110
                              u'Björn Tillenius',
10456.1.12 by William Grant
Start at revision 1 of both branches, and update the name maps for the older revisions.
111
                              u'Brad Bollenbach',
10301.1.13 by Karl Fogel
Various tweaks, based on Jonathan Lange's review.
112
                              u'Brad Crittenden',
113
                              u'Brian Fromme',
10456.1.12 by William Grant
Start at revision 1 of both branches, and update the name maps for the older revisions.
114
                              u'Canonical.com Patch Queue Manager',
10301.1.13 by Karl Fogel
Various tweaks, based on Jonathan Lange's review.
115
                              u'Carlos Perello Marin',
116
                              u'Carlos Perelló Marín',
10456.1.12 by William Grant
Start at revision 1 of both branches, and update the name maps for the older revisions.
117
                              u'carlos.perello {_AT_} canonical.com',
10301.1.13 by Karl Fogel
Various tweaks, based on Jonathan Lange's review.
118
                              u'Celso Providelo',
119
                              u'Christian Reis',
10456.1.12 by William Grant
Start at revision 1 of both branches, and update the name maps for the older revisions.
120
                              u'Christian Robottom Reis',
10301.1.13 by Karl Fogel
Various tweaks, based on Jonathan Lange's review.
121
                              u'kiko {_AT_} beetle',
122
                              u'Curtis Hovey',
123
                              u'Dafydd Harries',
124
                              u'Danilo Šegan',
10456.1.12 by William Grant
Start at revision 1 of both branches, and update the name maps for the older revisions.
125
                              u'david <david {_AT_} marvin>',
10301.1.13 by Karl Fogel
Various tweaks, based on Jonathan Lange's review.
126
                              u'Данило Шеган',
127
                              u'данило шеган',
128
                              u'Daniel Silverstone',
129
                              u'David Allouche',
130
                              u'Deryck Hodge',
131
                              u'Diogo Matsubara',
132
                              u'Edwin Grubbs',
10456.1.12 by William Grant
Start at revision 1 of both branches, and update the name maps for the older revisions.
133
                              u'Elliot Murphy',
134
                              u'Firstname Lastname',
10301.1.13 by Karl Fogel
Various tweaks, based on Jonathan Lange's review.
135
                              u'Francis Lacoste',
136
                              u'Francis J. Lacoste',
137
                              u'Gary Poster',
138
                              u'Gavin Panella',
139
                              u'Graham Binns',
140
                              u'Guilherme Salgado',
141
                              u'Henning Eggers',
10456.1.12 by William Grant
Start at revision 1 of both branches, and update the name maps for the older revisions.
142
                              u'Herb McNew',
10301.1.13 by Karl Fogel
Various tweaks, based on Jonathan Lange's review.
143
                              u'James Henstridge',
144
                              u'Jelmer Vernooij',
145
                              u'Jeroen Vermeulen',
146
                              u'Jeroen T. Vermeulen',
147
                              u'Joey Stanford',
148
                              u'Jonathan Lange',
149
                              u'jml {_AT_} canonical.com',
150
                              u'jml {_AT_} mumak.net',
151
                              u'Jonathan Knowles',
10456.1.12 by William Grant
Start at revision 1 of both branches, and update the name maps for the older revisions.
152
                              u'jonathan.knowles {_AT_} canonical.com',
10301.1.13 by Karl Fogel
Various tweaks, based on Jonathan Lange's review.
153
                              u'Julian Edwards',
154
                              u'Karl Fogel',
10456.1.12 by William Grant
Start at revision 1 of both branches, and update the name maps for the older revisions.
155
                              u'Launch Pad',
10301.1.13 by Karl Fogel
Various tweaks, based on Jonathan Lange's review.
156
                              u'Launchpad APA',
10456.1.12 by William Grant
Start at revision 1 of both branches, and update the name maps for the older revisions.
157
                              u'Launchpad Developers',
10301.1.13 by Karl Fogel
Various tweaks, based on Jonathan Lange's review.
158
                              u'Launchpad Patch Queue Manager',
159
                              u'Launchpad PQM Bot',
160
                              u'Leonard Richardson',
161
                              u'Malcolm Cleaton',
162
                              u'Maris Fogels',
163
                              u'Mark Shuttleworth',
164
                              u'Martin Albisetti',
165
                              u'Matt Zimmerman',
166
                              u'Matthew Paul Thomas',
10456.1.12 by William Grant
Start at revision 1 of both branches, and update the name maps for the older revisions.
167
                              u'Matthew Thomas',
10301.1.13 by Karl Fogel
Various tweaks, based on Jonathan Lange's review.
168
                              u'Matthew Revell',
169
                              u'matthew.revell {_AT_} canonical.com',
170
                              u'Michael Hudson',
10456.1.12 by William Grant
Start at revision 1 of both branches, and update the name maps for the older revisions.
171
                              u'michael.hudson {_AT_} canonical.com',
10301.1.13 by Karl Fogel
Various tweaks, based on Jonathan Lange's review.
172
                              u'Michael Nelson',
173
                              u'Muharem Hrnjadovic',
174
                              u'muharem {_AT_} canonical.com',
10456.1.12 by William Grant
Start at revision 1 of both branches, and update the name maps for the older revisions.
175
                              u'Patch Queue Manager',
10301.1.13 by Karl Fogel
Various tweaks, based on Jonathan Lange's review.
176
                              u'Paul Hummer',
177
                              u'Robert Collins',
178
                              u'Stuart Bishop',
10456.1.12 by William Grant
Start at revision 1 of both branches, and update the name maps for the older revisions.
179
                              u'Steve Alexander',
11057.1.1 by Jonathan Lange
Correct some bugs with the Launchpad data
180
                              u'Steve Kowalik',
10301.1.13 by Karl Fogel
Various tweaks, based on Jonathan Lange's review.
181
                              u'Steve McInerney',
182
                              u'<steve {_AT_} stedee.id.au>',
10456.1.12 by William Grant
Start at revision 1 of both branches, and update the name maps for the older revisions.
183
                              u'test {_AT_} canonical.com',
10301.1.13 by Karl Fogel
Various tweaks, based on Jonathan Lange's review.
184
                              u'Tom Haddon',
185
                              u'Tim Penhey',
186
                              u'Tom Berger',
10456.1.12 by William Grant
Start at revision 1 of both branches, and update the name maps for the older revisions.
187
                              u'ubuntu <ubuntu {_AT_} lp-dev>',
10301.1.13 by Karl Fogel
Various tweaks, based on Jonathan Lange's review.
188
                              u'Ursula Junque',
189
                              )]
10301.1.4 by Karl Fogel
Show Canonical contributors from outside the Launchpad team.
190
191
# People known to work for Canonical but not on the Launchpad team.
192
# Anyone with "@canonical.com" in their email address is considered to
193
# work for Canonical, but some people occasionally submit changes from
194
# their personal email addresses; this list contains people known to
195
# do that, so we can treat them appropriately in the output.
196
known_canonical_non_lp_devs = \
10301.1.13 by Karl Fogel
Various tweaks, based on Jonathan Lange's review.
197
    [wiki_encode(x) for x in (u'Adam Conrad',
198
                              u'Andrew Bennetts',
199
                              u'Anthony Lenton',
200
                              u'Cody Somerville',
201
                              u'Cody A.W. Somerville',
202
                              u'David Murphy',
11057.1.1 by Jonathan Lange
Correct some bugs with the Launchpad data
203
                              u'Didier Roche',
10301.1.13 by Karl Fogel
Various tweaks, based on Jonathan Lange's review.
204
                              u'Elliot Murphy',
205
                              u'Gabriel Neuman gneuman {_AT_} async.com',
206
                              u'Gustavo Niemeyer',
207
                              u'James Henstridge',
11057.1.1 by Jonathan Lange
Correct some bugs with the Launchpad data
208
                              u'James Westby',
10301.1.13 by Karl Fogel
Various tweaks, based on Jonathan Lange's review.
209
                              u'John Lenton',
210
                              u'Kees Cook',
211
                              u'LaMont Jones',
11057.1.1 by Jonathan Lange
Correct some bugs with the Launchpad data
212
                              u'Martin Pitt',
10301.1.13 by Karl Fogel
Various tweaks, based on Jonathan Lange's review.
213
                              u'Martin Pool',
214
                              u'Matt Zimmerman',
215
                              u'Michael Casadevall',
216
                              u'Michael Vogt',
217
                              u'Sidnei da Silva',
218
                              u'Steve Kowalik',
10667.1.1 by Karl Fogel
* utilities/community-contributions.py
219
                              u'Dustin Kirkland',
10301.1.13 by Karl Fogel
Various tweaks, based on Jonathan Lange's review.
220
                              )]
9373.1.1 by Karl Fogel
Add utilities/community-contributions.py (descended from lpcc.py in
221
11057.1.2 by Jonathan Lange
Flakes, space
222
# Some people have made commits using various names and/or email
10281.1.1 by Jamal Fanaian
Created name map for community-contributions to merge people using different names/email addresses.
223
# addresses, so this map will be used to merge them accordingly.
10301.1.8 by Karl Fogel
Clean up the way we do string encoding. No functional change.
224
# The map is initialized from this list of pairs, where each pair is
225
# of the form (CONTRIBUTOR_AS_SEEN, UNIFYING_IDENTITY_FOR_CONTRIBUTOR).
226
merge_names_pairs = (
227
    (u'Jamal Fanaian <jfanaian {_AT_} gmail.com>',
228
     u'Jamal Fanaian <jamal.fanaian {_AT_} gmail.com>'),
229
    (u'Jamal Fanaian <jamal {_AT_} jfvm1>',
230
     u'Jamal Fanaian <jamal.fanaian {_AT_} gmail.com>'),
231
    (u'LaMont Jones <lamont {_AT_} rover3>',
232
     u'LaMont Jones <lamont {_AT_} debian.org>'),
10456.1.12 by William Grant
Start at revision 1 of both branches, and update the name maps for the older revisions.
233
    (u'Sidnei <sidnei {_AT_} ubuntu>',
234
     u'Sidnei da Silva <sidnei.da.silva {_AT_} canonical.com>'),
235
    (u'Sidnei da Silva <sidnei.da.silva {_AT_} gmail.com>',
236
     u'Sidnei da Silva <sidnei.da.silva {_AT_} canonical.com>'),
237
    (u'Sidnei da Silva <sidnei {_AT_} canonical.com>',
238
     u'Sidnei da Silva <sidnei.da.silva {_AT_} canonical.com>'),
239
    (u'Adam Conrad <adconrad {_AT_} ziggup>',
240
     u'Adam Conrad <adconrad {_AT_} 0c3.net>'),
241
    (u'Elliot Murphy <elliot {_AT_} elliotmurphy.com>',
242
     u'Elliot Murphy <elliot {_AT_} canonical.com>'),
243
    (u'Elliot Murphy <elliot.murphy {_AT_} canonical.com>',
244
     u'Elliot Murphy <elliot {_AT_} canonical.com>'),
245
    (u'Cody Somerville <cody-somerville {_AT_} mercurial>',
246
     u'Cody A.W. Somerville <cody.somerville {_AT_} canonical.com>'),
247
    (u'Adam Conrad <adconrad {_AT_} chinstrap>',
248
     u'Adam Conrad <adconrad {_AT_} 0c3.net>'),
249
    (u'Adam Conrad <adconrad {_AT_} cthulhu>',
250
     u'Adam Conrad <adconrad {_AT_} 0c3.net>'),
11057.1.1 by Jonathan Lange
Correct some bugs with the Launchpad data
251
    (u'James Westby <james.westby@linaro.org>',
252
     u'James Westby <james.westby@canonical.com'),
253
    (u'Bryce Harrington <bryce@canonical.com>',
254
     u'Bryce Harrington <bryce.harrington@canonical.com>'),
255
    (u'Dustin Kirkland <kirkland@x200>',
256
     u'Dustin Kirkland <kirkland@canonical.com>'),
257
    (u'Anthony Lenton <antoniolenton@gmail.com>',
258
     u'Anthony Lenton <anthony.lenton@canonical.com>'),
259
    (u'Steve Kowalik <steven@quelled>',
260
     u'Steve Kowalik <steve.kowalik@canonical.com>'),
261
    (u'Steve Kowalik <stevenk@ubuntu.com>',
262
     u'Steve Kowalik <steve.kowalik@canonical.com>'),
10301.1.8 by Karl Fogel
Clean up the way we do string encoding. No functional change.
263
    )
264
# Then put it in dictionary form with the correct encodings.
10301.1.13 by Karl Fogel
Various tweaks, based on Jonathan Lange's review.
265
merge_names_map = dict((wiki_encode(a), wiki_encode(b))
266
                       for a, b in merge_names_pairs)
10301.1.8 by Karl Fogel
Clean up the way we do string encoding. No functional change.
267
9373.1.1 by Karl Fogel
Add utilities/community-contributions.py (descended from lpcc.py in
268
269
class ContainerRevision():
9373.1.4 by Karl Fogel
* utilities/community-contributions.py: Reindent with py-offset-level 4,
270
    """A wrapper for a top-level LogRevision containing child LogRevisions."""
271
10456.1.4 by William Grant
Fully support multiple branches -- they will now be linked properly, and their revisions will be labeled.
272
    def __init__(self, top_lr, branch_info):
10456.1.7 by William Grant
Address review comments.
273
        """Create a new ContainerRevision.
274
275
        :param top_lr: The top-level LogRevision.
276
        :param branch_info: The BranchInfo for the containing branch.
277
        """
9373.1.4 by Karl Fogel
* utilities/community-contributions.py: Reindent with py-offset-level 4,
278
        self.top_rev = top_lr       # e.g. LogRevision for r9371.
9373.1.12 by Karl Fogel
* utilities/community-contributions.py: More formatting and
279
        self.contained_revs = []    # e.g. [ {9369.1.1}, {9206.4.4}, ... ],
280
                                    # where "{X}" means "LogRevision for X"
10456.1.4 by William Grant
Fully support multiple branches -- they will now be linked properly, and their revisions will be labeled.
281
        self.branch_info = branch_info
282
9373.1.4 by Karl Fogel
* utilities/community-contributions.py: Reindent with py-offset-level 4,
283
    def add_subrev(self, lr):
284
        """Add a descendant child of this container revision."""
285
        self.contained_revs.append(lr)
286
287
    def __str__(self):
9373.1.12 by Karl Fogel
* utilities/community-contributions.py: More formatting and
288
        timestamp = self.top_rev.rev.timestamp
289
        timezone = self.top_rev.rev.timezone
290
        message = self.top_rev.rev.message or "(NO LOG MESSAGE)"
291
        rev_id = self.top_rev.rev.revision_id or "(NO REVISION ID)"
9373.1.4 by Karl Fogel
* utilities/community-contributions.py: Reindent with py-offset-level 4,
292
        if timestamp:
293
            date_str = format_date(timestamp, timezone or 0, 'original')
294
        else:
295
            date_str = "(NO DATE)"
296
10456.1.4 by William Grant
Fully support multiple branches -- they will now be linked properly, and their revisions will be labeled.
297
        rev_url_base = "http://bazaar.launchpad.net/%s/revision/" % (
10456.1.7 by William Grant
Address review comments.
298
            self.branch_info.loggerhead_path)
9373.1.4 by Karl Fogel
* utilities/community-contributions.py: Reindent with py-offset-level 4,
299
300
        # In loggerhead, you can use either a revision number or a
301
        # revision ID.  In other words, these would reach the same page:
302
        #
303
        # http://bazaar.launchpad.net/~launchpad-pqm/launchpad/devel/\
304
        # revision/9202
305
        #
306
        #   -and-
307
        #
10281.1.2 by Jamal Fanaian
Fixed lint messages regardling line length
308
        # http://bazaar.launchpad.net/~launchpad-pqm/launchpad/devel/\
309
        # revision/launchpad@pqm.canonical.com-20090821221206-\
310
        # ritpv21q8w61gbpt
9373.1.4 by Karl Fogel
* utilities/community-contributions.py: Reindent with py-offset-level 4,
311
        #
312
        # In our links, even when the link text is a revnum, we still
313
        # use a rev-id for the target.  This is both so that the URL will
314
        # still work if you manually tweak it (say to "db-devel" from
315
        # "devel") and so that hovering over a revnum on the wiki page
316
        # will give you some information about it before you click
317
        # (because a rev id often identifies the committer).
318
        rev_id_url = rev_url_base + rev_id
10301.1.4 by Karl Fogel
Show Canonical contributors from outside the Launchpad team.
319
320
        if len(self.contained_revs) <= 10:
321
            commits_block = "\n ".join(
322
                ["[[%s|%s]]" % (rev_url_base + lr.rev.revision_id, lr.revno)
323
                 for lr in self.contained_revs])
324
        else:
10301.1.13 by Karl Fogel
Various tweaks, based on Jonathan Lange's review.
325
            commits_block = ("''see the [[%s|full revision]] for details "
326
                             "(it contains %d commits)''"
327
                             % (rev_id_url, len(self.contained_revs)))
10301.1.4 by Karl Fogel
Show Canonical contributors from outside the Launchpad team.
328
10456.1.7 by William Grant
Address review comments.
329
        name = self.branch_info.name
330
9373.1.13 by Karl Fogel
* utilities/community-contributions.py:
331
        text = [
10456.1.4 by William Grant
Fully support multiple branches -- they will now be linked properly, and their revisions will be labeled.
332
            " * [[%s|r%s%s]] -- %s\n" % (
333
                rev_id_url, self.top_rev.revno,
10456.1.7 by William Grant
Address review comments.
334
                ' (%s)' % name if name else '',
10456.1.4 by William Grant
Fully support multiple branches -- they will now be linked properly, and their revisions will be labeled.
335
                date_str),
9373.1.13 by Karl Fogel
* utilities/community-contributions.py:
336
            " {{{\n%s\n}}}\n" % message,
337
            " '''Commits:'''\n ",
10301.1.4 by Karl Fogel
Show Canonical contributors from outside the Launchpad team.
338
            commits_block,
9373.1.13 by Karl Fogel
* utilities/community-contributions.py:
339
            "\n",
340
            ]
341
        return ''.join(text)
11057.1.2 by Jonathan Lange
Flakes, space
342
343
9373.1.1 by Karl Fogel
Add utilities/community-contributions.py (descended from lpcc.py in
344
# "ExternalContributor" is too much to type, so I guess we'll just use this.
345
class ExCon():
10301.1.4 by Karl Fogel
Show Canonical contributors from outside the Launchpad team.
346
    """A contributor to Launchpad from outside Canonical's Launchpad team."""
9373.1.9 by Karl Fogel
* utilities/community-contributions.py: Blank line after class docstrings.
347
10301.1.4 by Karl Fogel
Show Canonical contributors from outside the Launchpad team.
348
    def __init__(self, name, is_canonical=False):
10301.1.13 by Karl Fogel
Various tweaks, based on Jonathan Lange's review.
349
        """Create a new external contributor named 'name'.
350
351
        If 'is_canonical' is True, then this is a contributor from
10301.1.4 by Karl Fogel
Show Canonical contributors from outside the Launchpad team.
352
        within Canonical, but not on the Launchpad team at Canonical.
10301.1.13 by Karl Fogel
Various tweaks, based on Jonathan Lange's review.
353
        'name' is something like "Veronica Random <vr {_AT_} example.com>".
354
        """
10301.1.3 by Karl Fogel
Disguise email addresses in the source code.
355
        self.name = name
10301.1.4 by Karl Fogel
Show Canonical contributors from outside the Launchpad team.
356
        self.is_canonical = is_canonical
9373.1.4 by Karl Fogel
* utilities/community-contributions.py: Reindent with py-offset-level 4,
357
        # If name is "Veronica Random <veronica {_AT_} example.com>",
358
        # then name_as_anchor will be "veronica_random".
359
        self.name_as_anchor = \
360
            re.compile("\\s+").sub("_", name.split("<")[0].strip()).lower()
361
        # All the top-level revisions this contributor is associated with
362
        # (key == value == ContainerRevision).  We use a dictionary
363
        # instead of list to get set semantics; set() would be overkill.
9373.1.12 by Karl Fogel
* utilities/community-contributions.py: More formatting and
364
        self._landings = {}
10456.1.6 by William Grant
Comment on the purpose and usage of seen_revs.
365
        # A map of revision IDs authored by this contributor (probably
366
        # not top-level) to a (LogRevision, ContainerRevision) pair. The
367
        # pair contains details of the shallowest found instance of this
368
        # revision.
10456.1.3 by William Grant
Collect the shallowest authored revisions across all branches for each ExCon, only adding their top-level revs once we've seen everything. This gives sane results by ignoring merges from devel->db-devel and db-devel->devel. The main problem now is that db-devel links will point to devel instead.
369
        self.seen_revs = {}
9373.1.4 by Karl Fogel
* utilities/community-contributions.py: Reindent with py-offset-level 4,
370
371
    def num_landings(self):
372
        """Return the number of top-level landings that include revisions
373
        by this contributor."""
374
        return len(self._landings)
375
376
    def add_top_level_revision(self, cr):
9373.1.11 by Karl Fogel
* utilities/community-contributions.py: Stay within 80 columns.
377
        "Record ContainableRevision CR as associated with this contributor."
9373.1.4 by Karl Fogel
* utilities/community-contributions.py: Reindent with py-offset-level 4,
378
        self._landings[cr] = cr
379
380
    def show_contributions(self):
9373.1.11 by Karl Fogel
* utilities/community-contributions.py: Stay within 80 columns.
381
        "Return a wikified string showing this contributor's contributions."
9373.1.4 by Karl Fogel
* utilities/community-contributions.py: Reindent with py-offset-level 4,
382
        plural = "s"
10301.1.4 by Karl Fogel
Show Canonical contributors from outside the Launchpad team.
383
        name = self.name
384
        if self.is_canonical:
385
            name = name + " (Canonical developer)"
9373.1.4 by Karl Fogel
* utilities/community-contributions.py: Reindent with py-offset-level 4,
386
        if self.num_landings() == 1:
387
            plural = ""
9373.1.13 by Karl Fogel
* utilities/community-contributions.py:
388
        text = [
10301.1.4 by Karl Fogel
Show Canonical contributors from outside the Launchpad team.
389
            "=== %s ===\n\n" % name,
9373.1.13 by Karl Fogel
* utilities/community-contributions.py:
390
            "''%d top-level landing%s:''\n\n" % (self.num_landings(), plural),
391
            ''.join(map(str, sorted(self._landings,
10456.1.1 by William Grant
Order top-level revs by timestamp, not revno string (this broke when we reached r10000).
392
                                    key=lambda x: x.top_rev.rev.timestamp,
9373.1.13 by Karl Fogel
* utilities/community-contributions.py:
393
                                    reverse=True))),
394
            "\n",
395
            ]
396
        return ''.join(text)
9373.1.1 by Karl Fogel
Add utilities/community-contributions.py (descended from lpcc.py in
397
398
399
def get_ex_cons(authors, all_ex_cons):
9373.1.4 by Karl Fogel
* utilities/community-contributions.py: Reindent with py-offset-level 4,
400
    """Return a list of ExCon objects corresponding to AUTHORS (a list
401
    of strings).  If there are no external contributors in authors,
402
    return an empty list.
9373.1.1 by Karl Fogel
Add utilities/community-contributions.py (descended from lpcc.py in
403
9373.1.4 by Karl Fogel
* utilities/community-contributions.py: Reindent with py-offset-level 4,
404
    ALL_EX_CONS is a dictionary mapping author names (as received from
405
    the bzr logs, i.e., with email address undisguised) to ExCon objects.
406
    """
9373.1.12 by Karl Fogel
* utilities/community-contributions.py: More formatting and
407
    ex_cons_this_rev = []
10301.1.2 by Karl Fogel
* community-contributions.py
408
    for author in authors:
10301.1.4 by Karl Fogel
Show Canonical contributors from outside the Launchpad team.
409
        known_canonical_lp_dev = False
410
        known_canonical_non_lp_dev = False
10301.1.3 by Karl Fogel
Disguise email addresses in the source code.
411
        # The authors we list in the source code have their addresses
412
        # disguised (since this source code is public).  We must
413
        # disguise the ones coming from the Bazaar logs in the same way,
414
        # so string matches will work.
10301.1.13 by Karl Fogel
Various tweaks, based on Jonathan Lange's review.
415
        author = wiki_encode(author)
10301.1.3 by Karl Fogel
Disguise email addresses in the source code.
416
        author = author.replace("@", " {_AT_} ")
10301.1.13 by Karl Fogel
Various tweaks, based on Jonathan Lange's review.
417
418
        # If someone works/worked for Canonical on the Launchpad team,
419
        # then skip them -- we don't want to show them in the output.
10301.1.4 by Karl Fogel
Show Canonical contributors from outside the Launchpad team.
420
        for name_fragment in known_canonical_lp_devs:
421
            if name_fragment in author:
422
                known_canonical_lp_dev = True
9373.1.12 by Karl Fogel
* utilities/community-contributions.py: More formatting and
423
                break
10301.1.4 by Karl Fogel
Show Canonical contributors from outside the Launchpad team.
424
        if known_canonical_lp_dev:
425
            continue
426
427
        # Use the merge names map to merge contributions from the same
428
        # person using alternate names and/or emails.
10301.1.5 by Karl Fogel
* community-contributions.py
429
        author = merge_names_map.get(author, author)
10301.1.4 by Karl Fogel
Show Canonical contributors from outside the Launchpad team.
430
10301.1.13 by Karl Fogel
Various tweaks, based on Jonathan Lange's review.
431
        if CANONICAL_ADDR in author:
10301.1.4 by Karl Fogel
Show Canonical contributors from outside the Launchpad team.
432
            known_canonical_non_lp_dev = True
433
        else:
434
            for name_fragment in known_canonical_non_lp_devs:
435
                if name_fragment in author:
436
                    known_canonical_non_lp_dev = True
437
                    break
438
10301.1.13 by Karl Fogel
Various tweaks, based on Jonathan Lange's review.
439
        # There's a variant of the Singleton pattern that could be
440
        # used for this, whereby instantiating an ExCon object would
441
        # just get back an existing object if such has already been
442
        # instantiated for this name.  But that would make this code
443
        # non-reentrant, and that's just not cool.
444
        ec = all_ex_cons.get(author, None)
445
        if ec is None:
10301.1.4 by Karl Fogel
Show Canonical contributors from outside the Launchpad team.
446
            ec = ExCon(author, is_canonical=known_canonical_non_lp_dev)
447
            all_ex_cons[author] = ec
448
        ex_cons_this_rev.append(ec)
9373.1.4 by Karl Fogel
* utilities/community-contributions.py: Reindent with py-offset-level 4,
449
    return ex_cons_this_rev
9373.1.1 by Karl Fogel
Add utilities/community-contributions.py (descended from lpcc.py in
450
451
452
# The LogFormatter abstract class should really be called LogReceiver
453
# or something -- subclasses don't have to be about display.
454
class LogExCons(log.LogFormatter):
9373.1.4 by Karl Fogel
* utilities/community-contributions.py: Reindent with py-offset-level 4,
455
    """Log all the external contributions, by Contributor."""
9373.1.9 by Karl Fogel
* utilities/community-contributions.py: Blank line after class docstrings.
456
9373.1.4 by Karl Fogel
* utilities/community-contributions.py: Reindent with py-offset-level 4,
457
    # See log.LogFormatter documentation.
458
    supports_merge_revisions = True
459
460
    def __init__(self):
461
        super(LogExCons, self).__init__(to_file=None)
462
        # Dictionary mapping author names (with undisguised email
463
        # addresses) to ExCon objects.
9373.1.12 by Karl Fogel
* utilities/community-contributions.py: More formatting and
464
        self.all_ex_cons = {}
9373.1.11 by Karl Fogel
* utilities/community-contributions.py: Stay within 80 columns.
465
        # ContainerRevision object representing most-recently-seen
466
        # top-level rev.
10456.1.4 by William Grant
Fully support multiple branches -- they will now be linked properly, and their revisions will be labeled.
467
        self.current_top_level_rev = None
468
        self.branch_info = None
9373.1.4 by Karl Fogel
* utilities/community-contributions.py: Reindent with py-offset-level 4,
469
10301.1.4 by Karl Fogel
Show Canonical contributors from outside the Launchpad team.
470
    def _toc(self, contributors):
471
        toc_text = []
472
        for val in contributors:
473
            plural = "s"
474
            if val.num_landings() == 1:
475
                plural = ""
476
            toc_text.extend(" 1. [[#%s|%s]] ''(%d top-level landing%s)''\n"
477
                            % (val.name_as_anchor, val.name,
478
                               val.num_landings(), plural))
479
        return toc_text
10456.1.4 by William Grant
Fully support multiple branches -- they will now be linked properly, and their revisions will be labeled.
480
9373.1.4 by Karl Fogel
* utilities/community-contributions.py: Reindent with py-offset-level 4,
481
    def result(self):
9373.1.11 by Karl Fogel
* utilities/community-contributions.py: Stay within 80 columns.
482
        "Return a moin-wiki-syntax string with TOC followed by contributions."
10301.1.4 by Karl Fogel
Show Canonical contributors from outside the Launchpad team.
483
10456.1.6 by William Grant
Comment on the purpose and usage of seen_revs.
484
        # Go through the shallowest authored revisions and add their
485
        # top level revisions.
10456.1.3 by William Grant
Collect the shallowest authored revisions across all branches for each ExCon, only adding their top-level revs once we've seen everything. This gives sane results by ignoring merges from devel->db-devel and db-devel->devel. The main problem now is that db-devel links will point to devel instead.
486
        for excon in self.all_ex_cons.values():
487
            for rev, top_level_rev in excon.seen_revs.values():
488
                excon.add_top_level_revision(top_level_rev)
489
10301.1.4 by Karl Fogel
Show Canonical contributors from outside the Launchpad team.
490
        # Divide contributors into non-Canonical and Canonical.
491
        non_canonical_contributors = [x for x in self.all_ex_cons.values()
492
                                      if not x.is_canonical]
493
        canonical_contributors = [x for x in self.all_ex_cons.values()
494
                                      if x.is_canonical]
495
        # Sort them.
496
        non_canonical_contributors = sorted(non_canonical_contributors,
497
                                            key=lambda x: x.num_landings(),
498
                                            reverse=True)
499
        canonical_contributors = sorted(canonical_contributors,
500
                                        key=lambda x: x.num_landings(),
501
                                        reverse=True)
502
9373.1.13 by Karl Fogel
* utilities/community-contributions.py:
503
        text = [
504
            "-----\n\n",
10301.1.4 by Karl Fogel
Show Canonical contributors from outside the Launchpad team.
505
            "= Who =\n\n"
506
            "== Contributors (from outside Canonical) ==\n\n",
9373.1.13 by Karl Fogel
* utilities/community-contributions.py:
507
            ]
10301.1.4 by Karl Fogel
Show Canonical contributors from outside the Launchpad team.
508
        text.extend(self._toc(non_canonical_contributors))
509
        text.extend([
510
            "== Contributors (from Canonical, but outside "
511
            "the Launchpad team) ==\n\n",
512
            ])
513
        text.extend(self._toc(canonical_contributors))
9373.1.13 by Karl Fogel
* utilities/community-contributions.py:
514
        text.extend(["\n-----\n\n",
515
                     "= What =\n\n",
10301.1.4 by Karl Fogel
Show Canonical contributors from outside the Launchpad team.
516
                     "== Contributions (from outside Canonical) ==\n\n",
517
                     ])
518
        for val in non_canonical_contributors:
519
            text.extend("<<Anchor(%s)>>\n" % val.name_as_anchor)
520
            text.extend(val.show_contributions())
521
        text.extend(["== Contributions (from Canonical, but outside "
522
                     "the Launchpad team) ==\n\n",
523
                     ])
524
        for val in canonical_contributors:
9373.1.13 by Karl Fogel
* utilities/community-contributions.py:
525
            text.extend("<<Anchor(%s)>>\n" % val.name_as_anchor)
526
            text.extend(val.show_contributions())
527
        return ''.join(text)
9373.1.4 by Karl Fogel
* utilities/community-contributions.py: Reindent with py-offset-level 4,
528
529
    def log_revision(self, lr):
530
        """Log a revision.
531
        :param  lr:   The LogRevision to be logged.
532
        """
533
        # We count on always seeing the containing rev before its subrevs.
534
        if lr.merge_depth == 0:
10456.1.4 by William Grant
Fully support multiple branches -- they will now be linked properly, and their revisions will be labeled.
535
            self.current_top_level_rev = ContainerRevision(
536
                lr, self.branch_info)
9373.1.4 by Karl Fogel
* utilities/community-contributions.py: Reindent with py-offset-level 4,
537
        else:
538
            self.current_top_level_rev.add_subrev(lr)
539
        ex_cons = get_ex_cons(lr.rev.get_apparent_authors(), self.all_ex_cons)
540
        for ec in ex_cons:
10456.1.6 by William Grant
Comment on the purpose and usage of seen_revs.
541
            # If this is the shallowest sighting of a revision, note it
10456.1.9 by William Grant
Comment on shallowness a little more.
542
            # in the ExCon. We may see the revision at different depths
543
            # in different branches, mostly when one of the trunks is
544
            # merged into the other. We only care about the initial
545
            # merge, which should be shallowest.
10456.1.3 by William Grant
Collect the shallowest authored revisions across all branches for each ExCon, only adding their top-level revs once we've seen everything. This gives sane results by ignoring merges from devel->db-devel and db-devel->devel. The main problem now is that db-devel links will point to devel instead.
546
            if (lr.rev.revision_id not in ec.seen_revs or
10456.1.11 by William Grant
Wrap two long lines.
547
                lr.merge_depth <
548
                    ec.seen_revs[lr.rev.revision_id][0].merge_depth):
549
                ec.seen_revs[lr.rev.revision_id] = (
550
                    lr, self.current_top_level_rev)
9373.1.1 by Karl Fogel
Add utilities/community-contributions.py (descended from lpcc.py in
551
552
10456.1.7 by William Grant
Address review comments.
553
class BranchInfo:
554
    """A collection of information about a branch."""
555
10456.1.13 by William Grant
Remove start_revno support.
556
    def __init__(self, path, loggerhead_path, name=None):
10456.1.7 by William Grant
Address review comments.
557
        """Create a new BranchInfo.
558
559
        :param path: Filesystem path to the branch.
560
        :param loggerhead_path: The path to the branch on Launchpad's
561
            Loggerhead instance.
562
        :param name: Optional name to identify the branch's revisions in the
563
            produced document.
564
        """
565
        self.path = path
566
        self.name = name
567
        self.loggerhead_path = loggerhead_path
568
569
9373.1.15 by Karl Fogel
* utilities/community-contributions.py: Add name to "XXX" comments, as
570
# XXX: Karl Fogel 2009-09-10: is this really necessary?  See bzrlib/log.py.
9373.1.1 by Karl Fogel
Add utilities/community-contributions.py (descended from lpcc.py in
571
log.log_formatter_registry.register('external_contributors', LogExCons,
572
                                    'Find non-Canonical contributors.')
573
574
575
def usage():
9373.1.4 by Karl Fogel
* utilities/community-contributions.py: Reindent with py-offset-level 4,
576
    print __doc__
9373.1.1 by Karl Fogel
Add utilities/community-contributions.py (descended from lpcc.py in
577
578
10301.1.13 by Karl Fogel
Various tweaks, based on Jonathan Lange's review.
579
# Use backslashes to suppress newlines because this is wiki syntax,
580
# not HTML, so newlines would be rendered as line breaks.
9373.1.14 by Karl Fogel
* utilities/community-contributions.py (page_intro): Format source
581
page_intro = """This page shows contributions to Launchpad from \
10301.1.4 by Karl Fogel
Show Canonical contributors from outside the Launchpad team.
582
developers not on the Launchpad team at Canonical.
583
10456.1.5 by William Grant
Fix obsolete comments, and add a couple more.
584
It lists all changes that have landed in the Launchpad ''devel'' \
585
or ''db-devel'' trees (see the [[Trunk|trunk explanation]] for more).
9373.1.1 by Karl Fogel
Add utilities/community-contributions.py (descended from lpcc.py in
586
9373.1.14 by Karl Fogel
* utilities/community-contributions.py (page_intro): Format source
587
~-''Note for maintainers: this page is updated every 10 minutes by a \
588
cron job running as kfogel on devpad (though if there are no new \
589
contributions, the page's timestamp won't change).  The code that \
590
generates this page is \
591
[[http://bazaar.launchpad.net/%7Elaunchpad-pqm/launchpad/devel/annotate/head%3A/utilities/community-contributions.py|utilities/community-contributions.py]] \
592
in the Launchpad tree.''-~
9373.1.1 by Karl Fogel
Add utilities/community-contributions.py (descended from lpcc.py in
593
594
"""
595
596
def main():
9373.1.4 by Karl Fogel
* utilities/community-contributions.py: Reindent with py-offset-level 4,
597
    quiet = False
598
    dry_run = False
10456.1.10 by William Grant
Take devel and db-devel paths as options instead of arguments, to make ordering more obvious.
599
    devel_path = None
600
    db_devel_path = None
9373.1.4 by Karl Fogel
* utilities/community-contributions.py: Reindent with py-offset-level 4,
601
10301.1.1 by Karl Fogel
* community-contributions.py
602
    wiki_dest = "https://dev.launchpad.net/Contributions"
603
10456.1.4 by William Grant
Fully support multiple branches -- they will now be linked properly, and their revisions will be labeled.
604
    if len(sys.argv) < 3:
9373.1.4 by Karl Fogel
* utilities/community-contributions.py: Reindent with py-offset-level 4,
605
        usage()
606
        sys.exit(1)
607
608
    try:
609
        opts, args = getopt.getopt(sys.argv[1:], '?hq',
10456.1.10 by William Grant
Take devel and db-devel paths as options instead of arguments, to make ordering more obvious.
610
                                   ['help', 'usage', 'dry-run', 'draft-run',
611
                                    'devel=', 'db-devel='])
9373.1.4 by Karl Fogel
* utilities/community-contributions.py: Reindent with py-offset-level 4,
612
    except getopt.GetoptError, e:
613
        sys.stderr.write("ERROR: " + str(e) + '\n\n')
614
        usage()
615
        sys.exit(1)
616
617
    for opt, value in opts:
618
        if opt == '--help' or opt == '-h' or opt == '-?' or opt == 'usage':
619
            usage()
620
            sys.exit(0)
621
        elif opt == '-q' or opt == '--quiet':
622
            quiet = True
623
        elif opt == '--dry-run':
624
            dry_run = True
10301.1.1 by Karl Fogel
* community-contributions.py
625
        elif opt == '--draft-run':
626
            wiki_dest += "/Draft"
10456.1.10 by William Grant
Take devel and db-devel paths as options instead of arguments, to make ordering more obvious.
627
        elif opt == '--devel':
628
            devel_path = value
629
        elif opt == '--db-devel':
630
            db_devel_path = value
9373.1.4 by Karl Fogel
* utilities/community-contributions.py: Reindent with py-offset-level 4,
631
632
    # Ensure we have the arguments we need.
10456.1.10 by William Grant
Take devel and db-devel paths as options instead of arguments, to make ordering more obvious.
633
    if not devel_path or not db_devel_path:
10456.1.4 by William Grant
Fully support multiple branches -- they will now be linked properly, and their revisions will be labeled.
634
        sys.stderr.write("ERROR: paths to Launchpad devel and db-devel "
10456.1.10 by William Grant
Take devel and db-devel paths as options instead of arguments, to make ordering more obvious.
635
                         "branches required as options\n")
9373.1.4 by Karl Fogel
* utilities/community-contributions.py: Reindent with py-offset-level 4,
636
        usage()
637
        sys.exit(1)
638
10456.1.7 by William Grant
Address review comments.
639
    branches = (
640
        BranchInfo(
10456.1.13 by William Grant
Remove start_revno support.
641
            devel_path, '~launchpad-pqm/launchpad/devel'),
10456.1.7 by William Grant
Address review comments.
642
        BranchInfo(
10456.1.13 by William Grant
Remove start_revno support.
643
            db_devel_path, '~launchpad-pqm/launchpad/db-devel', 'db-devel'),
10456.1.7 by William Grant
Address review comments.
644
        )
10456.1.4 by William Grant
Fully support multiple branches -- they will now be linked properly, and their revisions will be labeled.
645
9373.1.4 by Karl Fogel
* utilities/community-contributions.py: Reindent with py-offset-level 4,
646
    lec = LogExCons()
10456.1.2 by William Grant
Take multiple branches, and only count each authored revision once.
647
10456.1.7 by William Grant
Address review comments.
648
    for branch_info in branches:
10456.1.2 by William Grant
Take multiple branches, and only count each authored revision once.
649
        # Do everything.
10456.1.7 by William Grant
Address review comments.
650
        b = Branch.open(branch_info.path)
10456.1.2 by William Grant
Take multiple branches, and only count each authored revision once.
651
10456.1.13 by William Grant
Remove start_revno support.
652
        logger = log.Logger(b, {'direction' : 'reverse',
10456.1.2 by William Grant
Take multiple branches, and only count each authored revision once.
653
                                'levels' : 0, })
654
        if not quiet:
655
            print "Calculating (this may take a while)..."
10456.1.5 by William Grant
Fix obsolete comments, and add a couple more.
656
657
        # Set information about the current branch for later formatting.
10456.1.4 by William Grant
Fully support multiple branches -- they will now be linked properly, and their revisions will be labeled.
658
        lec.branch_info = branch_info
10456.1.2 by William Grant
Take multiple branches, and only count each authored revision once.
659
        logger.show(lec)  # Won't "show" anything -- just gathers data.
660
9373.1.4 by Karl Fogel
* utilities/community-contributions.py: Reindent with py-offset-level 4,
661
    page_contents = page_intro + lec.result()
662
    def update_if_modified(moinfile):
663
        if moinfile._unescape(moinfile.body) == page_contents:
664
            return 0  # Nothing changed, so cancel the edit.
665
        else:
666
            moinfile.body = page_contents
667
            return 1
668
    if not dry_run:
669
        if not quiet:
670
            print "Updating wiki..."
671
        # Not sure how to get editmoin to obey our quiet flag.
10301.1.1 by Karl Fogel
* community-contributions.py
672
        editshortcut(wiki_dest, editfile_func=update_if_modified)
9373.1.4 by Karl Fogel
* utilities/community-contributions.py: Reindent with py-offset-level 4,
673
        if not quiet:
674
            print "Done updating wiki."
9373.1.1 by Karl Fogel
Add utilities/community-contributions.py (descended from lpcc.py in
675
    else:
9373.1.4 by Karl Fogel
* utilities/community-contributions.py: Reindent with py-offset-level 4,
676
        print page_contents
9373.1.1 by Karl Fogel
Add utilities/community-contributions.py (descended from lpcc.py in
677
678
679
if __name__ == '__main__':
9373.1.4 by Karl Fogel
* utilities/community-contributions.py: Reindent with py-offset-level 4,
680
    main()