whimsy-asf 0.0.75 → 0.0.76

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: fbe033a993bc2428dbed396c0a98fece7b33f82b
4
- data.tar.gz: 17ca4624c5340b1354ff01f540999338d426f2b0
3
+ metadata.gz: 42ee442311d0fc054e4dd8fb6f2bdb4635d03ce1
4
+ data.tar.gz: d556f03175acf95b7882d59b1abfe0b3332c81b5
5
5
  SHA512:
6
- metadata.gz: 801153db8122f912ead1cd2df9a56ad65349bb50c95b1719497dd65da332862c2b3ccbf0e26b92f17981dc1ccb5a34474b2bd1eb4266a6267eb5104b97de34cf
7
- data.tar.gz: 28230fd4887ed1198b94f5d27bba0d2a38eec271ffeaeaa4d010d4aa9b07129a27ab0bb21b621058df05ed551c2ff621218237cff9a003496c3f5fc316d76824
6
+ metadata.gz: 5a00c4fa7f49262e06d04cdffb11929b1955e27afb55b3bc5f2224703a1ccd3d6d609a3656e6a6a3d54677bfcd8e7191641e944888aeebc8359028ab33d7c57c
7
+ data.tar.gz: ed851c6a5edc6fef8325345c96d75d9abfaafa2b1e6753b2de2a8ff3d65ccdb30f35f5bbc74fc0912eb2cdd87227c27d08c4564610b753a06e84721638b2525c
@@ -1 +1 @@
1
- 0.0.75
1
+ 0.0.76
@@ -35,7 +35,7 @@ class ASF::Board::Agenda
35
35
  unless @quick
36
36
  begin
37
37
  committee = ASF::Committee.find(attrs['title'])
38
- attrs['chair_email'] = committee.chair.mail.first
38
+ attrs['chair_email'] = "#{committee.chair.id}@apache.org"
39
39
  attrs['mail_list'] = committee.mail_list
40
40
  attrs.delete('mail_list') if attrs['mail_list'].include? ' '
41
41
 
@@ -6,11 +6,10 @@ module ASF
6
6
  end
7
7
 
8
8
  class Committee < Base
9
- attr_accessor :info, :emeritus, :report, :roster, :established, :chairs,
9
+ attr_accessor :info, :report, :roster, :established, :chairs,
10
10
  :schedule
11
11
  def initialize(*args)
12
12
  @info = []
13
- @emeritus = []
14
13
  @chairs = []
15
14
  @roster = {}
16
15
  super
@@ -49,12 +48,12 @@ module ASF
49
48
  file = "#{board}/committee-info.txt"
50
49
  return unless File.exist? file
51
50
 
52
- list = Hash.new {|hash, name| hash[name] = find(name)}
53
-
54
51
  if @committee_info and File.mtime(file) == @committee_mtime
55
52
  return @committee_info
56
53
  end
57
54
 
55
+ list = Hash.new {|hash, name| hash[name] = find(name)}
56
+
58
57
  @committee_mtime = File.mtime(file)
