Show More
@@ -1,179 +1,182 b'' | |||||
1 | #!/usr/bin/env python3 |
|
1 | #!/usr/bin/env python3 | |
2 | # -*- coding: utf-8 -*- |
|
2 | # -*- coding: utf-8 -*- | |
3 |
|
3 | |||
4 | """ |
|
4 | """ | |
5 | Kallithea script for maintaining contributor lists from version control |
|
5 | Kallithea script for maintaining contributor lists from version control | |
6 | history. |
|
6 | history. | |
7 |
|
7 | |||
8 | This script and the data in it is a best effort attempt at reverse engineering |
|
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 |
|
9 | previous attributions and correlate that with version control history while | |
10 | preserving all existing copyright statements and attribution. This script is |
|
10 | preserving all existing copyright statements and attribution. This script is | |
11 | processing and summarizing information found elsewhere - it is not by itself |
|
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 |
|
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 |
|
13 | possible explanations - they are not showing any intent or confirming it is | |
14 | correct. |
|
14 | correct. | |
15 |
|
15 | |||
16 | Three files are generated / modified by this script: |
|
16 | Three files are generated / modified by this script: | |
17 |
|
17 | |||
18 | kallithea/templates/about.html claims to show copyright holders, and the GPL |
|
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 |
|
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. |
|
20 | keep it updated with copyright holders, but do not claim it is a correct list. | |
21 |
|
21 | |||
22 | CONTRIBUTORS has the purpose of giving credit where credit is due and list all |
|
22 | CONTRIBUTORS has the purpose of giving credit where credit is due and list all | |
23 | the contributor names in the source. |
|
23 | the contributor names in the source. | |
24 |
|
24 | |||
25 | kallithea/templates/base/base.html contains the copyright years in the page |
|
25 | kallithea/templates/base/base.html contains the copyright years in the page | |
26 | footer. |
|
26 | footer. | |
27 |
|
27 | |||
28 | Both make a best effort of listing all copyright holders, but revision control |
|
28 | Both make a best effort of listing all copyright holders, but revision control | |
29 | history might be a better and more definitive source. |
|
29 | history might be a better and more definitive source. | |
30 |
|
30 | |||
31 | Contributors are sorted "fairly" by copyright year and amount of |
|
31 | Contributors are sorted "fairly" by copyright year and amount of | |
32 | contribution. |
|
32 | contribution. | |
33 |
|
33 | |||
34 | New contributors are listed, without considering if the contribution contains |
|
34 | New contributors are listed, without considering if the contribution contains | |
35 | copyrightable work. |
|
35 | copyrightable work. | |
36 |
|
36 | |||
37 | When the copyright might belong to a different legal entity than the |
|
37 | When the copyright might belong to a different legal entity than the | |
38 | contributor, the legal entity is given credit too. |
|
38 | contributor, the legal entity is given credit too. | |
39 | """ |
|
39 | """ | |
40 |
|
40 | |||
41 | import os |
|
41 | import os | |
42 | import re |
|
42 | import re | |
43 | from collections import defaultdict |
|
43 | from collections import defaultdict | |
44 |
|
44 | |||
45 | import contributor_data |
|
45 | import contributor_data | |
46 |
|
46 | |||
47 |
|
47 | |||
48 | def sortkey(x): |
|
48 | def sortkey(x): | |
49 | """Return key for sorting contributors "fairly": |
|
49 | """Return key for sorting contributors "fairly": | |
50 | * latest contribution |
|
50 | * latest contribution | |
51 | * first contribution |
|
51 | * first contribution | |
52 | * number of contribution years |
|
52 | * number of contribution years | |
53 | * name (with some unicode normalization) |
|
53 | * name (with some unicode normalization) | |
54 | The entries must be 2-tuples of a list of string years and the name""" |
|
54 | The entries must be 2-tuples of a list of string years and the name""" | |
55 | return (x[0] and -int(x[0][-1]), |
|
55 | years, name = x | |
56 | x[0] and int(x[0][0]), |
|
56 | if not years: | |
57 | -len(x[0]), |
|
57 | years = ['0'] | |
58 | x[1].decode('utf-8').lower().replace('\xe9', 'e').replace('\u0142', 'l') |
|
58 | return (-int(years[-1]), # primarily sort by latest contribution | |
|
59 | int(years[0]), # then sort by first contribution | |||
|
60 | -len(years), # then sort by length of contribution (no gaps) | |||
|
61 | name.lower().replace('\xe9', 'e').replace('\u0142', 'l') # finally sort by name | |||
59 | ) |
|
62 | ) | |
60 |
|
63 | |||
61 |
|
64 | |||
62 | def nice_years(l, dash='-', join=' '): |
|
65 | def nice_years(l, dash='-', join=' '): | |
63 | """Convert a list of years into brief range like '1900-1901, 1921'.""" |
|
66 | """Convert a list of years into brief range like '1900-1901, 1921'.""" | |
64 | if not l: |
|
67 | if not l: | |
65 | return '' |
|
68 | return '' | |
66 | start = end = int(l[0]) |
|
69 | start = end = int(l[0]) | |
67 | ranges = [] |
|
70 | ranges = [] | |
68 | for year in l[1:] + [0]: |
|
71 | for year in l[1:] + [0]: | |
69 | year = int(year) |
|
72 | year = int(year) | |
70 | if year == end + 1: |
|
73 | if year == end + 1: | |
71 | end = year |
|
74 | end = year | |
72 | continue |
|
75 | continue | |
73 | if start == end: |
|
76 | if start == end: | |
74 | ranges.append('%s' % start) |
|
77 | ranges.append('%s' % start) | |
75 | else: |
|
78 | else: | |
76 | ranges.append('%s%s%s' % (start, dash, end)) |
|
79 | ranges.append('%s%s%s' % (start, dash, end)) | |
77 | start = end = year |
|
80 | start = end = year | |
78 | assert start == 0 and end == 0, (start, end) |
|
81 | assert start == 0 and end == 0, (start, end) | |
79 | return join.join(ranges) |
|
82 | return join.join(ranges) | |
80 |
|
83 | |||
81 |
|
84 | |||
82 | def insert_entries( |
|
85 | def insert_entries( | |
83 | filename, |
|
86 | filename, | |
84 | all_entries, |
|
87 | all_entries, | |
85 | no_entries, |
|
88 | no_entries, | |
86 | domain_extra, |
|
89 | domain_extra, | |
87 | split_re, |
|
90 | split_re, | |
88 | normalize_name, |
|
91 | normalize_name, | |
89 | format_f): |
|
92 | format_f): | |
90 | """Update file with contributor information. |
|
93 | """Update file with contributor information. | |
91 | all_entries: list of tuples with year and name |
|
94 | all_entries: list of tuples with year and name | |
92 | no_entries: set of names or name and year tuples to ignore |
|
95 | no_entries: set of names or name and year tuples to ignore | |
93 | domain_extra: map domain name to extra credit name |
|
96 | domain_extra: map domain name to extra credit name | |
94 | split_re: regexp matching the part of file to rewrite |
|
97 | split_re: regexp matching the part of file to rewrite | |
95 | normalize_name: function to normalize names for grouping and display |
|
98 | normalize_name: function to normalize names for grouping and display | |
96 | format_f: function formatting year list and name to a string |
|
99 | format_f: function formatting year list and name to a string | |
97 | """ |
|
100 | """ | |
98 | name_years = defaultdict(set) |
|
101 | name_years = defaultdict(set) | |
99 |
|
102 | |||
100 | for year, name in all_entries: |
|
103 | for year, name in all_entries: | |
101 | if name in no_entries or (name, year) in no_entries: |
|
104 | if name in no_entries or (name, year) in no_entries: | |
102 | continue |
|
105 | continue | |
103 | parts = name.split(' <', 1) |
|
106 | parts = name.split(' <', 1) | |
104 | if len(parts) == 2: |
|
107 | if len(parts) == 2: | |
105 | name = parts[0] + ' <' + parts[1].lower() |
|
108 | name = parts[0] + ' <' + parts[1].lower() | |
106 | domain = name.split('@', 1)[-1].rstrip('>') |
|
109 | domain = name.split('@', 1)[-1].rstrip('>') | |
107 | if domain in domain_extra: |
|
110 | if domain in domain_extra: | |
108 | name_years[domain_extra[domain]].add(year) |
|
111 | name_years[domain_extra[domain]].add(year) | |
109 | name_years[normalize_name(name)].add(year) |
|
112 | name_years[normalize_name(name)].add(year) | |
110 |
|
113 | |||
111 | l = [(list(sorted(year for year in years if year)), name) |
|
114 | l = [(list(sorted(year for year in years if year)), name) | |
112 | for name, years in name_years.items()] |
|
115 | for name, years in name_years.items()] | |
113 | l.sort(key=sortkey) |
|
116 | l.sort(key=sortkey) | |
114 |
|
117 | |||
115 | with open(filename) as f: |
|
118 | with open(filename) as f: | |
116 | pre, post = re.split(split_re, f.read()) |
|
119 | pre, post = re.split(split_re, f.read()) | |
117 |
|
120 | |||
118 | with open(filename, 'w') as f: |
|
121 | with open(filename, 'w') as f: | |
119 | f.write(pre + |
|
122 | f.write(pre + | |
120 | ''.join(format_f(years, name) for years, name in l) + |
|
123 | ''.join(format_f(years, name) for years, name in l) + | |
121 | post) |
|
124 | post) | |
122 |
|
125 | |||
123 |
|
126 | |||
124 | def main(): |
|
127 | def main(): | |
125 | repo_entries = [ |
|
128 | repo_entries = [ | |
126 | (year, contributor_data.name_fixes.get(name) or contributor_data.name_fixes.get(name.rsplit('<', 1)[0].strip()) or name) |
|
129 | (year, contributor_data.name_fixes.get(name) or contributor_data.name_fixes.get(name.rsplit('<', 1)[0].strip()) or name) | |
127 | for year, name in |
|
130 | for year, name in | |
128 | (line.strip().split(' ', 1) |
|
131 | (line.strip().split(' ', 1) | |
129 | for line in os.popen("""hg log -r '::.' -T '{date(date,"%Y")} {author}\n'""").readlines()) |
|
132 | for line in os.popen("""hg log -r '::.' -T '{date(date,"%Y")} {author}\n'""").readlines()) | |
130 | ] |
|
133 | ] | |
131 |
|
134 | |||
132 | insert_entries( |
|
135 | insert_entries( | |
133 | filename='kallithea/templates/about.html', |
|
136 | filename='kallithea/templates/about.html', | |
134 | all_entries=repo_entries + contributor_data.other_about + contributor_data.other, |
|
137 | all_entries=repo_entries + contributor_data.other_about + contributor_data.other, | |
135 | no_entries=contributor_data.no_about, |
|
138 | no_entries=contributor_data.no_about, | |
136 | domain_extra=contributor_data.domain_extra, |
|
139 | domain_extra=contributor_data.domain_extra, | |
137 | split_re=r'(?: <li>Copyright © [^\n]+</li>\n)+', |
|
140 | split_re=r'(?: <li>Copyright © [^\n]+</li>\n)+', | |
138 | normalize_name=lambda name: name.split('<', 1)[0].strip(), |
|
141 | normalize_name=lambda name: name.split('<', 1)[0].strip(), | |
139 | format_f=lambda years, name: ' <li>Copyright © %s, %s</li>\n' % (nice_years(years, '–', ', '), name), |
|
142 | format_f=lambda years, name: ' <li>Copyright © %s, %s</li>\n' % (nice_years(years, '–', ', '), name), | |
140 | ) |
|
143 | ) | |
141 |
|
144 | |||
142 | insert_entries( |
|
145 | insert_entries( | |
143 | filename='CONTRIBUTORS', |
|
146 | filename='CONTRIBUTORS', | |
144 | all_entries=repo_entries + contributor_data.other_contributors + contributor_data.other, |
|
147 | all_entries=repo_entries + contributor_data.other_contributors + contributor_data.other, | |
145 | no_entries=contributor_data.total_ignore, |
|
148 | no_entries=contributor_data.total_ignore, | |
146 | domain_extra=contributor_data.domain_extra, |
|
149 | domain_extra=contributor_data.domain_extra, | |
147 | split_re=r'(?: [^\n]+\n)+', |
|
150 | split_re=r'(?: [^\n]+\n)+', | |
148 | normalize_name=lambda name: name, |
|
151 | normalize_name=lambda name: name, | |
149 | format_f=lambda years, name: (' %s%s%s\n' % (name, ' ' if years else '', nice_years(years))), |
|
152 | format_f=lambda years, name: (' %s%s%s\n' % (name, ' ' if years else '', nice_years(years))), | |
150 | ) |
|
153 | ) | |
151 |
|
154 | |||
152 | insert_entries( |
|
155 | insert_entries( | |
153 | filename='kallithea/templates/base/base.html', |
|
156 | filename='kallithea/templates/base/base.html', | |
154 | all_entries=repo_entries, |
|
157 | all_entries=repo_entries, | |
155 | no_entries=contributor_data.total_ignore, |
|
158 | no_entries=contributor_data.total_ignore, | |
156 | domain_extra={}, |
|
159 | domain_extra={}, | |
157 | split_re=r'(?<=©) .+ (?=by various authors)', |
|
160 | split_re=r'(?<=©) .+ (?=by various authors)', | |
158 | normalize_name=lambda name: '', |
|
161 | normalize_name=lambda name: '', | |
159 | format_f=lambda years, name: ' ' + nice_years(years, '–', ', ') + ' ', |
|
162 | format_f=lambda years, name: ' ' + nice_years(years, '–', ', ') + ' ', | |
160 | ) |
|
163 | ) | |
161 |
|
164 | |||
162 | #docs/conf.py:copyright = u'2010-2016 by various authors, licensed as GPLv3.' |
|
165 | #docs/conf.py:copyright = u'2010-2016 by various authors, licensed as GPLv3.' | |
163 | insert_entries( |
|
166 | insert_entries( | |
164 | filename='docs/conf.py', |
|
167 | filename='docs/conf.py', | |
165 | all_entries=repo_entries, |
|
168 | all_entries=repo_entries, | |
166 | no_entries=contributor_data.total_ignore, |
|
169 | no_entries=contributor_data.total_ignore, | |
167 | domain_extra={}, |
|
170 | domain_extra={}, | |
168 | split_re=r"(?<=copyright = ').+(?= by various authors)", |
|
171 | split_re=r"(?<=copyright = ').+(?= by various authors)", | |
169 | normalize_name=lambda name: '', |
|
172 | normalize_name=lambda name: '', | |
170 | format_f=lambda years, name: nice_years(years, '-', ', '), |
|
173 | format_f=lambda years, name: nice_years(years, '-', ', '), | |
171 | ) |
|
174 | ) | |
172 |
|
175 | |||
173 |
|
176 | |||
174 | if __name__ == '__main__': |
|
177 | if __name__ == '__main__': | |
175 | main() |
|
178 | main() | |
176 |
|
179 | |||
177 |
|
180 | |||
178 | # To list new contributors since last tagging: |
|
181 | # To list new contributors since last tagging: | |
179 | # { hg log -r '::tagged()' -T ' {author}\n {author}\n'; hg log -r '::.' -T ' {author}\n' | sort | uniq; } | sort | uniq -u |
|
182 | # { hg log -r '::tagged()' -T ' {author}\n {author}\n'; hg log -r '::.' -T ' {author}\n' | sort | uniq; } | sort | uniq -u |
General Comments 0
You need to be logged in to leave comments.
Login now