whimsy-asf 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/asf.gemspec +32 -0
- data/asf.version +1 -0
- data/lib/whimsy/asf.rb +9 -0
- data/lib/whimsy/asf/agenda.rb +117 -0
- data/lib/whimsy/asf/agenda/attachments.rb +42 -0
- data/lib/whimsy/asf/agenda/back.rb +29 -0
- data/lib/whimsy/asf/agenda/committee.rb +20 -0
- data/lib/whimsy/asf/agenda/exec-officer.rb +34 -0
- data/lib/whimsy/asf/agenda/front.rb +72 -0
- data/lib/whimsy/asf/agenda/minutes.rb +23 -0
- data/lib/whimsy/asf/agenda/special.rb +107 -0
- data/lib/whimsy/asf/auth.rb +28 -0
- data/lib/whimsy/asf/committee.rb +124 -0
- data/lib/whimsy/asf/icla.rb +67 -0
- data/lib/whimsy/asf/ldap.rb +232 -0
- data/lib/whimsy/asf/mail.rb +81 -0
- data/lib/whimsy/asf/member.rb +82 -0
- data/lib/whimsy/asf/nominees.rb +32 -0
- data/lib/whimsy/asf/podlings.rb +23 -0
- data/lib/whimsy/asf/rack.rb +81 -0
- data/lib/whimsy/asf/site.rb +24 -0
- data/lib/whimsy/asf/svn.rb +27 -0
- data/lib/whimsy/asf/watch.rb +41 -0
- metadata +149 -0
@@ -0,0 +1,28 @@
|
|
1
|
+
module ASF
|
2
|
+
|
3
|
+
class Authorization
|
4
|
+
include Enumerable
|
5
|
+
|
6
|
+
def self.find_by_id(value)
|
7
|
+
new.select {|auth, ids| ids.include? value}.map(&:first)
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(file='asf')
|
11
|
+
@file = file
|
12
|
+
end
|
13
|
+
|
14
|
+
def each
|
15
|
+
auth = ASF::SVN['infra/infrastructure/trunk/subversion/authorization']
|
16
|
+
File.read("#{auth}/#{@file}-authorization-template").
|
17
|
+
scan(/^([-\w]+)=(\w.*)$/).each do |pmc, ids|
|
18
|
+
yield pmc, ids.split(',')
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class Person
|
24
|
+
def auth
|
25
|
+
@auths ||= ASF::Authorization.find_by_id(name)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
module ASF
|
2
|
+
|
3
|
+
class Base
|
4
|
+
end
|
5
|
+
|
6
|
+
class Committee < Base
|
7
|
+
attr_accessor :info, :names, :emeritus, :report
|
8
|
+
def initialize(*args)
|
9
|
+
@info = []
|
10
|
+
@emeritus = []
|
11
|
+
super
|
12
|
+
end
|
13
|
+
|
14
|
+
# mapping of committee names to canonical names (generally from ldap)
|
15
|
+
# See also www/roster/committee.cgi
|
16
|
+
@@aliases = Hash.new {|hash, name| name}
|
17
|
+
@@aliases.merge! \
|
18
|
+
'community development' => 'comdev',
|
19
|
+
'conference planning' => 'concom',
|
20
|
+
'conferences' => 'concom',
|
21
|
+
'http server' => 'httpd',
|
22
|
+
'httpserver' => 'httpd',
|
23
|
+
'java community process' => 'jcp',
|
24
|
+
'lucene.net' => 'lucenenet',
|
25
|
+
'quetzalcoatl' => 'quetz',
|
26
|
+
'security team' => 'security',
|
27
|
+
'open climate workbench' => 'climate',
|
28
|
+
'c++ standard library' => 'stdcxx',
|
29
|
+
'travel assistance' => 'tac',
|
30
|
+
'traffic server' => 'trafficserver',
|
31
|
+
'web services' => 'ws',
|
32
|
+
'xml graphics' => 'xmlgraphics'
|
33
|
+
|
34
|
+
@@namemap = Proc.new do |name|
|
35
|
+
cname = @@aliases[name.downcase]
|
36
|
+
cname
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.load_committee_info
|
40
|
+
board = ASF::SVN['private/committers/board']
|
41
|
+
file = "#{board}/committee-info.txt"
|
42
|
+
if @committee_info and File.mtime(file) == @committee_mtime
|
43
|
+
return @committee_info
|
44
|
+
end
|
45
|
+
@committee_mtime = File.mtime(file)
|
46
|
+
|
47
|
+
info = File.read(file).split(/^\* /)
|
48
|
+
head, report = info.shift.split(/^\d\./)[1..2]
|
49
|
+
head.scan(/^\s+(\w.*?)\s\s+.*<(\w+)@apache\.org>/).each do |name, id|
|
50
|
+
find(name).chair = ASF::Person.find(id)
|
51
|
+
end
|
52
|
+
@nonpmcs = head.sub(/.*?also has/m,'').
|
53
|
+
scan(/^\s+(\w.*?)\s\s+.*<\w+@apache\.org>/).flatten.uniq.
|
54
|
+
map {|name| find(name)}
|
55
|
+
|
56
|
+
info.each do |roster|
|
57
|
+
committee = find(@@namemap.call(roster[/(\w.*?)\s+\(/,1]))
|
58
|
+
roster.gsub! /^.*\(\s*emeritus\s*\).*/i do |line|
|
59
|
+
committee.emeritus += line.scan(/<(.*?)@apache\.org>/).flatten
|
60
|
+
''
|
61
|
+
end
|
62
|
+
committee.info = roster.scan(/<(.*?)@apache\.org>/).flatten
|
63
|
+
committee.names = Hash[roster.gsub(/\(\w+\)/, '').
|
64
|
+
scan(/^\s*(.*?)\s*<(.*?)@apache\.org>/).map {|list| list.reverse}]
|
65
|
+
end
|
66
|
+
|
67
|
+
report.scan(/^([^\n]+)\n---+\n(.*?)\n\n/m).each do |period, committees|
|
68
|
+
committees.scan(/^ \s*(.*)/).each do |committee|
|
69
|
+
committee, comment = committee.first.split(/\s+#\s+/,2)
|
70
|
+
committee = find(committee)
|
71
|
+
if comment
|
72
|
+
committee.report = "#{period}: #{comment}"
|
73
|
+
elsif period == 'Next month'
|
74
|
+
committee.report = 'Every month'
|
75
|
+
else
|
76
|
+
committee.report = period
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
@committee_info = ASF::Committee.collection.values
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.nonpmcs
|
85
|
+
@nonpmcs
|
86
|
+
end
|
87
|
+
|
88
|
+
def self.find(name)
|
89
|
+
result = super(@@aliases[name.downcase])
|
90
|
+
result.display_name = name if name =~ /[A-Z]/
|
91
|
+
result
|
92
|
+
end
|
93
|
+
|
94
|
+
def chair
|
95
|
+
Committee.load_committee_info
|
96
|
+
@chair
|
97
|
+
end
|
98
|
+
|
99
|
+
def display_name
|
100
|
+
Committee.load_committee_info
|
101
|
+
@display_name || name
|
102
|
+
end
|
103
|
+
|
104
|
+
def display_name=(name)
|
105
|
+
@display_name ||= name
|
106
|
+
end
|
107
|
+
|
108
|
+
def chair=(person)
|
109
|
+
@chair = person
|
110
|
+
end
|
111
|
+
|
112
|
+
def info=(list)
|
113
|
+
@info = list
|
114
|
+
end
|
115
|
+
|
116
|
+
def names=(hash)
|
117
|
+
@names = hash
|
118
|
+
end
|
119
|
+
|
120
|
+
def nonpmc?
|
121
|
+
Committee.nonpmcs.include? self
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module ASF
|
2
|
+
|
3
|
+
class ICLA
|
4
|
+
include Enumerable
|
5
|
+
|
6
|
+
def self.find_by_id(value)
|
7
|
+
return if value == 'notinavail'
|
8
|
+
new.each do |id, name, email|
|
9
|
+
if id == value
|
10
|
+
return Struct.new(:id, :name, :email).new(id, name, email)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
nil
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.find_by_email(value)
|
17
|
+
value = value.downcase
|
18
|
+
ICLA.new.each do |id, name, email|
|
19
|
+
if email.downcase == value
|
20
|
+
return Struct.new(:id, :name, :email).new(id, name, email)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
nil
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.availids
|
27
|
+
return @availids if @availids
|
28
|
+
availids = []
|
29
|
+
ICLA.new.each {|id, name, email| availids << id unless id == 'notinavail'}
|
30
|
+
@availids = availids
|
31
|
+
end
|
32
|
+
|
33
|
+
def each(&block)
|
34
|
+
officers = ASF::SVN['private/foundation/officers']
|
35
|
+
iclas = File.read("#{officers}/iclas.txt")
|
36
|
+
iclas.scan(/^([-\w]+):.*?:(.*?):(.*?):/).each(&block)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class Person
|
41
|
+
def icla
|
42
|
+
@icla ||= ASF::ICLA.find_by_id(name)
|
43
|
+
end
|
44
|
+
|
45
|
+
def icla?
|
46
|
+
ICLA.availids.include? name
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Search archive for historical records of people who were committers
|
51
|
+
# but never submitted an ICLA (some of which are still ASF members or
|
52
|
+
# members of a PMC).
|
53
|
+
def self.search_archive_by_id(value)
|
54
|
+
require 'net/http'
|
55
|
+
require 'nokogiri'
|
56
|
+
historical_committers = 'http://people.apache.org/~rubys/committers.html'
|
57
|
+
doc = Nokogiri::HTML(Net::HTTP.get(URI.parse(historical_committers)))
|
58
|
+
doc.search('tr').each do |tr|
|
59
|
+
tds = tr.search('td')
|
60
|
+
next unless tds.length == 3
|
61
|
+
return tds[1].text if tds[0].text == value
|
62
|
+
end
|
63
|
+
nil
|
64
|
+
rescue
|
65
|
+
nil
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,232 @@
|
|
1
|
+
require 'wunderbar'
|
2
|
+
require 'rubygems'
|
3
|
+
require 'ldap'
|
4
|
+
|
5
|
+
module ASF
|
6
|
+
|
7
|
+
# determine whether or not the LDAP API can be used
|
8
|
+
def self.init_ldap
|
9
|
+
@ldap = nil
|
10
|
+
begin
|
11
|
+
conf = '/etc/ldap/ldap.conf'
|
12
|
+
host = File.read(conf).scan(/^uri\s+ldaps:\/\/(\S+?):(\d+)/i).first
|
13
|
+
if host
|
14
|
+
Wunderbar.info "Connecting to LDAP server: [ldaps://#{host[0]}:#{host[1]}]"
|
15
|
+
end
|
16
|
+
rescue Errno::ENOENT
|
17
|
+
host = nil
|
18
|
+
Wunderbar.error "No LDAP server defined, there must be a LDAP ldaps:// URI in /etc/ldap/ldap.conf"
|
19
|
+
end
|
20
|
+
begin
|
21
|
+
@ldap = LDAP::SSLConn.new(host.first, host.last.to_i) if host
|
22
|
+
rescue LDAP::ResultError=>re
|
23
|
+
Wunderbar.error "Error binding to LDAP server: message: ["+ re.message + "]"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# search with a scope of one
|
28
|
+
def self.search_one(base, filter, attrs=nil)
|
29
|
+
init_ldap unless defined? @ldap
|
30
|
+
return [] unless @ldap
|
31
|
+
|
32
|
+
Wunderbar.info "ldapsearch -x -LLL -b #{base} -s one #{filter} " +
|
33
|
+
"#{[attrs].flatten.join(' ')}"
|
34
|
+
|
35
|
+
begin
|
36
|
+
result = @ldap.search2(base, LDAP::LDAP_SCOPE_ONELEVEL, filter, attrs)
|
37
|
+
rescue
|
38
|
+
result = []
|
39
|
+
end
|
40
|
+
|
41
|
+
result.map! {|hash| hash[attrs]} if String === attrs
|
42
|
+
|
43
|
+
result
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.pmc_chairs
|
47
|
+
@pmc_chairs ||= Service.find('pmc-chairs').members
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.committers
|
51
|
+
@committers ||= Group.find('committers').members
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.members
|
55
|
+
@members ||= Group.find('member').members
|
56
|
+
end
|
57
|
+
|
58
|
+
class Base
|
59
|
+
attr_reader :name
|
60
|
+
|
61
|
+
def self.base
|
62
|
+
@base
|
63
|
+
end
|
64
|
+
|
65
|
+
def base
|
66
|
+
self.class.base
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.collection
|
70
|
+
@collection ||= Hash.new
|
71
|
+
end
|
72
|
+
|
73
|
+
def self.[] name
|
74
|
+
collection[name] || new(name)
|
75
|
+
end
|
76
|
+
|
77
|
+
def self.find name
|
78
|
+
collection[name] || new(name)
|
79
|
+
end
|
80
|
+
|
81
|
+
def self.new name
|
82
|
+
collection[name] || super
|
83
|
+
end
|
84
|
+
|
85
|
+
def initialize name
|
86
|
+
self.class.collection[name] = self
|
87
|
+
@name = name
|
88
|
+
end
|
89
|
+
|
90
|
+
unless Object.respond_to? :id
|
91
|
+
def id
|
92
|
+
@name
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
class LazyHash < Hash
|
98
|
+
def initialize(&initializer)
|
99
|
+
@initializer = initializer
|
100
|
+
end
|
101
|
+
|
102
|
+
def load
|
103
|
+
return unless @initializer
|
104
|
+
merge! @initializer.call || {}
|
105
|
+
@initializer = super
|
106
|
+
end
|
107
|
+
|
108
|
+
def [](key)
|
109
|
+
result = super
|
110
|
+
if not result and not keys.include? key and @initializer
|
111
|
+
merge! @initializer.call || {}
|
112
|
+
@initializer = nil
|
113
|
+
result = super
|
114
|
+
end
|
115
|
+
result
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
class Person < Base
|
120
|
+
@base = 'ou=people,dc=apache,dc=org'
|
121
|
+
|
122
|
+
def self.list(filter='uid=*')
|
123
|
+
ASF.search_one(base, filter, 'uid').flatten.map {|uid| find(uid)}
|
124
|
+
end
|
125
|
+
|
126
|
+
# pre-fetch a given attribute, for a given list of people
|
127
|
+
def self.preload(attributes, people={})
|
128
|
+
attributes = [attributes].flatten
|
129
|
+
|
130
|
+
if people.empty?
|
131
|
+
filter = "(|#{attributes.map {|attribute| "(#{attribute}=*)"}.join})"
|
132
|
+
else
|
133
|
+
filter = "(|#{people.map {|person| "(uid=#{person.name})"}.join})"
|
134
|
+
end
|
135
|
+
|
136
|
+
zero = Hash[attributes.map {|attribute| [attribute,nil]}]
|
137
|
+
|
138
|
+
data = ASF.search_one(base, filter, attributes + ['uid'])
|
139
|
+
data = Hash[data.map! {|hash| [find(hash['uid'].first), hash]}]
|
140
|
+
data.each {|person, hash| person.attrs.merge!(zero.merge(hash))}
|
141
|
+
|
142
|
+
if people.empty?
|
143
|
+
(collection.values - data.keys).each do |person|
|
144
|
+
person.attrs.merge! zero
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def attrs
|
150
|
+
@attrs ||= LazyHash.new {ASF.search_one(base, "uid=#{name}").first}
|
151
|
+
end
|
152
|
+
|
153
|
+
def public_name
|
154
|
+
cn = [attrs['cn']].flatten.first
|
155
|
+
cn.force_encoding('utf-8') if cn.respond_to? :force_encoding
|
156
|
+
return cn if cn
|
157
|
+
return icla.name if icla
|
158
|
+
ASF.search_archive_by_id(name)
|
159
|
+
end
|
160
|
+
|
161
|
+
def asf_member?
|
162
|
+
ASF::Member.status[name] or ASF.members.include? self
|
163
|
+
end
|
164
|
+
|
165
|
+
def banned?
|
166
|
+
not attrs['loginShell'] or attrs['loginShell'].include? "/usr/bin/false"
|
167
|
+
end
|
168
|
+
|
169
|
+
def mail
|
170
|
+
attrs['mail'] || []
|
171
|
+
end
|
172
|
+
|
173
|
+
def alt_email
|
174
|
+
attrs['asf-altEmail'] || []
|
175
|
+
end
|
176
|
+
|
177
|
+
def pgp_key_fingerprints
|
178
|
+
attrs['asf-pgpKeyFingerprint']
|
179
|
+
end
|
180
|
+
|
181
|
+
def urls
|
182
|
+
attrs['asf-personalURL'] || []
|
183
|
+
end
|
184
|
+
|
185
|
+
def committees
|
186
|
+
Committee.list("member=uid=#{name},#{base}")
|
187
|
+
end
|
188
|
+
|
189
|
+
def groups
|
190
|
+
Group.list("memberUid=#{name}")
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
class Group < Base
|
195
|
+
@base = 'ou=groups,dc=apache,dc=org'
|
196
|
+
|
197
|
+
def self.list(filter='cn=*')
|
198
|
+
ASF.search_one(base, filter, 'cn').flatten.map {|cn| find(cn)}
|
199
|
+
end
|
200
|
+
|
201
|
+
def members
|
202
|
+
ASF.search_one(base, "cn=#{name}", 'memberUid').flatten.
|
203
|
+
map {|uid| Person.find(uid)}
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
class Committee < Base
|
208
|
+
@base = 'ou=pmc,ou=committees,ou=groups,dc=apache,dc=org'
|
209
|
+
|
210
|
+
def self.list(filter='cn=*')
|
211
|
+
ASF.search_one(base, filter, 'cn').flatten.map {|cn| Committee.find(cn)}
|
212
|
+
end
|
213
|
+
|
214
|
+
def members
|
215
|
+
ASF.search_one(base, "cn=#{name}", 'member').flatten.
|
216
|
+
map {|uid| Person.find uid[/uid=(.*?),/,1]}
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
class Service < Base
|
221
|
+
@base = 'ou=groups,ou=services,dc=apache,dc=org'
|
222
|
+
|
223
|
+
def self.list(filter='cn=*')
|
224
|
+
ASF.search_one(base, filter, 'cn').flatten
|
225
|
+
end
|
226
|
+
|
227
|
+
def members
|
228
|
+
ASF.search_one(base, "cn=#{name}", 'member').flatten.
|
229
|
+
map {|uid| Person.find uid[/uid=(.*?),/,1]}
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|