whimsy-asf 0.0.74 → 0.0.75

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 6a3cda4938653e84801388a33933a5ff5753ccd8
4
- data.tar.gz: cbd46d98523e091ae41001899b2b88c086a8bc73
3
+ metadata.gz: fbe033a993bc2428dbed396c0a98fece7b33f82b
4
+ data.tar.gz: 17ca4624c5340b1354ff01f540999338d426f2b0
5
5
  SHA512:
6
- metadata.gz: e098ec455420ce5293b5457c9056c63ceb44f3187a0242f9d5e0798cd314b1ea8f4ea734f042359ec442e9e4e49578ff4bb948e29742b349db8c99a4e3d52620
7
- data.tar.gz: fd7b79fddc1883e8a57248496867ecef4ddd7aaf918d927796df7cacd4c6a9f049906547d74e62420ac749ef90733f5e215248693b5a78673ee4309a1dd662b0
6
+ metadata.gz: 801153db8122f912ead1cd2df9a56ad65349bb50c95b1719497dd65da332862c2b3ccbf0e26b92f17981dc1ccb5a34474b2bd1eb4266a6267eb5104b97de34cf
7
+ data.tar.gz: 28230fd4887ed1198b94f5d27bba0d2a38eec271ffeaeaa4d010d4aa9b07129a27ab0bb21b621058df05ed551c2ff621218237cff9a003496c3f5fc316d76824
data/asf.version CHANGED
@@ -1 +1 @@
1
- 0.0.74
1
+ 0.0.75
data/lib/whimsy/asf.rb CHANGED
@@ -17,4 +17,8 @@ module ASF
17
17
  times = sources.map {|source| File.mtime(source)}
18
18
  times.max.gmtime
19
19
  end
20
+ def self.library_gitinfo
21
+ return @info if @info
22
+ @info = `git show --format="%h %ci" -s HEAD`.chomp
23
+ end
20
24
  end
