blob: d75976b4a8d8f7740071fd4bda0cbcd5f02e873a [file] [log] [blame]
module ASF
class ICLA
# N.B. only id and name should be considered public
# form and claRef may contain details of the legal name beyond that in the public name
attr_accessor :id, :legal_name, :name, :email, :form
attr_accessor :claRef # cla name or SVN revision info
@@mtime = nil
OFFICERS = ASF::SVN['private/foundation/officers']
SOURCE = OFFICERS ? "#{OFFICERS}/iclas.txt" : nil
# flush caches if source file changed
def self.refresh
if not SOURCE or File.mtime(SOURCE) != @@mtime
@@mtime = SOURCE ? File.mtime(SOURCE) : Time.now
@@id_index = nil
@@email_index = nil
@@name_index = nil
@@svn_change = nil
@@availids = nil
end
end
def self.svn_change
self.refresh
if SOURCE
@@svn_change ||= Time.parse(
`svn info #{SOURCE}`[/Last Changed Date: (.*) \(/, 1]).gmtime
end
end
# load ICLA information for every committer
def self.preload
each do |icla|
ASF::Person.find(icla.id).icla = icla unless icla.id == 'notinaval'
end
end
# find ICLA by ID
def self.find_by_id(value)
return if value == 'notinavail' or not SOURCE
refresh
unless @@id_index
@@id_index = {}
each {|icla| @@id_index[icla.id] = icla}
end
@@id_index[value]
end
# find ICLA by email
def self.find_by_email(value)
return unless SOURCE
refresh
unless @@email_index
@@email_index = {}
each {|icla| @@email_index[icla.email.downcase] = icla}
end
@@email_index[value.downcase]
end
# find ICLA by name
def self.find_by_name(value)
return unless SOURCE
refresh
unless @@name_index
@@name_index = {}
each {|icla| @@name_index[icla.name] = icla}
end
@@email_index[value]
end
# list of all ids
def self.availids
return unless SOURCE
refresh
return @@availids if @@availids
availids = []
each {|icla| availids << icla.id unless icla.id == 'notinavail'}
@availids = availids
end
# iterate over all of the ICLAs
def self.each(&block)
refresh
if @@id_index and not @@id_index.empty?
@@id_index.values.each(&block)
elsif @@email_index and not @@email_index.empty?
@@email_index.values.each(&block)
elsif @@name_index and not @@name_index.empty?
@@name_index.values.each(&block)
elsif SOURCE and File.exist?(SOURCE)
File.read(SOURCE).scan(/^([-\w]+):(.*?):(.*?):(.*?):(.*)/).each do |list|
icla = ICLA.new()
icla.id = list[0]
icla.legal_name = list[1]
icla.name = list[2]
icla.email = list[3]
icla.form = list[4]
match = icla.form.match(/^Signed CLA(?:;(\S+)| \((\+=.+)\))/)
if match
# match either the cla name or the SVN ref (+=...)
icla.claRef = match[1] || match[2]
end
block.call(icla)
end
end
end
# sort support
def self.asciize(name)
if name.match /[^\x00-\x7F]/
# digraphs. May be culturally sensitive
name.gsub! /\u00df/, 'ss'
name.gsub! /\u00e4|a\u0308/, 'ae'
name.gsub! /\u00e5|a\u030a/, 'aa'
name.gsub! /\u00e6/, 'ae'
name.gsub! /\u00f1|n\u0303/, 'ny'
name.gsub! /\u00f6|o\u0308/, 'oe'
name.gsub! /\u00fc|u\u0308/, 'ue'
# latin 1
name.gsub! /[\u00e0-\u00e5]/, 'a'
name.gsub! /\u00e7/, 'c'
name.gsub! /[\u00e8-\u00eb]/, 'e'
name.gsub! /[\u00ec-\u00ef]/, 'i'
name.gsub! /[\u00f2-\u00f6]|\u00f8/, 'o'
name.gsub! /[\u00f9-\u00fc]/, 'u'
name.gsub! /[\u00fd\u00ff]/, 'y'
# Latin Extended-A
name.gsub! /[\u0100-\u0105]/, 'a'
name.gsub! /[\u0106-\u010d]/, 'c'
name.gsub! /[\u010e-\u0111]/, 'd'
name.gsub! /[\u0112-\u011b]/, 'e'
name.gsub! /[\u011c-\u0123]/, 'g'
name.gsub! /[\u0124-\u0127]/, 'h'
name.gsub! /[\u0128-\u0131]/, 'i'
name.gsub! /[\u0132-\u0133]/, 'ij'
name.gsub! /[\u0134-\u0135]/, 'j'
name.gsub! /[\u0136-\u0138]/, 'k'
name.gsub! /[\u0139-\u0142]/, 'l'
name.gsub! /[\u0143-\u014b]/, 'n'
name.gsub! /[\u014C-\u0151]/, 'o'
name.gsub! /[\u0152-\u0153]/, 'oe'
name.gsub! /[\u0154-\u0159]/, 'r'
name.gsub! /[\u015a-\u0162]/, 's'
name.gsub! /[\u0162-\u0167]/, 't'
name.gsub! /[\u0168-\u0173]/, 'u'
name.gsub! /[\u0174-\u0175]/, 'w'
name.gsub! /[\u0176-\u0178]/, 'y'
name.gsub! /[\u0179-\u017e]/, 'z'
# denormalized diacritics
name.gsub! /[\u0300-\u036f]/, ''
end
name.strip.gsub /[^\w]+/, '-'
end
SUFFIXES = /^([Jj][Rr]\.?|I{2,3}|I?V|VI{1,3}|[A-Z]\.)$/
# rearrange line in an order suitable for sorting
def self.lname(line)
return '' if line.start_with? '#'
id, name, rest = line.split(':',3)
return '' unless name
# Drop trailing (comment string) or /* comment */
name.sub! /\(.+\)$/,''
name.sub! /\/\*.+\*\/$/,''
return '' if name.strip.empty?
name = name.split.reverse
suffix = (name.shift if name.first =~ SUFFIXES)
suffix += ' ' + name.shift if name.first =~ SUFFIXES
name << name.shift
name << name.shift if name.first=='Lewis' and name.last=='Ship'
name << name.shift if name.first=='Gallardo' and name.last=='Rivera'
name << name.shift if name.first=="S\u00e1nchez" and name.last=='Vega'
# name << name.shift if name.first=='van'
name.last.sub! /^IJ/, 'Ij'
name.unshift(suffix) if suffix
name.map! {|word| asciize(word)}
name = name.reverse.join(' ')
"#{name}:#{rest}"
end
# sort an entire iclas.txt file
def self.sort(source)
headers = source.scan(/^#.*/)
lines = source.scan(/^\w.*/)
headers.join("\n") + "\n" +
lines.sort_by {|line| lname(line + "\n")}.join("\n") + "\n"
end
end
class Person
def icla
@icla ||= ASF::ICLA.find_by_id(name)
end
def icla=(icla)
@icla = icla
end
def icla?
@icla || ICLA.availids.include?(name)
end
end
# Search archive for historical records of people who were committers
# but never submitted an ICLA (some of which are still ASF members or
# members of a PMC).
def self.search_archive_by_id(value)
require 'net/http'
require 'nokogiri'
historical_committers = 'http://people.apache.org/~rubys/committers.html'
doc = Nokogiri::HTML(Net::HTTP.get(URI.parse(historical_committers)))
doc.search('tr').each do |tr|
tds = tr.search('td')
next unless tds.length == 3
return tds[1].text if tds[0].text == value
end
nil
rescue
nil
end
end