59
58
  @@svn_change = Time.parse(
60
59
  `svn info #{file}`[/Last Changed Date: (.*) \(/, 1]).gmtime
@@ -86,11 +85,6 @@ module ASF
86
85
  committee = list[@@namemap.call(roster[/(\w.*?)\s+\(/,1])]
87
86
  # get the start date
88
87
  committee.established = roster[/\(est\. (.*?)\)/, 1]
89
- # Extract any emeritus members (now probably redundant)
90
- roster.gsub! /^.*\(\s*emeritus\s*\).*/i do |line|
91
- committee.emeritus += line.scan(/<(.*?)@apache\.org>/).flatten
92
- ''
93
- end
94
88
  # extract the availids (is this used?)
95
89
  committee.info = roster.scan(/<(.*?)@apache\.org>/).flatten
96
90
  # drop (chair) markers and extract 0: name, 1: availid, 2: [date], 3: date
@@ -1,7 +1,10 @@
1
1
  module ASF
2
2
 
3
3
  class ICLA
4
- attr_accessor :id, :legal_name, :name, :email
4
+ # N.B. only id and name should be considered public
5
+ # form and claRef may contain details of the legal name beyond that in the public name
6
+ attr_accessor :id, :legal_name, :name, :email, :form
7
+ attr_accessor :claRef # cla name or SVN revision info
5
8
 
6
9
  @@mtime = nil
7
10
 
@@ -31,9 +34,15 @@ module ASF
31
34
 
32
35
  # load ICLA information for every committer
33
36
  def self.preload
37
+ people = []
34
38
  each do |icla|
35
- ASF::Person.find(icla.id).icla = icla unless icla.id == 'notinaval'
39
+ unless icla.id == 'notinaval'
40
+ person = ASF::Person.find(icla.id)
41
+ people << person
42
+ person.icla = icla
43
+ end
36
44
  end
45
+ people
37
46
  end
38
47
 
39
48
  # find ICLA by ID
@@ -94,12 +103,18 @@ module ASF
94
103
  elsif @@name_index and not @@name_index.empty?
95
104
  @@name_index.values.each(&block)
96
105
  elsif SOURCE and File.exist?(SOURCE)
97
- File.read(SOURCE).scan(/^([-\w]+):(.*?):(.*?):(.*?):/).each do |list|
106
+ File.read(SOURCE).scan(/^([-\w]+):(.*?):(.*?):(.*?):(.*)/).each do |list|
98
107
  icla = ICLA.new()
99
108
  icla.id = list[0]
100
109
  icla.legal_name = list[1]
101
110
  icla.name = list[2]
102
111
  icla.email = list[3]
112
+ icla.form = list[4]
113
+ match = icla.form.match(/^Signed CLA(?:;(\S+)| \((\+=.+)\))/)
114
+ if match
115
+ # match either the cla name or the SVN ref (+=...)
116
+ icla.claRef = match[1] || match[2]
117
+ end
103
118
  block.call(icla)
104
119
  end
105
120
  end
@@ -33,6 +33,7 @@ require 'ldap'
33
33
  require 'weakref'
34
34
  require 'net/http'
35
35
  require 'base64'
36
+ require 'thread'
36
37
 
37
38
  module ASF
38
39
  module LDAP
@@ -48,6 +49,9 @@ module ASF
48
49
  ldaps://ldap2-lw-eu.apache.org:636
49
50
  )
50
51
 
52
+ CONNECT_LOCK = Mutex.new
53
+ HOST_QUEUE = Queue.new
54
+
51
55
  # fetch configuration from apache/infrastructure-puppet
52
56
  def self.puppet_config
53
57
  return @puppet if @puppet
@@ -71,8 +75,13 @@ module ASF
71
75
  end
72
76
 
73
77
  # connect to LDAP
74
- def self.connect
75
- hosts.shuffle.each do |host|
78
+ def self.connect(test = true)
79
+ # Try each host at most once
80
+ hosts.length.times do
81
+ # Ensure we use each host in turn
82
+ hosts.each {|host| HOST_QUEUE.push host} if HOST_QUEUE.empty?
83
+ host = HOST_QUEUE.shift
84
+
76
85
  Wunderbar.info "Connecting to LDAP server: #{host}"
77
86
 
78
87
  begin
@@ -85,7 +94,7 @@ module ASF
85
94
  end
86
95
 
87
96
  # test the connection
88
- ldap.bind
97
+ ldap.bind if test
89
98
 
90
99
  # save the host
91
100
  @host = host
@@ -97,14 +106,18 @@ module ASF
97
106
  end
98
107
 
99
108
  end
109
+
100
110
  Wunderbar.error "Failed to connect to any LDAP host"
101
111
  return nil
102
112
  end
103
113
  end
104
114
 
105
- # backwards compatibility for tools that called this interface
106
- def self.init_ldap
107
- @ldap ||= ASF::LDAP.connect
115
+ # public entry point for establishing a connection safely
116
+ def self.init_ldap(reset = false)
117
+ ASF::LDAP::CONNECT_LOCK.synchronize do
118
+ @ldap = nil if reset
119
+ @ldap ||= ASF::LDAP.connect(!reset)
120
+ end
108
121
  end
109
122
 
110
123
  # determine where ldap.conf resides
@@ -118,23 +131,39 @@ module ASF
118
131
  @ldap || self.init_ldap
119
132
  end
120
133
 
121
- # search with a scope of one
134
+ # search with a scope of one, with automatic retry/failover
122
135
  def self.search_one(base, filter, attrs=nil)
123
- init_ldap unless defined? @ldap
124
- return [] unless @ldap
125
136
 
126
- Wunderbar.info "ldapsearch -x -LLL -b #{base} -s one #{filter} " +
137
+ cmd = "ldapsearch -x -LLL -b #{base} -s one #{filter} " +
127
138
  "#{[attrs].flatten.join(' ')}"
128
-
139
+
140
+ # try once per host, with a minimum of two tries
141
+ attempts_left = [ASF::LDAP.hosts.length, 2].max
129
142
  begin
143
+ attempts_left -= 1
144
+ init_ldap unless @ldap
145
+ return [] unless @ldap
146
+
147
+ target = @ldap.get_option(::LDAP::LDAP_OPT_HOST_NAME) rescue '?'
148
+ Wunderbar.info "[#{target}] #{cmd}"
149
+
130
150
  result = @ldap.search2(base, ::LDAP::LDAP_SCOPE_ONELEVEL, filter, attrs)
131
- rescue
132
- result = []
151
+ rescue Exception => re
152
+ if attempts_left <= 0
153
+ Wunderbar.error "[#{target}] => #{re.inspect} for #{cmd}"
154
+ raise
155
+ else
156
+ Wunderbar.warn "[#{target}] => #{re.inspect} for #{cmd}, retrying ..."
157
+ @ldap.unbind if @ldap.bound? rescue nil
158
+ @ldap = nil # force new connection
159
+ sleep 1
160
+ retry
161
+ end
133
162
  end
134
163
 
135
164
  result.map! {|hash| hash[attrs]} if String === attrs
136
165
 
137
- result
166
+ result.compact
138
167
  end
139
168
 
140
169
  # safely dereference a weakref array attribute. Block provided is
@@ -146,7 +175,9 @@ module ASF
146
175
  rescue WeakRef::RefError
147
176
  value = block.call
148
177
  ensure
149
- if value and not value.instance_of? WeakRef
178
+ if not value or RUBY_VERSION.start_with? '1'
179
+ object.instance_variable_set(attr, value)
180
+ elsif value and not value.instance_of? WeakRef
150
181
  object.instance_variable_set(attr, WeakRef.new(value))
151
182
  end
152
183
  end
@@ -313,7 +344,7 @@ module ASF
313
344
  end
314
345
 
315
346
  def pgp_key_fingerprints
316
- attrs['asf-pgpKeyFingerprint']
347
+ attrs['asf-pgpKeyFingerprint'] || []
317
348
  end
318
349
 
319
350
  def urls
@@ -383,17 +414,18 @@ module ASF
383
414
  end
384
415
 
385
416
  def self.preload
386
- Hash[ASF.search_one(base, "cn=*", %w(dn memberUid modifyTimestamp)).map do |results|
417
+ Hash[ASF.search_one(base, "cn=*", %w(dn memberUid modifyTimestamp createTimestamp)).map do |results|
387
418
  cn = results['dn'].first[/^cn=(.*?),/, 1]
388
419
  group = ASF::Group.find(cn)
389
420
  group.modifyTimestamp = results['modifyTimestamp'].first # it is returned as an array of 1 entry
390
- members = results['memberUid']
391
- group.members = members || []
421
+ group.createTimestamp = results['createTimestamp'].first # it is returned as an array of 1 entry
422
+ members = results['memberUid'] || []
423
+ group.members = members
392
424
  [group, members]
393
425
  end]
394
426
  end
395
427
 
396
- attr_accessor :modifyTimestamp
428
+ attr_accessor :modifyTimestamp, :createTimestamp
397
429
 
398
430
  def members=(members)
399
431
  @members = WeakRef.new(members)
@@ -406,6 +438,24 @@ module ASF
406
438
 
407
439
  members.map {|uid| Person.find(uid)}
408
440
  end
441
+
442
+ def dn
443
+ @dn ||= ASF.search_one(base, "cn=#{name}", 'dn').first.first
444
+ end
445
+
446
+ def remove(people)
447
+ people = Array(people).map(&:id)
448
+ mod = ::LDAP::Mod.new(::LDAP::LDAP_MOD_DELETE, 'memberUid', people)
449
+ ASF.ldap.modify(self.dn, [mod])
450
+ @members = nil
451
+ end
452
+
453
+ def add(people)
454
+ people = Array(people).map(&:dn)
455
+ mod = ::LDAP::Mod.new(::LDAP::LDAP_MOD_ADD, 'memberUid', people)
456
+ ASF.ldap.modify(self.dn, [mod])
457
+ @members = nil
458
+ end
409
459
  end
410
460
 
411
461
  class Committee < Base
@@ -416,17 +466,18 @@ module ASF
416
466
  end
417
467
 
418
468
  def self.preload
419
- Hash[ASF.search_one(base, "cn=*", %w(dn member modifyTimestamp)).map do |results|
469
+ Hash[ASF.search_one(base, "cn=*", %w(dn member modifyTimestamp createTimestamp)).map do |results|
420
470
  cn = results['dn'].first[/^cn=(.*?),/, 1]
421
471
  committee = ASF::Committee.find(cn)
422
472
  committee.modifyTimestamp = results['modifyTimestamp'].first # it is returned as an array of 1 entry
423
- members = results['member']
473
+ committee.createTimestamp = results['createTimestamp'].first # it is returned as an array of 1 entry
474
+ members = results['member'] || []
424
475
  committee.members = members
425
476
  [committee, members]
426
477
  end]
427
478
  end
428
479
 
429
- attr_accessor :modifyTimestamp
480
+ attr_accessor :modifyTimestamp, :createTimestamp
430
481
 
431
482
  def members=(members)
432
483
  @members = WeakRef.new(members)
@@ -443,6 +494,20 @@ module ASF
443
494
  def dn
444
495
  @dn ||= ASF.search_one(base, "cn=#{name}", 'dn').first.first
445
496
  end
497
+
498
+ def remove(people)
499
+ people = Array(people).map(&:dn)
500
+ mod = ::LDAP::Mod.new(::LDAP::LDAP_MOD_DELETE, 'member', people)
501
+ ASF.ldap.modify(self.dn, [mod])
502
+ @members = nil
503
+ end
504
+
505
+ def add(people)
506
+ people = Array(people).map(&:dn)
507
+ mod = ::LDAP::Mod.new(::LDAP::LDAP_MOD_ADD, 'member', people)
508
+ ASF.ldap.modify(self.dn, [mod])
509
+ @members = nil
510
+ end
446
511
  end
447
512
 
448
513
  class Service < Base
@@ -456,21 +521,44 @@ module ASF
456
521
  "cn=#{id},#{self.class.base}"
457
522
  end
458
523
 
524
+ def self.preload
525
+ Hash[ASF.search_one(base, "cn=*", %w(dn member modifyTimestamp createTimestamp)).map do |results|
526
+ cn = results['dn'].first[/^cn=(.*?),/, 1]
527
+ service = ASF::Service.find(cn)
528
+ service.modifyTimestamp = results['modifyTimestamp'].first # it is returned as an array of 1 entry
529
+ service.createTimestamp = results['createTimestamp'].first # it is returned as an array of 1 entry
530
+ members = results['member'] || []
531
+ service.members = members
532
+ [service, members]
533
+ end]
534
+ end
535
+
536
+ attr_accessor :modifyTimestamp, :createTimestamp
537
+
538
+ def members=(members)
539
+ @members = WeakRef.new(members)
540
+ end
541
+
459
542
  def members
460
- ASF.search_one(base, "cn=#{name}", 'member').flatten.
461
- map {|uid| Person.find uid[/uid=(.*?),/,1]}
543
+ members = weakref(:members) do
544
+ ASF.search_one(base, "cn=#{name}", 'member').flatten
545
+ end
546
+
547
+ members.map {|uid| Person.find uid[/uid=(.*?),/,1]}
462
548
  end
463
549
 
464
550
  def remove(people)
465
551
  people = Array(people).map(&:dn)
466
552
  mod = ::LDAP::Mod.new(::LDAP::LDAP_MOD_DELETE, 'member', people)
467
553
  ASF.ldap.modify(self.dn, [mod])
554
+ @members = nil
468
555
  end
469
556
 
470
557
  def add(people)
471
558
  people = Array(people).map(&:dn)
472
559
  mod = ::LDAP::Mod.new(::LDAP::LDAP_MOD_ADD, 'member', people)
473
560
  ASF.ldap.modify(self.dn, [mod])
561
+ @members = nil
474
562
  end
475
563
  end
476
564
 
@@ -479,12 +567,13 @@ module ASF
479
567
  dn = ASF::Person.new(user).dn
480
568
  raise ::LDAP::ResultError.new('Unknown user') unless dn
481
569
 
482
- ASF.ldap.unbind rescue nil
570
+ ASF.ldap.unbind if ASF.ldap.bound? rescue nil
571
+ ldap = ASF.init_ldap(true)
483
572
  if block
484
- ASF.ldap.bind(dn, password, &block)
485
- ASF.init_ldap
573
+ ldap.bind(dn, password, &block)
574
+ ASF.init_ldap(true)
486
575
  else
487
- ASF.ldap.bind(dn, password)
576
+ ldap.bind(dn, password)
488
577
  end
489
578
  end
490
579
 
@@ -509,6 +598,7 @@ module ASF
509
598
 
510
599
  # determine what LDAP hosts are available
511
600
  def self.hosts
601
+ return @hosts if @hosts # cache the hosts list
512
602
  # try whimsy config
513
603
  hosts = Array(ASF::Config.get(:ldap))
514
604
 
@@ -518,23 +608,24 @@ module ASF
518
608
  if File.exist? conf
519
609
  uris = File.read(conf)[/^uri\s+(.*)/i, 1].to_s
520
610
  hosts = uris.scan(/ldaps?:\/\/\S+?:\d+/)
611
+ Wunderbar.debug "Using hosts from LDAP config"
521
612
  end
613
+ else
614
+ Wunderbar.debug "Using hosts from Whimsy config"
522
615
  end
523
616
 
524
617
  # if all else fails, use default list
618
+ Wunderbar.debug "Using default host list" if hosts.empty?
525
619
  hosts = ASF::LDAP::HOSTS if hosts.empty?
526
620
 
527
- hosts
528
- end
529
-
530
- # select LDAP host
531
- def self.host
532
- @host ||= hosts.sample
621
+ hosts.shuffle!
622
+ #Wunderbar.debug "Hosts:\n#{hosts.join(' ')}"
623
+ @hosts = hosts
533
624
  end
534
625
 
535
626
  # query and extract cert from openssl output
536
627
  def self.extract_cert
537
- host = LDAP.host[%r{//(.*?)(/|$)}, 1]
628
+ host = hosts.sample[%r{//(.*?)(/|$)}, 1]
538
629
  puts ['openssl', 's_client', '-connect', host, '-showcerts'].join(' ')
539
630
  out, err, rc = Open3.capture3 'openssl', 's_client',
540
631
  '-connect', host, '-showcerts'
@@ -1,15 +1,19 @@
1
+ require 'weakref'
1
2
 
2
3
  module ASF
3
4
 
4
5
  class Mail
5
6
  def self.list
6
- return @list if @list
7
+ begin
8
+ return Hash[@list.to_a] if @list
9
+ rescue NoMethodError, WeakRef::RefError
10
+ end
7
11
 
8
12
  list = Hash.new
9
13
 
10
14
  # load info from LDAP
11
15
  people = ASF::Person.preload(['mail', 'asf-altEmail'])
12
- people.each do |name, person|
16
+ people.each do |person|
13
17
  (person.mail+person.alt_email).each do |mail|
14
18
  list[mail.downcase] = person
15
19
  end
@@ -28,7 +32,8 @@ module ASF
28
32
  list["#{icla.id.downcase}@apache.org"] ||= person
29
33
  end
30
34
 
31
- @list = list
35
+ @list = WeakRef.new(list)
36
+ list
32
37
  end
33
38
 
34
39
  def self.lists(public_private= false)
@@ -1,3 +1,5 @@
1
+ require 'weakref'
2
+
1
3
  module ASF
2
4
  class Member
3
5
  include Enumerable
@@ -15,10 +17,16 @@ module ASF
15
17
  end
16
18
 
17
19
  def self.list
18
- result = Hash[self.new(true).map {|name, text| [name, {text: text}]}]
20
+ result = Hash[self.new.map {|id, text|
21
+ # extract 1st line and remove any trailing /* comment */
22
+ name = text[/(.*?)\n/, 1].sub(/\s+\/\*.*/,'')
23
+ [id, {text: text, name: name}]
24
+ }]
25
+
19
26
  self.status.each do |name, value|
20
27
  result[name]['status'] = value
21
28
  end
29
+
22
30
  result
23
31
  end
24
32
 
@@ -33,7 +41,11 @@ module ASF
33
41
  end
34
42
 
35
43
  def self.status
36
- return @status if @status
44
+ begin
45
+ return Hash[@status.to_a] if @status
46
+ rescue NoMethodError, WeakRef::RefError
47
+ end
48
+
37
49
  status = {}
38
50
  foundation = ASF::SVN['private/foundation']
39
51
  return status unless foundation
@@ -43,18 +55,15 @@ module ASF
43
55
  header.sub!(/s\n=+/,'')
44
56
  text.scan(/Avail ID: (.*)/).flatten.each {|id| status[id] = header}
45
57
  end
46
- @status = status
47
- end
48
58
 
49
- def initialize(full = nil)
50
- @full = (full.nil? ? ASF::Person.new($USER).asf_member? : full)
59
+ @status = WeakRef.new(status)
60
+ status
51
61
  end
52
62
 
53
63
  def each
54
64
  foundation = ASF::SVN['private/foundation']
55
65
  File.read("#{foundation}/members.txt").split(/^ \*\) /).each do |section|
56
66
  id = section[/Avail ID: (.*)/,1]
57
- section = '' unless @full
58
67
  yield id, section.sub(/\n.*\n===+\s*?\n(.*\n)+.*/,'').strip if id
59
68
  end
60
69
  nil
@@ -79,7 +88,7 @@ module ASF
79
88
 
80
89
  class Person
81
90
  def members_txt
82
- @members_txt ||= ASF::Member.find_text_by_id(name)
91
+ @members_txt ||= ASF::Member.find_text_by_id(id)
83
92
  end
84
93
 
85
94
  def member_emails
@@ -1,9 +1,14 @@
1
+ require 'weakref'
2
+
1
3
  module ASF
2
4
 
3
5
  class Person < Base
4
6
 
5
7
  def self.member_nominees
6
- return @member_nominees if @member_nominees
8
+ begin
9
+ return Hash[@member_nominees.to_a] if @member_nominees
10
+ rescue NoMethodError, WeakRef::RefError
11
+ end
7
12
 
8
13
  meetings = ASF::SVN['private/foundation/Meetings']
9
14
  nominations = Dir["#{meetings}/*/nominated-members.txt"].sort.last.untaint
@@ -22,11 +27,12 @@ module ASF
22
27
  nominees[find(id)] = nomination
23
28
  end
24
29
 
25
- @member_nominees = nominees
30
+ @member_nominees = WeakRef.new(nominees)
31
+ nominees
26
32
  end
27
33
 
28
34
  def member_nomination
29
- Person.member_nominees[self]
35
+ @member_nomination ||= Person.member_nominees[self]
30
36
  end
31
37
  end
32
38
  end
@@ -21,12 +21,14 @@ module ASF
21
21
  def self.decode(env)
22
22
  class << env; attr_accessor :user, :password; end
23
23
 
24
- if env['HTTP_AUTHORIZATION'].to_s.empty?
24
+ auth = env['HTTP_AUTHORIZATION'] || ENV['HTTP_AUTHORIZATION']
25
+
26
+ if auth.to_s.empty?
25
27
  env.user = env['REMOTE_USER'] || ENV['USER'] || Etc.getpwuid.name
26
28
  else
27
29
  require 'base64'
28
- env.user, env.password = Base64.decode64(env['HTTP_AUTHORIZATION'][
29
- /^Basic ([A-Za-z0-9+\/=]+)$/,1].to_s).split(':',2)
30
+ env.user, env.password = Base64.
31
+ decode64(auth[/^Basic ([A-Za-z0-9+\/=]+)$/,1].to_s).split(':',2)
30
32
  end
31
33
 
32
34
  env['REMOTE_USER'] ||= env.user
@@ -52,6 +52,10 @@ module ASF
52
52
  def self.list
53
53
  templates = ASF::SVN['asf/infrastructure/site/trunk/content']
54
54
  file = "#{templates}/index.html"
55
+ if not File.exist?(file)
56
+ Wunderbar.warn "Unable to find 'infrastructure/site/trunk/content'"
57
+ return {}
58
+ end
55
59
  return @@list if not @@list.empty? and File.mtime(file) == @@mtime
56
60
  @@mtime = File.mtime(file)
57
61
 
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.75
4
+ version: 0.0.76
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-25 00:00:00.000000000 Z
11
+ date: 2016-02-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: nokogiri