@@ -59,34 +59,47 @@ module ASF
59
59
  @@svn_change = Time.parse(
60
60
  `svn info #{file}`[/Last Changed Date: (.*) \(/, 1]).gmtime
61
61
 
62
+ # Split the file on lines starting "* ", i.e. the start of each group in section 3
62
63
  info = File.read(file).split(/^\* /)
64
+ # Extract the text before first entry in section 3 and split on section headers,
65
+ # keeping sections 1 (COMMITTEES) and 2 (REPORTING).
63
66
  head, report = info.shift.split(/^\d\./)[1..2]
67
+ # Drop lines which could match group entries
64
68
  head.gsub! /^\s+NAME\s+CHAIR\s*$/,'' # otherwise could match an entry with no e-mail
65
69
 
66
70
  # extract the committee chairs (e-mail address is required here)
71
+ # Note: this includes the non-PMC entries
67
72
  head.scan(/^[ \t]+(\w.*?)[ \t][ \t]+(.*)[ \t]+<(.*?)@apache\.org>/).
68
73
  each do |committee, name, id|
69
74
  list[committee].chairs << {name: name, id: id}
70
75
  end
71
76
 
72
77
  # Extract the non-PMC committees (e-mail address may be absent)
73
- @nonpmcs = head.sub(/.*?also has/m,'').
78
+ # first drop leading text so we only match non-PMCs
79
+ @nonpmcs = head.sub(/.*?also has /m,'').
74
80
  scan(/^[ \t]+(\w.*?)(?:[ \t][ \t]|[ \t]?$)/).flatten.uniq.
75
81
  map {|name| list[name]}
76
82
 
83
+ # for each committee in section 3
77
84
  info.each do |roster|
85
+ # extract the committee name and canonicalise
78
86
  committee = list[@@namemap.call(roster[/(\w.*?)\s+\(/,1])]
87
+ # get the start date
79
88
  committee.established = roster[/\(est\. (.*?)\)/, 1]
89
+ # Extract any emeritus members (now probably redundant)
80
90
  roster.gsub! /^.*\(\s*emeritus\s*\).*/i do |line|
81
91
  committee.emeritus += line.scan(/<(.*?)@apache\.org>/).flatten
82
92
  ''
83
93
  end
94
+ # extract the availids (is this used?)
84
95
  committee.info = roster.scan(/<(.*?)@apache\.org>/).flatten
96
+ # drop (chair) markers and extract 0: name, 1: availid, 2: [date], 3: date
85
97
  committee.roster = Hash[roster.gsub(/\(\w+\)/, '').
86
98
  scan(/^\s*(.*?)\s*<(.*?)@apache\.org>\s+(\[(.*?)\])?/).
87
99
  map {|list| [list[1], {name: list[0], date: list[3]}]}]
88
100
  end
89
101
 
102
+ # process report section
90
103
  report.scan(/^([^\n]+)\n---+\n(.*?)\n\n/m).each do |period, committees|
91
104
  committees.scan(/^ \s*(.*)/).each do |committee|
92
105
  committee, comment = committee.first.split(/\s+#\s+/,2)
@@ -101,7 +114,7 @@ module ASF
101
114
  end
102
115
  end
103
116
 
104
- @committee_info = list.values
117
+ @committee_info = list.values.uniq
105
118
  end
106
119
 
107
120
  def self.nonpmcs
@@ -9,12 +9,25 @@ module ASF
9
9
 
10
10
  @config = YAML.load_file("#@home/.whimsy") rescue {}
11
11
 
12
- # default :svn for backwards compatibility
13
- @config[:svn] ||= ['/srv/svn/*', '/home/whimsysvn/svn/*', "#{@home}/svn/*"]
12
+ # default :svn and :git
13
+ @config[:svn] ||= '/srv/svn/*'
14
+ @config[:git] ||= '/srv/git/*'
15
+
16
+ @config[:lib] ||= []
17
+
18
+ # add gems to lib
19
+ (@config[:gem] || {}).to_a.reverse.each do |name, version|
20
+ begin
21
+ gem = Gem::Specification.find_by_name(name, version)
22
+ @config[:lib] += Dir[gem.lib_dirs_glob]
23
+ rescue Gem::LoadError
24
+ end
25
+ end
14
26
 
15
27
  # add libraries to RUBYLIB, load path
16
28
  (@config[:lib] || []).reverse.each do |lib|
17
29
  next unless File.exist? lib
30
+ lib = File.realpath(lib)
18
31
  ENV['RUBYLIB']=([lib] + ENV['RUBYLIB'].to_s.split(':')).uniq.join(':')
19
32
  $LOAD_PATH.unshift lib.untaint unless $LOAD_PATH.include? lib
20
33
  end
@@ -1,6 +1,38 @@
1
+ #
2
+ # Encapsulate access to LDAP, caching results for performance. For best
3
+ # performance in applications that access large number of objects, make use
4
+ # of the preload methods to pre-fetch multiple objects in a single LDAP
5
+ # call, and rely on the cache to find the objects later.
6
+ #
7
+ # The cache makes heavy use of Weak References internally to enable garbage
8
+ # collection to reclaim objects; among other things, this ensures that
9
+ # LDAP results don't become too stale.
10
+ #
11
+ # Until garbage collection reclaims an object, calls to find methods for the
12
+ # same name is guaranteed to return the same object. Holding on to the
13
+ # results of find or preload calls (by assigning it to a variable) is
14
+ # sufficient to prevent reclaiming of objects.
15
+ #
16
+ # To illustrate, the following is likely to return the same id twice, followed
17
+ # by a new id:
18
+ # puts ASF::Person.find('rubys').__id__
19
+ # puts ASF::Person.find('rubys').__id__
20
+ # GC.start
21
+ # puts ASF::Person.find('rubys').__id__
22
+ #
23
+ # By contrast, the following is guaranteed to produce the same id three times:
24
+ # rubys1 = ASF::Person.find('rubys')
25
+ # rubys2 = ASF::Person.find('rubys')
26
+ # GC.start
27
+ # rubys3 = ASF::Person.find('rubys')
28
+ # puts [rubys1.__id__, rubys2.__id__, rubys3.__id__]
29
+ #
30
+
1
31
  require 'wunderbar'
2
32
  require 'ldap'
3
33
  require 'weakref'
34
+ require 'net/http'
35
+ require 'base64'
4
36
 
5
37
  module ASF
6
38
  module LDAP
@@ -8,41 +40,80 @@ module ASF
8
40
  # https://github.com/apache/infrastructure-puppet/blob/deployment/data/common.yaml (ldapserver::slapd_peers)
9
41
  HOSTS = %w(
10
42
  ldaps://ldap1-us-west.apache.org:636
11
- ldaps://ldap1-eu-central.apache.org:636
43
+ ldaps://ldap1-lw-us.apache.org:636
12
44
  ldaps://ldap2-us-west.apache.org:636
13
- ldaps://ldap1-us-east.apache.org:636
45
+ ldaps://ldap1-lw-eu.apache.org:636
14
46
  ldaps://snappy5.apache.org:636
47
+ ldaps://ldap2-lw-us.apache.org:636
48
+ ldaps://ldap2-lw-eu.apache.org:636
15
49
  )
16
- end
17
50
 
18
- # determine where ldap.conf resides
19
- if Dir.exist? '/etc/openldap'
20
- ETCLDAP = '/etc/openldap'
21
- else
22
- ETCLDAP = '/etc/ldap'
23
- end
51
+ # fetch configuration from apache/infrastructure-puppet
52
+ def self.puppet_config
53
+ return @puppet if @puppet
54
+ file = '/apache/infrastructure-puppet/deployment/data/common.yaml'
55
+ http = Net::HTTP.new('raw.githubusercontent.com', 443)
56
+ http.use_ssl = true
57
+ @puppet = YAML.load(http.request(Net::HTTP::Get.new(file)).body)
58
+ end
24
59
 
25
- # determine whether or not the LDAP API can be used
26
- def self.init_ldap
27
- @ldap = nil
28
- @mtime = Time.now
60
+ # extract the ldapcert from the puppet configuration
61
+ def self.puppet_cert
62
+ puppet_config['ldapclient::ldapcert']
63
+ end
29
64
 
30
- host = ASF::LDAP.host
65
+ # extract the ldap servers from the puppet configuration
66
+ def self.puppet_ldapservers
67
+ puppet_config['ldapserver::slapd_peers'].values.
68
+ map {|host| "ldaps://#{host}:636"}
69
+ rescue
70
+ nil
71
+ end
31
72
 
32
- Wunderbar.info "Connecting to LDAP server: #{host}"
73
+ # connect to LDAP
74
+ def self.connect
75
+ hosts.shuffle.each do |host|
76
+ Wunderbar.info "Connecting to LDAP server: #{host}"
77
+
78
+ begin
79
+ # request connection
80
+ uri = URI.parse(host)
81
+ if uri.scheme == 'ldaps'
82
+ ldap = ::LDAP::SSLConn.new(uri.host, uri.port)
83
+ else
84
+ ldap = ::LDAP::Conn.new(uri.host, uri.port)
85
+ end
86
+
87
+ # test the connection
88
+ ldap.bind
89
+
90
+ # save the host
91
+ @host = host
92
+
93
+ return ldap
94
+ rescue ::LDAP::ResultError => re
95
+ Wunderbar.warn "Error connecting to LDAP server #{host}: " +
96
+ re.message + " (continuing)"
97
+ end
33
98
 
34
- begin
35
- uri = URI.parse(host)
36
- if uri.scheme == 'ldaps'
37
- @ldap = ::LDAP::SSLConn.new(uri.host, uri.port)
38
- else
39
- @ldap = ::LDAP::Conn.new(uri.host, uri.port)
40
99
  end
41
- rescue ::LDAP::ResultError=>re
42
- Wunderbar.error "Error binding to LDAP server: message: ["+ re.message + "]"
100
+ Wunderbar.error "Failed to connect to any LDAP host"
101
+ return nil
43
102
  end
44
103
  end
45
104
 
105
+ # backwards compatibility for tools that called this interface
106
+ def self.init_ldap
107
+ @ldap ||= ASF::LDAP.connect
108
+ end
109
+
110
+ # determine where ldap.conf resides
111
+ if Dir.exist? '/etc/openldap'
112
+ ETCLDAP = '/etc/openldap'
113
+ else
114
+ ETCLDAP = '/etc/ldap'
115
+ end
116
+
46
117
  def self.ldap
47
118
  @ldap || self.init_ldap
48
119
  end
@@ -66,30 +137,34 @@ module ASF
66
137
  result
67
138
  end
68
139
 
69
- def self.refresh(symbol)
70
- if not @mtime or Time.now - @mtime > 300.0
71
- @mtime = Time.now
140
+ # safely dereference a weakref array attribute. Block provided is
141
+ # used when reference is not set or has been reclaimed.
142
+ def self.dereference_weakref(object, attr, &block)
143
+ attr = "@#{attr}"
144
+ value = object.instance_variable_get(attr) || block.call
145
+ value[0..-1]
146
+ rescue WeakRef::RefError
147
+ value = block.call
148
+ ensure
149
+ if value and not value.instance_of? WeakRef
150
+ object.instance_variable_set(attr, WeakRef.new(value))
72
151
  end
152
+ end
73
153
 
74
- if instance_variable_get("#{symbol}_mtime") != @mtime
75
- instance_variable_set("#{symbol}_mtime", @mtime)
76
- instance_variable_set(symbol, nil)
77
- end
154
+ def self.weakref(attr, &block)
155
+ self.dereference_weakref(self, attr, &block)
78
156
  end
79
157
 
80
158
  def self.pmc_chairs
81
- refresh(:@pmc_chairs)
82
- @pmc_chairs ||= Service.find('pmc-chairs').members
159
+ weakref(:pmc_chairs) {Service.find('pmc-chairs').members}
83
160
  end
84
161
 
85
162
  def self.committers
86
- refresh(:@committers)
87
- @committers ||= Group.find('committers').members
163
+ weakref(:committers) {Group.find('committers').members}
88
164
  end
89
165
 
90
166
  def self.members
91
- refresh(:@members)
92
- @members ||= Group.find('member').members
167
+ weakref(:members) {Group.find('member').members}
93
168
  end
94
169
 
95
170
  class Base
@@ -134,6 +209,10 @@ module ASF
134
209
  self
135
210
  end
136
211
 
212
+ def weakref(attr, &block)
213
+ ASF.dereference_weakref(self, attr, &block)
214
+ end
215
+
137
216
  unless Object.respond_to? :id
138
217
  def id
139
218
  @name
@@ -213,12 +292,16 @@ module ASF
213
292
  ASF::Member.status[name] or ASF.members.include? self
214
293
  end
215
294
 
295
+ def asf_officer_or_member?
296
+ asf_member? or ASF.pmc_chairs.include? self
297
+ end
298
+
216
299
  def asf_committer?
217
300
  ASF::Group.new('committers').include? self
218
301
  end
219
302
 
220
303
  def banned?
221
- not attrs['loginShell'] or attrs['loginShell'].include? "/usr/bin/false"
304
+ not attrs['loginShell'] or %w(/usr/bin/false bin/nologin bin/no-cla).any? {|a| attrs['loginShell'].first.include? a}
222
305
  end
223
306
 
224
307
  def mail
@@ -299,9 +382,29 @@ module ASF
299
382
  end
300
383
  end
301
384
 
385
+ def self.preload
386
+ Hash[ASF.search_one(base, "cn=*", %w(dn memberUid modifyTimestamp)).map do |results|
387
+ cn = results['dn'].first[/^cn=(.*?),/, 1]
388
+ group = ASF::Group.find(cn)
389
+ group.modifyTimestamp = results['modifyTimestamp'].first # it is returned as an array of 1 entry
390
+ members = results['memberUid']
391
+ group.members = members || []
392
+ [group, members]
393
+ end]
394
+ end
395
+
396
+ attr_accessor :modifyTimestamp
397
+
398
+ def members=(members)
399
+ @members = WeakRef.new(members)
400
+ end
401
+
302
402
  def members
303
- ASF.search_one(base, "cn=#{name}", 'memberUid').flatten.
304
- map {|uid| Person.find(uid)}
403
+ members = weakref(:members) do
404
+ ASF.search_one(base, "cn=#{name}", 'memberUid').flatten
405
+ end
406
+
407
+ members.map {|uid| Person.find(uid)}
305
408
  end
306
409
  end
307
410
 
@@ -312,9 +415,29 @@ module ASF
312
415
  ASF.search_one(base, filter, 'cn').flatten.map {|cn| Committee.find(cn)}
313
416
  end
314
417
 
418
+ def self.preload
419
+ Hash[ASF.search_one(base, "cn=*", %w(dn member modifyTimestamp)).map do |results|
420
+ cn = results['dn'].first[/^cn=(.*?),/, 1]
421
+ committee = ASF::Committee.find(cn)
422
+ committee.modifyTimestamp = results['modifyTimestamp'].first # it is returned as an array of 1 entry
423
+ members = results['member']
424
+ committee.members = members
425
+ [committee, members]
426
+ end]
427
+ end
428
+
429
+ attr_accessor :modifyTimestamp
430
+
431
+ def members=(members)
432
+ @members = WeakRef.new(members)
433
+ end
434
+
315
435
  def members
316
- ASF.search_one(base, "cn=#{name}", 'member').flatten.
317
- map {|uid| Person.find uid[/uid=(.*?),/,1]}
436
+ members = weakref(:members) do
437
+ ASF.search_one(base, "cn=#{name}", 'member').flatten
438
+ end
439
+
440
+ members.map {|uid| Person.find uid[/uid=(.*?),/,1]}
318
441
  end
319
442
 
320
443
  def dn
@@ -354,59 +477,112 @@ module ASF
354
477
  module LDAP
355
478
  def self.bind(user, password, &block)
356
479
  dn = ASF::Person.new(user).dn
480
+ raise ::LDAP::ResultError.new('Unknown user') unless dn
481
+
482
+ ASF.ldap.unbind rescue nil
357
483
  if block
358
484
  ASF.ldap.bind(dn, password, &block)
485
+ ASF.init_ldap
359
486
  else
360
487
  ASF.ldap.bind(dn, password)
361
488
  end
362
- ASF.init_ldap
363
489
  end
364
490
 
365
- # select LDAP host
366
- def self.host
491
+ # validate HTTP authorization, and optionally invoke a block bound to
492
+ # that user.
493
+ def self.http_auth(string, &block)
494
+ auth = Base64.decode64(string.to_s[/Basic (.*)/, 1] || '')
495
+ user, password = auth.split(':', 2)
496
+ return unless password
497
+
498
+ if block
499
+ self.bind(user, password, &block)
500
+ else
501
+ begin
502
+ ASF::LDAP.bind(user, password) {}
503
+ return ASF::Person.new(user)
504
+ rescue ::LDAP::ResultError
505
+ return nil
506
+ end
507
+ end
508
+ end
509
+
510
+ # determine what LDAP hosts are available
511
+ def self.hosts
367
512
  # try whimsy config
368
- host = ASF::Config.get(:ldap)
513
+ hosts = Array(ASF::Config.get(:ldap))
369
514
 
370
515
  # check system configuration
371
- unless host
516
+ if hosts.empty?
372
517
  conf = "#{ETCLDAP}/ldap.conf"
373
518
  if File.exist? conf
374
- host = File.read(conf)[/^uri\s+(ldaps?:\/\/\S+?:\d+)/i, 1]
519
+ uris = File.read(conf)[/^uri\s+(.*)/i, 1].to_s
520
+ hosts = uris.scan(/ldaps?:\/\/\S+?:\d+/)
375
521
  end
376
522
  end
377
523
 
378
- # if all else fails, pick one at random
379
- host = ASF::LDAP::HOSTS.sample unless host
524
+ # if all else fails, use default list
525
+ hosts = ASF::LDAP::HOSTS if hosts.empty?
526
+
527
+ hosts
528
+ end
380
529
 
381
- host
530
+ # select LDAP host
531
+ def self.host
532
+ @host ||= hosts.sample
382
533
  end
383
534
 
384
535
  # query and extract cert from openssl output
385
- def self.cert
536
+ def self.extract_cert
386
537
  host = LDAP.host[%r{//(.*?)(/|$)}, 1]
387
- query = "openssl s_client -connect #{host} -showcerts"
388
- output = `#{query} < /dev/null 2> /dev/null`
389
- output[/^-+BEGIN.*?\n-+END[^\n]+\n/m]
538
+ puts ['openssl', 's_client', '-connect', host, '-showcerts'].join(' ')
539
+ out, err, rc = Open3.capture3 'openssl', 's_client',
540
+ '-connect', host, '-showcerts'
541
+ out[/^-+BEGIN.*?\n-+END[^\n]+\n/m]
390
542
  end
391
543
 
392
544
  # update /etc/ldap.conf. Usage:
545
+ #
393
546
  # sudo ruby -r whimsy/asf -e "ASF::LDAP.configure"
547
+ #
394
548
  def self.configure
395
- if not File.exist? "#{ETCLDAP}/asf-ldap-client.pem"
396
- File.write "#{ETCLDAP}/asf-ldap-client.pem", self.cert
549
+ cert = Dir["#{ETCLDAP}/asf*-ldap-client.pem"].first
550
+
551
+ # verify/obtain/write the cert
552
+ if not cert
553
+ cert = "#{ETCLDAP}/asf-ldap-client.pem"
554
+ File.write cert, ASF::LDAP.puppet_cert || self.extract_cert
397
555
  end
398
556
 
557
+ # read the current configuration file
399
558
  ldap_conf = "#{ETCLDAP}/ldap.conf"
400
559
  content = File.read(ldap_conf)
401
- unless content.include? 'asf-ldap-client.pem'
402
- content.gsub!(/^TLS_CACERT/, '# TLS_CACERT')
403
- content.gsub!(/^TLS_REQCERT/, '# TLS_REQCERT')
560
+
561
+ # ensure that the right cert is used
562
+ unless content =~ /asf.*-ldap-client\.pem/
563
+ content.gsub!(/^TLS_CACERT/i, '# TLS_CACERT')
404
564
  content += "TLS_CACERT #{ETCLDAP}/asf-ldap-client.pem\n"
405
- content += "uri #{LDAP.host}\n"
565
+ end
566
+
567
+ # provide the URIs of the ldap hosts
568
+ content.gsub!(/^URI/, '# URI')
569
+ content += "uri \n" unless content =~ /^uri /
570
+ content[/uri (.*)\n/, 1] = hosts.join(' ')
571
+
572
+ # verify/set the base
573
+ unless content.include? 'base dc=apache'
574
+ content.gsub!(/^BASE/i, '# BASE')
406
575
  content += "base dc=apache,dc=org\n"
407
- content += "TLS_REQCERT allow\n" if ETCLDAP.include? 'openldap'
408
- File.write(ldap_conf, content)
409
576
  end
577
+
578
+ # ensure TLS_REQCERT is allow (Mac OS/X only)
579
+ if ETCLDAP.include? 'openldap' and not content.include? 'REQCERT allow'
580
+ content.gsub!(/^TLS_REQCERT/i, '# TLS_REQCERT')
581
+ content += "TLS_REQCERT allow\n"
582
+ end
583
+
584
+ # write the configuration if there were any changes
585
+ File.write(ldap_conf, content) unless content == File.read(ldap_conf)
410
586
  end
411
587
  end
412
588
  end
@@ -21,12 +21,12 @@ module ASF
21
21
  def self.decode(env)
22
22
  class << env; attr_accessor :user, :password; end
23
23
 
24
- if env['HTTP_AUTHORIZATION']
24
+ if env['HTTP_AUTHORIZATION'].to_s.empty?
25
+ env.user = env['REMOTE_USER'] || ENV['USER'] || Etc.getpwuid.name
26
+ else
25
27
  require 'base64'
26
28
  env.user, env.password = Base64.decode64(env['HTTP_AUTHORIZATION'][
27
- /^Basic ([A-Za-z0-9+\/=]+)$/,1]).split(':',2)
28
- else
29
- env.user = env['REMOTE_USER'] || ENV['USER'] || Etc.getpwuid.name
29
+ /^Basic ([A-Za-z0-9+\/=]+)$/,1].to_s).split(':',2)
30
30
  end
31
31
 
32
32
  env['REMOTE_USER'] ||= env.user
@@ -189,6 +189,7 @@ module ASF
189
189
  env['SCRIPT_URI'] += '/'
190
190
  end
191
191
  end
192
+
192
193
  return @app.call(env)
193
194
  end
194
195
  end
@@ -12,8 +12,9 @@ module ASF
12
12
 
13
13
  def self.repos
14
14
  @semaphore.synchronize do
15
- svn = ASF::Config.get(:svn).map {|dir| dir.untaint}
15
+ svn = Array(ASF::Config.get(:svn)).map {|dir| dir.untaint}
16
16
  @repos ||= Hash[Dir[*svn].map { |name|
17
+ next unless Dir.exist? name.untaint
17
18
  Dir.chdir name.untaint do
18
19
  out, err, status = Open3.capture3('svn', 'info')
19
20
  if status.success?
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: whimsy-asf
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.74
4
+ version: 0.0.75
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sam Ruby
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-01-12 00:00:00.000000000 Z
11
+ date: 2016-01-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: nokogiri
@@ -104,6 +104,8 @@ extra_rdoc_files: []
104
104
  files:
105
105
  - asf.gemspec
106
106
  - asf.version
107
+ - lib/whimsy/asf.rb
108
+ - lib/whimsy/asf/agenda.rb
107
109
  - lib/whimsy/asf/agenda/attachments.rb
108
110
  - lib/whimsy/asf/agenda/back.rb
109
111
  - lib/whimsy/asf/agenda/committee.rb
@@ -111,7 +113,6 @@ files:
111
113
  - lib/whimsy/asf/agenda/front.rb
112
114
  - lib/whimsy/asf/agenda/minutes.rb
113
115
  - lib/whimsy/asf/agenda/special.rb
114
- - lib/whimsy/asf/agenda.rb
115
116
  - lib/whimsy/asf/auth.rb
116
117
  - lib/whimsy/asf/bundler.rb
117
118
  - lib/whimsy/asf/committee.rb
@@ -126,7 +127,6 @@ files:
126
127
  - lib/whimsy/asf/site.rb
127
128
  - lib/whimsy/asf/svn.rb
128
129
  - lib/whimsy/asf/watch.rb
129
- - lib/whimsy/asf.rb
130
130
  homepage: https://whimsy.apache.org/
131
131
  licenses:
132
132
  - Apache License, Version 2.0
@@ -147,7 +147,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
147
147
  version: '0'
148
148
  requirements: []
149
149
  rubyforge_project:
150
- rubygems_version: 2.0.14
150
+ rubygems_version: 2.5.1
151
151
  signing_key:
152
152
  specification_version: 4
153
153
  summary: Whimsy 'model' of the ASF