Mercurial > kallithea
comparison scripts/update-copyrights.py @ 6777:5dde581f703f stable
scripts: add update-copyrights.py
This script will automate how the current attribution and copyright listings
are generated, thus also enabling it to be maintained from repository history
in the future.
This doesn't change anything about who has or hasn't contributed or deserves
credit, it just does a best effort attempt at maintaining existing attribution
we inherited and automating it going forward. The automation might expose
existing problems - they should be fixed by adding or removing exceptions in
the script.
author | Mads Kiilerich <mads@kiilerich.com> |
---|---|
date | Mon, 03 Jul 2017 00:54:36 +0200 |
parents | |
children | 665dfa112f2c d06c0566cb23 |
comparison
equal
deleted
inserted
replaced
6776:687152f4f052 | 6777:5dde581f703f |
---|---|
1 #!/usr/bin/env python2 | |
2 # -*- coding: utf-8 -*- | |
3 | |
4 """ | |
5 Kallithea script for maintaining contributor lists from version control | |
6 history. | |
7 | |
8 This script and the data in it is a best effort attempt at reverse engineering | |
9 previous attributions and correlate that with version control history while | |
10 preserving all existing copyright statements and attribution. This script is | |
11 processing and summarizing information found elsewhere - it is not by itself | |
12 making any claims. Comments in the script are an attempt at reverse engineering | |
13 possible explanations - they are not showing any intent or confirming it is | |
14 correct. | |
15 | |
16 Three files are generated / modified by this script: | |
17 | |
18 kallithea/templates/about.html claims to show copyright holders, and the GPL | |
19 license requires such existing "legal notices" to be preserved. We also try to | |
20 keep it updated with copyright holders, but do not claim it is a correct list. | |
21 | |
22 CONTRIBUTORS has the purpose of giving credit where credit is due and list all | |
23 the contributor names in the source. | |
24 | |
25 kallithea/templates/base/base.html contains the copyright years in the page | |
26 footer. | |
27 | |
28 Both make a best effort of listing all copyright holders, but revision control | |
29 history might be a better and more definitive source. | |
30 | |
31 Contributors are sorted "fairly" by copyright year and amount of | |
32 contribution. | |
33 | |
34 New contributors are listed, without considering if the contribution contains | |
35 copyrightable work. | |
36 | |
37 When the copyright might belong to a different legal entity than the | |
38 contributor, the legal entity is given credit too. | |
39 """ | |
40 | |
41 | |
42 # Some committers are so wrong that it doesn't point at any contributor: | |
43 total_ignore = set() | |
44 total_ignore.add('*** failed to import extension hggit: No module named hggit') | |
45 total_ignore.add('<>') | |
46 | |
47 # Normalize some committer names where people have contributed under different | |
48 # names or email addresses: | |
49 name_fixes = {} | |
50 name_fixes['Andrew Shadura'] = "Andrew Shadura <andrew@shadura.me>" | |
51 name_fixes['aparkar'] = "Aparkar <aparkar@icloud.com>" | |
52 name_fixes['Aras Pranckevicius'] = "Aras Pranckevičius <aras@unity3d.com>" | |
53 name_fixes['Augosto Hermann'] = "Augusto Herrmann <augusto.herrmann@planejamento.gov.br>" | |
54 name_fixes['"Bradley M. Kuhn" <bkuhn@ebb.org>'] = "Bradley M. Kuhn <bkuhn@sfconservancy.org>" | |
55 name_fixes['dmitri.kuznetsov'] = "Dmitri Kuznetsov" | |
56 name_fixes['Dmitri Kuznetsov'] = "Dmitri Kuznetsov" | |
57 name_fixes['domruf'] = "Dominik Ruf <dominikruf@gmail.com>" | |
58 name_fixes['Ingo von borstel'] = "Ingo von Borstel <kallithea@planetmaker.de>" | |
59 name_fixes['Jan Heylen'] = "Jan Heylen <heyleke@gmail.com>" | |
60 name_fixes['Jason F. Harris'] = "Jason Harris <jason@jasonfharris.com>" | |
61 name_fixes['Jelmer Vernooij'] = "Jelmer Vernooij <jelmer@samba.org>" | |
62 name_fixes['jfh <jason@jasonfharris.com>'] = "Jason Harris <jason@jasonfharris.com>" | |
63 name_fixes['Leonardo Carneiro<leonardo@unity3d.com>'] = "Leonardo Carneiro <leonardo@unity3d.com>" | |
64 name_fixes['leonardo'] = "Leonardo Carneiro <leonardo@unity3d.com>" | |
65 name_fixes['Leonardo <leo@unity3d.com>'] = "Leonardo Carneiro <leonardo@unity3d.com>" | |
66 name_fixes['Les Peabody'] = "Les Peabody <lpeabody@gmail.com>" | |
67 name_fixes['"Lorenzo M. Catucci" <lorenzo@sancho.ccd.uniroma2.it>'] = "Lorenzo M. Catucci <lorenzo@sancho.ccd.uniroma2.it>" | |
68 name_fixes['Lukasz Balcerzak'] = "Łukasz Balcerzak <lukaszbalcerzak@gmail.com>" | |
69 name_fixes['mao <mao@lins.fju.edu.tw>'] = "Ching-Chen Mao <mao@lins.fju.edu.tw>" | |
70 name_fixes['marcink'] = "Marcin Kuźmiński <marcin@python-works.com>" | |
71 name_fixes['Marcin Kuzminski'] = "Marcin Kuźmiński <marcin@python-works.com>" | |
72 name_fixes['nansenat16@null.tw'] = "nansenat16 <nansenat16@null.tw>" | |
73 name_fixes['Peter Vitt'] = "Peter Vitt <petervitt@web.de>" | |
74 name_fixes['philip.j@hostdime.com'] = "Philip Jameson <philip.j@hostdime.com>" | |
75 name_fixes['Søren Løvborg'] = "Søren Løvborg <sorenl@unity3d.com>" | |
76 name_fixes['Thomas De Schampheleire'] = "Thomas De Schampheleire <thomas.de.schampheleire@gmail.com>" | |
77 name_fixes['Weblate'] = "<>" | |
78 name_fixes['xpol'] = "xpol <xpolife@gmail.com>" | |
79 | |
80 | |
81 # Some committer email address domains that indicate that another entity might | |
82 # hold some copyright too: | |
83 domain_extra = {} | |
84 domain_extra['unity3d.com'] = "Unity Technologies" | |
85 domain_extra['rhodecode.com'] = "RhodeCode GmbH" | |
86 | |
87 # Repository history show some old contributions that traditionally hasn't been | |
88 # listed in about.html - preserve that: | |
89 no_about = set(total_ignore) | |
90 # The following contributors were traditionally not listed in about.html and it | |
91 # seems unclear if the copyright is personal or belongs to a company. | |
92 no_about.add(('Thayne Harbaugh <thayne@fusionio.com>', '2011')) | |
93 no_about.add(('Dies Koper <diesk@fast.au.fujitsu.com>', '2012')) | |
94 no_about.add(('Erwin Kroon <e.kroon@smartmetersolutions.nl>', '2012')) | |
95 no_about.add(('Vincent Caron <vcaron@bearstech.com>', '2012')) | |
96 # These contributors' contributions might be too small to be copyrightable: | |
97 no_about.add(('philip.j@hostdime.com', '2012')) | |
98 no_about.add(('Stefan Engel <mail@engel-stefan.de>', '2012')) | |
99 no_about.add(('Ton Plomp <tcplomp@gmail.com>', '2013')) | |
100 # Was reworked and contributed later and shadowed by other contributions: | |
101 no_about.add(('Sean Farley <sean.michael.farley@gmail.com>', '2013')) | |
102 | |
103 # Preserve contributors listed in about.html but not appearing in repository | |
104 # history: | |
105 other_about = [ | |
106 ("2011", "Aparkar <aparkar@icloud.com>"), | |
107 ("2010", "RhodeCode GmbH"), | |
108 ("2011", "RhodeCode GmbH"), | |
109 ("2012", "RhodeCode GmbH"), | |
110 ("2013", "RhodeCode GmbH"), | |
111 ] | |
112 | |
113 # Preserve contributors listed in CONTRIBUTORS but not appearing in repository | |
114 # history: | |
115 other_contributors = [ | |
116 ("", "Andrew Kesterson <andrew@aklabs.net>"), | |
117 ("", "cejones"), | |
118 ("", "David A. Sjøen <david.sjoen@westcon.no>"), | |
119 ("", "James Rhodes <jrhodes@redpointsoftware.com.au>"), | |
120 ("", "Jonas Oberschweiber <jonas.oberschweiber@d-velop.de>"), | |
121 ("", "larikale"), | |
122 ("", "RhodeCode GmbH"), | |
123 ("", "Sebastian Kreutzberger <sebastian@rhodecode.com>"), | |
124 ("", "Steve Romanow <slestak989@gmail.com>"), | |
125 ("", "SteveCohen"), | |
126 ("", "Thomas <thomas@rhodecode.com>"), | |
127 ("", "Thomas Waldmann <tw-public@gmx.de>"), | |
128 ] | |
129 | |
130 | |
131 import os | |
132 import re | |
133 from collections import defaultdict | |
134 | |
135 | |
136 def sortkey(x): | |
137 """Return key for sorting contributors "fairly": | |
138 * latest contribution | |
139 * first contribution | |
140 * number of contribution years | |
141 * name (with some unicode normalization) | |
142 The entries must be 2-tuples of a list of string years and the unicode name""" | |
143 return (x[0] and -int(x[0][-1]), | |
144 x[0] and int(x[0][0]), | |
145 -len(x[0]), | |
146 x[1].decode('utf8').lower().replace(u'\xe9', u'e').replace(u'\u0142', u'l') | |
147 ) | |
148 | |
149 | |
150 def nice_years(l, dash='-', join=' '): | |
151 """Convert a list of years into brief range like '1900-1901, 1921'.""" | |
152 if not l: | |
153 return '' | |
154 start = end = int(l[0]) | |
155 ranges = [] | |
156 for year in l[1:] + [0]: | |
157 year = int(year) | |
158 if year == end + 1: | |
159 end = year | |
160 continue | |
161 if start == end: | |
162 ranges.append('%s' % start) | |
163 else: | |
164 ranges.append('%s%s%s' % (start, dash, end)) | |
165 start = end = year | |
166 assert start == 0 and end == 0, (start, end) | |
167 return join.join(ranges) | |
168 | |
169 | |
170 def insert_entries( | |
171 filename, | |
172 all_entries, | |
173 no_entries, | |
174 domain_extra, | |
175 split_re, | |
176 normalize_name, | |
177 format_f): | |
178 """Update file with contributor information. | |
179 all_entries: list of tuples with year and name | |
180 no_entries: set of names or name and year tuples to ignore | |
181 domain_extra: map domain name to extra credit name | |
182 split_re: regexp matching the part of file to rewrite | |
183 normalize_name: function to normalize names for grouping and display | |
184 format_f: function formatting year list and name to a string | |
185 """ | |
186 name_years = defaultdict(set) | |
187 | |
188 for year, name in all_entries: | |
189 if name in no_entries or (name, year) in no_entries: | |
190 continue | |
191 domain = name.split('@', 1)[-1].rstrip('>') | |
192 if domain in domain_extra: | |
193 name_years[domain_extra[domain]].add(year) | |
194 name_years[normalize_name(name)].add(year) | |
195 | |
196 l = [(list(sorted(year for year in years if year)), name) | |
197 for name, years in name_years.items()] | |
198 l.sort(key=sortkey) | |
199 | |
200 with file(filename) as f: | |
201 pre, post = re.split(split_re, f.read()) | |
202 | |
203 with file(filename, 'w') as f: | |
204 f.write(pre + | |
205 ''.join(format_f(years, name) for years, name in l) + | |
206 post) | |
207 | |
208 | |
209 def main(): | |
210 repo_entries = [ | |
211 (year, name_fixes.get(name) or name_fixes.get(name.rsplit('<', 1)[0].strip()) or name) | |
212 for year, name in | |
213 (line.strip().split(' ', 1) | |
214 for line in os.popen("""hg log -r '::.' -T '{date(date,"%Y")} {author}\n'""").readlines()) | |
215 ] | |
216 | |
217 insert_entries( | |
218 filename='kallithea/templates/about.html', | |
219 all_entries=repo_entries + other_about, | |
220 no_entries=no_about, | |
221 domain_extra=domain_extra, | |
222 split_re=r'(?: <li>Copyright © [^\n]*</li>\n)*', | |
223 normalize_name=lambda name: name.split('<', 1)[0].strip(), | |
224 format_f=lambda years, name: ' <li>Copyright © %s, %s</li>\n' % (nice_years(years, '–', ', '), name), | |
225 ) | |
226 | |
227 insert_entries( | |
228 filename='CONTRIBUTORS', | |
229 all_entries=repo_entries + other_contributors, | |
230 no_entries=total_ignore, | |
231 domain_extra=domain_extra, | |
232 split_re=r'(?: [^\n]*\n)*', | |
233 normalize_name=lambda name: name, | |
234 format_f=lambda years, name: (' %s%s%s\n' % (name, ' ' if years else '', nice_years(years))), | |
235 ) | |
236 | |
237 insert_entries( | |
238 filename='kallithea/templates/base/base.html', | |
239 all_entries=repo_entries, | |
240 no_entries=total_ignore, | |
241 domain_extra={}, | |
242 split_re=r'(?<=©) .* (?=by various authors)', | |
243 normalize_name=lambda name: '', | |
244 format_f=lambda years, name: ' ' + nice_years(years, '–', ', ') + ' ', | |
245 ) | |
246 | |
247 | |
248 if __name__ == '__main__': | |
249 main() | |
250 | |
251 | |
252 # To list new contributors since last tagging: | |
253 # { hg log -r '::tagged()' -T ' {author}\n {author}\n'; hg log -r '::.' -T ' {author}\n' | sort | uniq; } | sort | uniq -u |