· 8 years ago · May 25, 2018, 11:22 PM
1require 'rubygems'
2require 'grit'
3
4$KCODE = 'u'
5RAILS_DIR = '/Users/fxn/prj/rails'
6
7# Simple trick to be able to post this script with readable addresses.
8def email(user, domain)
9 user + '@' + domain
10end
11
12# Some people appear in Rails logs under different names, there are nicks,
13# typos, email addresses, shortenings, etc. This is a hand-made list to map
14# them in order to be able to aggregate commits from the same real author.
15SEEN_IN_LOG_ALSO_AS = {
16 'Aliaksey Kandratsenka' => 'Aleksey Kondratenko',
17 'Andrew Kaspick' => [email('akaspick', 'gmail.com'),'akaspick'],
18 'Anthony Eden' => 'aeden',
19 'Austin Ziegler' => 'Thanks to Austin Ziegler for Transaction::Simple',
20 'Blaine' => ['blaine', email('blaine', 'odeo.com')],
21 'Bob Silva' => 'BobSilva',
22 'Brad Greenlee' => 'bgreenlee',
23 'Cheah Chu Yeow' => ['Chu Yeow', 'chuyeow'],
24 'court3nay' => 'courtenay',
25 'Chris McGrath' => [email('c.r.mcgrath', 'gmail.com'), 'c.r.mcgrath', email('chris', 'octopod.info'), 'octopod'],
26 'Damian Janowski' => 'djanowski',
27 'Dan Manges' => 'dcmanges',
28 'Daniel Morrison' => 'danielmorrison',
29 'Daniel Von Fange' => ['Suggested by Daniel Von Fange', 'Spotted by Daniel Von Fange'],
30 'Dave Thomas' => [email('dave', 'pragprog.com'), 'After much pestering from Dave Thomas'],
31 'David Heinemeier Hansson' => 'DHH',
32 'Don Park' => email('don.park', 'gmail.com'),
33 'Duncan Beevers' => 'duncanbeevers',
34 'Eloy Duran' => 'alloy',
35 'Ernesto Jimenez' => 'ernesto.jimenez',
36 'Frederick Cheung' => ['fcheung', 'Fred Cheung', 'frederick.cheung', email('frederick.cheung', 'gmail.com')],
37 'Geoff Buesing' => ['gbuesing', 'Geoffrey Buesing'],
38 'Grant Hollingworth' => email('grant', 'antiflux.org'),
39 'Hongli Lai (Phusion)' => ['Hongli Lai (Phusion', 'FooBarWidget'],
40 'Ian White' => email('ian.w.white', 'gmail.com'),
41 'Isaac Feliu' => 'isaacfeliu',
42 'Jarkko Laine' => [email('jarkko', 'jlaine.net'), 'Jarkko', 'jarkko'],
43 'James Mead' => 'floehopper',
44 'Jeremy Evans' => [email('jeremyevans0', 'gmail.com'), 'jeremyevans'],
45 'Jeremy Kemper' => 'bitsweat',
46 'Jeremy McAnally' => ['jeremymcnally', 'jeremymcanally'],
47 'John Barnette' => 'jbarnette',
48 'Jonathan Weiss' => 'jweiss',
49 'Jordi Bunster' => 'jordi',
50 'José Valim' => 'josevalim',
51 'Josh Knowles' => email('joshknowles', 'gmail.com'),
52 'Josh Peek' => ['josh', 'Josh', 'Joshua Peek', 'joshpeek', email('josh', 'joshpeek.com')],
53 'Josh Starcher' => email('josh.starcher', 'gmail.com'),
54 'Josh Susser' => ['hasmanyjosh', email('josh', 'hasmanythrough.com')],
55 'Juanjo Bazan' => 'juanjo.bazan',
56 'Justin French' => 'justinfrench',
57 'Ken Barker' => email('ken.barker', 'gmail.com'),
58 'Ken Miller' => email('kenneth.miller', 'bitfield.net'),
59 'Kevin Clark' => "Kevin Clark #{email('kevin.clark', 'gmail.com')}",
60 'Manfred Stienstra' => [email('m.stienstra', 'fngtps.com'), 'manfred'],
61 'Marcel Molina' => ['Marcel Molina Jr.', 'Marcel'],
62 'Mark Somerville' => 'Spakman',
63 'Michael Klishin' => 'Michael S. Klishin',
64 'Michael Koziarski' => %w(Koz nzkoz),
65 'Michael Schoen' => 'Michael A. Schoen',
66 'Michael Schubert' => [email('michael', 'schubert'), email('michael', 'schubert.cx')],
67 'Michael Schuerig' => [email('michael', 'schuerig.de'), 'Michael Shuerig'],
68 'Mike Naberezny' => 'mnaberez',
69 'Mikel Lindsaar' => ['mikel', 'raasdnil'],
70 'Mislav Marohnić' => ['mislav', 'mislaw', email('mislav', 'nippur.irb.hr')],
71 'Nick Sieger' => ['nicksieger', email('nicksieger', 'gmail.com')],
72 'Nik Wakelin' => 'nik.wakelin',
73 'Obie Fernandez' => 'ObieFernandez',
74 'Philip Hallstrom' => 'phallstrom',
75 'Pratik Naik' => %w(Pratik lifofifo lifo),
76 'Rick Olson' => ['rick', 'Rick', 'Rick Olsen', email('technoweenie', 'gmail.com')],
77 'Rob Biedenharn' => ['rabiedenharn', email('Rob', 'AgileConsultingLLC.com')],
78 'Robby Russell' => 'robbyrussell',
79 'Ryan Bates' => 'ryanb',
80 'Ryan Davis' => 'zenspider',
81 'Ryan Tomayko' => email('rtomayko', 'gmail.com'),
82 'Sam Granieri' => 'sjgman9',
83 'Stefan Kaes' => [email('skaes', 'web.de'), 'skaes', 'Stephan Kaes', 'Skaes', 'skaes.web.de'],
84 'Steve Purcell' => email('stephen_purcell', 'yahoo.com'),
85 'Tarmo Tänav' => ['tarmo', 'tarmo_t', 'Tarmo Täna'],
86 'Thijs van der Vossen' => ['thijsv', email('thijs', 'vandervossen.net'), email('thijs', 'fngtps.com')],
87 'Tim Pope' => 'tpope',
88 'Tobias Lütke' => ['Tobias Luetke', 'TobiasLuetke'],
89 'Will Bryant' => 'will.bryant',
90 'Xavier Noria' => 'fxn',
91 'Zach Dennis' => 'zdennis',
92}
93
94# Reverse SEEN_IN_LOG_ALSO_AS as NAME_NORMALIZER.
95NAME_NORMALIZER = {}
96SEEN_IN_LOG_ALSO_AS.each do |name, also_as|
97 [*also_as].each { |alt| NAME_NORMALIZER[alt] = name }
98end
99
100def normalize_name(name)
101 name = name.sub(/\s*<[^>]+>/, '') # remove any email address in angles
102 name.strip!
103 NAME_NORMALIZER[name] || name
104end
105
106def commit_from_svn?(commit)
107 commit.message.include?('git-svn-id:')
108end
109
110# When Rails had a svn repo there was a convention for authors: the committer
111# put their name between brackets at the end of the commit or changelog message.
112# For example:
113#
114# Fix case-sensitive validates_uniqueness_of. Closes #11366 [miloops]
115#
116# Of course this is not robust, but it is the best we can get.
117def extract_svn_authors_from_message(message)
118 svn_authors = []
119 if message =~ /\[([^\]]+)\]\s*$/ # end-of-line anchor on purpose
120 # [Adam Milligan, Pratik]
121 # [Rick Olson/Nicholas Seckar]
122 # [Kevin Clark & Jeremy Hopple]
123 $1.split(%r{\s*[,/&]\s*}).each do |c|
124 if looks_like_an_author_name(c)
125 svn_authors << c
126 end
127 end
128 end
129 svn_authors
130end
131
132# Returns a list of authors extracted from the corresponding svn commit.
133def extract_svn_authors(commit)
134 svn_authors = extract_svn_authors_from_message(commit.message)
135 if svn_authors.empty?
136 svn_authors = extract_svn_authors_diffing(commit)
137 end
138 svn_authors = [commit.author.name] if svn_authors.empty?
139 svn_authors.map {|a| normalize_name(a)}.uniq
140end
141
142# Searches for author names in CHANGELOGs.
143def extract_svn_authors_diffing(commit)
144 svn_authors = []
145 Dir.chdir(RAILS_DIR) do
146 in_changelog = false
147 `git show #{commit.id}`.each do |line|
148 if line =~ /^diff --git/
149 in_changelog = false
150 next
151 end
152 if line =~ /^\+\+\+.*changelog$/i
153 in_changelog = true
154 next
155 end
156 if in_changelog && line =~ /^\+\s*\*/
157 svn_authors += extract_svn_authors_from_message(line)
158 end
159 end
160 end
161 svn_authors
162end
163
164# Author name extraction in svn commits returns a few strings we just ignore.
165def looks_like_an_author_name(str)
166 str !~ /\A\d+\z/ && # Remove side effects of [5684]
167 str !~ /\A\s*\z/ &&
168 str != 'See rails ML' &&
169 str != 'subject "Text::Format Licence Exception" on Oct 15' &&
170 str !~ /RubyConf/ && # example: RubyConf '05
171 str !~ /^Includes duplicates of changes/ # example: Includes duplicates of changes from 1.1.4 - 1.2.3
172end
173
174def print_authors(authors)
175 authors.sort {|a,b| b[1] <=> a[1]}.each do |name, count|
176 puts "%4d %s" % [count, name.gsub(/([\w.]+)@([\w.]+)/, "\\1 ~ at ~ \\2")]
177 end
178end
179
180def process_commits!
181 authors = Hash.new(0)
182 repo = Grit::Repo.new(RAILS_DIR)
183 i = 0
184 page_size = 300
185
186 loop do
187 commits = repo.commits('master', page_size, i)
188 break if commits.empty?
189 commits.each do |commit|
190 if commit_from_svn?(commit)
191 extract_svn_authors(commit).each {|a| authors[normalize_name(a)] += 1}
192 else
193 author_name = commit.author.name
194 if looks_like_an_author_name(author_name)
195 authors[normalize_name(author_name)] += 1
196 end
197 end
198 i += 1
199 end
200 end
201
202 print_authors(authors)
203end
204
205process_commits!