1
1
#!/usr/bin/env python
2
+ # -*- encoding:utf-8 -*-
2
3
"""
3
- git- authors [OPTIONS] REV1..REV2
4
+ List the authors who contributed within a given revision interval::
4
5
5
- List the authors who contributed within a given revision interval.
6
+ python tools/authors.py REV1..REV2
7
+
8
+ `REVx` being a commit hash.
9
+
10
+ To change the name mapping, edit .mailmap on the top-level of the
11
+ repository.
6
12
7
13
"""
8
14
# Author: Pauli Virtanen <[email protected] >. This script is in the public domain.
11
17
import re
12
18
import sys
13
19
import os
20
+ import io
14
21
import subprocess
22
+ import collections
15
23
16
-
17
- from scipy ._lib .six import u , PY3
18
- if PY3 :
19
- stdout_b = sys .stdout .buffer
20
- else :
21
- stdout_b = sys .stdout
22
-
23
-
24
- NAME_MAP = {
25
- u ('Helder' ): u ('Helder Oliveira' ),
26
- }
24
+ stdout_b = sys .stdout .buffer
25
+ MAILMAP_FILE = os .path .join (os .path .dirname (__file__ ), ".." , ".mailmap" )
27
26
28
27
29
28
def main ():
30
29
p = optparse .OptionParser (__doc__ .strip ())
31
30
p .add_option ("-d" , "--debug" , action = "store_true" ,
32
31
help = "print debug output" )
32
+ p .add_option ("-n" , "--new" , action = "store_true" ,
33
+ help = "print debug output" )
33
34
options , args = p .parse_args ()
34
35
35
36
if len (args ) != 1 :
@@ -40,36 +41,38 @@ def main():
40
41
except ValueError :
41
42
p .error ("argument is not a revision range" )
42
43
44
+ NAME_MAP = load_name_map (MAILMAP_FILE )
45
+
43
46
# Analyze log data
44
47
all_authors = set ()
45
- authors = set ()
48
+ authors = collections . Counter ()
46
49
47
50
def analyze_line (line , names , disp = False ):
48
51
line = line .strip ().decode ('utf-8' )
49
52
50
53
# Check the commit author name
51
- m = re .match (u ( '^@@@([^@]*)@@@' ) , line )
54
+ m = re .match (u'^@@@([^@]*)@@@' , line )
52
55
if m :
53
56
name = m .group (1 )
54
57
line = line [m .end ():]
55
58
name = NAME_MAP .get (name , name )
56
59
if disp :
57
60
if name not in names :
58
61
stdout_b .write ((" - Author: %s\n " % name ).encode ('utf-8' ))
59
- names .add ( name )
62
+ names .update (( name ,) )
60
63
61
64
# Look for "thanks to" messages in the commit log
62
- m = re .search (u ( r'([Tt]hanks to|[Cc]ourtesy of) ([A-Z][A-Za-z]*? [A-Z][A-Za-z]*? [A-Z][A-Za-z]*|[A-Z][A-Za-z]*? [A-Z]\. [A-Z][A-Za-z]*|[A-Z][A-Za-z ]*? [A-Z][A-Za-z]*|[a-z0-9]+)($|\.| )' ) , line )
65
+ m = re .search (r'([Tt]hanks to|[Cc]ourtesy of|Co-authored-by: ) ([A-Z][A-Za-z]*? [A-Z][A-Za-z]*? [A-Z][A-Za-z]*|[A-Z][A-Za-z]*? [A-Z]\. [A-Z][A-Za-z]*|[A-Z][A-Za-z ]*? [A-Z][A-Za-z]*|[a-z0-9]+)($|\.| )' , line )
63
66
if m :
64
67
name = m .group (2 )
65
- if name not in (u ( 'this' ) ,):
68
+ if name not in (u'this' ,):
66
69
if disp :
67
70
stdout_b .write (" - Log : %s\n " % line .strip ().encode ('utf-8' ))
68
71
name = NAME_MAP .get (name , name )
69
- names .add ( name )
72
+ names .update (( name ,) )
70
73
71
74
line = line [m .end ():].strip ()
72
- line = re .sub (u ( r'^(and|, and|, ) ' ) , u ( 'Thanks to ' ) , line )
75
+ line = re .sub (r'^(and|, and|, ) ' , u'Thanks to ' , line )
73
76
analyze_line (line .encode ('utf-8' ), names )
74
77
75
78
# Find all authors before the named range
@@ -84,24 +87,39 @@ def analyze_line(line, names, disp=False):
84
87
85
88
# Sort
86
89
def name_key (fullname ):
87
- m = re .search (u ( ' [a-z ]*[A-Za-z-\' ]+$' ) , fullname )
90
+ m = re .search (u' [a-z ]*[A-Za-z-]+$' , fullname )
88
91
if m :
89
92
forename = fullname [:m .start ()].strip ()
90
93
surname = fullname [m .start ():].strip ()
91
94
else :
92
95
forename = ""
93
96
surname = fullname .strip ()
94
- surname = surname .replace ('\' ' , '' )
95
- if surname .startswith (u ('van der ' )):
97
+ if surname .startswith (u'van der ' ):
96
98
surname = surname [8 :]
97
- if surname .startswith (u ( 'de ' ) ):
99
+ if surname .startswith (u'de ' ):
98
100
surname = surname [3 :]
99
- if surname .startswith (u ( 'von ' ) ):
101
+ if surname .startswith (u'von ' ):
100
102
surname = surname [4 :]
101
103
return (surname .lower (), forename .lower ())
102
104
103
- authors = list (authors )
104
- authors .sort (key = name_key )
105
+ # generate set of all new authors
106
+ if vars (options )['new' ]:
107
+ new_authors = set (authors .keys ()).difference (all_authors )
108
+ n_authors = list (new_authors )
109
+ n_authors .sort (key = name_key )
110
+ # Print some empty lines to separate
111
+ stdout_b .write (("\n \n " ).encode ('utf-8' ))
112
+ for author in n_authors :
113
+ stdout_b .write (("- %s\n " % author ).encode ('utf-8' ))
114
+ # return for early exit so we only print new authors
115
+ return
116
+
117
+ try :
118
+ authors .pop ('GitHub' )
119
+ except KeyError :
120
+ pass
121
+ # Order by name. Could order by count with authors.most_common()
122
+ authors = sorted (authors .items (), key = lambda i : name_key (i [0 ]))
105
123
106
124
# Print
107
125
stdout_b .write (b"""
@@ -110,11 +128,14 @@ def name_key(fullname):
110
128
111
129
""" )
112
130
113
- for author in authors :
131
+ for author , count in authors :
132
+ # remove @ if only GH handle is available
133
+ author_clean = author .strip ('@' )
134
+
114
135
if author in all_authors :
115
- stdout_b .write (("* %s \n " % author ).encode ('utf-8' ))
136
+ stdout_b .write ((f "* { author_clean } ( { count } ) \n " ).encode ('utf-8' ))
116
137
else :
117
- stdout_b .write (("* %s +\n " % author ).encode ('utf-8' ))
138
+ stdout_b .write ((f "* { author_clean } ( { count } ) +\n " ).encode ('utf-8' ))
118
139
119
140
stdout_b .write (("""
120
141
A total of %(count)d people contributed to this release.
@@ -123,8 +144,32 @@ def name_key(fullname):
123
144
124
145
""" % dict (count = len (authors ))).encode ('utf-8' ))
125
146
126
- stdout_b .write ("\n NOTE: Check this list manually! It is automatically generated "
127
- "and some names\n may be missing.\n " )
147
+ stdout_b .write (("\n NOTE: Check this list manually! It is automatically generated "
148
+ "and some names\n may be missing.\n " ).encode ('utf-8' ))
149
+
150
+
151
+ def load_name_map (filename ):
152
+ name_map = {}
153
+
154
+ with io .open (filename , 'r' , encoding = 'utf-8' ) as f :
155
+ for line in f :
156
+ line = line .strip ()
157
+ if line .startswith (u"#" ) or not line :
158
+ continue
159
+
160
+ m = re .match (r'^(.*?)\s*<(.*?)>(.*?)\s*<(.*?)>\s*$' , line )
161
+ if not m :
162
+ print ("Invalid line in .mailmap: '{!r}'" .format (line ), file = sys .stderr )
163
+ sys .exit (1 )
164
+
165
+ new_name = m .group (1 ).strip ()
166
+ old_name = m .group (3 ).strip ()
167
+
168
+ if old_name and new_name :
169
+ name_map [old_name ] = new_name
170
+
171
+ return name_map
172
+
128
173
129
174
#------------------------------------------------------------------------------
130
175
# Communicating with Git
@@ -182,6 +227,7 @@ def test(self, command, *a, **kw):
182
227
call = True , ** kw )
183
228
return (ret == 0 )
184
229
230
+
185
231
git = Cmd ("git" )
186
232
187
233
#------------------------------------------------------------------------------
0 commit comments