whimsy-asf 0.0.76 → 0.0.77

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.
@@ -48,6 +48,30 @@ module ASF
48
48
 
49
49
  public_private ? @lists : @lists.keys
50
50
  end
51
+
52
+ # common configuration for sending mail
53
+ def self.configure
54
+ # fetch overrides
55
+ sendmail = ASF::Config.get(:sendmail)
56
+
57
+ if sendmail
58
+ # convert string keys to symbols
59
+ options = Hash[sendmail.map {|key, value| [key.to_sym, value.untaint]}]
60
+
61
+ # extract delivery method
62
+ method = options.delete(:delivery_method).to_sym
63
+ else
64
+ # provide defaults that work on whimsy-vm* infrastructure. Since
65
+ # procmail is configured with a self-signed certificate, verification
66
+ # isn't a possibility
67
+ method = :smtp
68
+ options = {openssl_verify_mode: 'none'}
69
+ end
70
+
71
+ ::Mail.defaults do
72
+ delivery_method method, options
73
+ end
74
+ end
51
75
  end
52
76
 
53
77
  class Person < Base
@@ -93,7 +117,7 @@ module ASF
93
117
  when 'executive assistant'
94
118
  'ea@apache.org'
95
119
  when 'legal affairs'
96
- 'legal-discuss@apache.org'
120
+ 'legal-internal@apache.org'
97
121
  when 'marketing and publicity'
98
122
  'press@apache.org'
99
123
  when 'tac'
@@ -3,7 +3,8 @@ require 'weakref'
3
3
  module ASF
4
4
  class Member
5
5
  include Enumerable
6
- attr_accessor :full
6
+ @@text = nil
7
+ @@mtime = 0
7
8
 
8
9
  def self.find_text_by_id(value)
9
10
  new.each do |id, text|
@@ -42,14 +43,14 @@ module ASF
42
43
 
43
44
  def self.status
44
45
  begin
46
+ @status = nil if @mtime != @@mtime
47
+ @mtime = @@mtime
45
48
  return Hash[@status.to_a] if @status
46
- rescue NoMethodError, WeakRef::RefError
49
+ rescue
47
50
  end
48
51
 
49
52
  status = {}
50
- foundation = ASF::SVN['private/foundation']
51
- return status unless foundation
52
- sections = File.read("#{foundation}/members.txt").split(/(.*\n===+)/)
53
+ sections = ASF::Member.text.split(/(.*\n===+)/)
53
54
  sections.shift(3)
54
55
  sections.each_slice(2) do |header, text|
55
56
  header.sub!(/s\n=+/,'')
@@ -61,8 +62,7 @@ module ASF
61
62
  end
62
63
 
63
64
  def each
64
- foundation = ASF::SVN['private/foundation']
65
- File.read("#{foundation}/members.txt").split(/^ \*\) /).each do |section|
65
+ ASF::Member.text.split(/^ \*\) /).each do |section|
66
66
  id = section[/Avail ID: (.*)/,1]
67
67
  yield id, section.sub(/\n.*\n===+\s*?\n(.*\n)+.*/,'').strip if id
68
68
  end
@@ -84,15 +84,81 @@ module ASF
84
84
  file = "#{foundation}/members.txt"
85
85
  return Time.parse(`svn info #{file}`[/Last Changed Date: (.*) \(/, 1]).gmtime
86
86
  end
87
+
88
+ # sort an entire members.txt file
89
+ def self.sort(source)
90
+ # split into sections
91
+ sections = source.split(/^([A-Z].*\n===+\n\n)/)
92
+
93
+ # sort sections that contain names
94
+ sections.map! do |section|
95
+ next section unless section =~ /^\s\*\)\s/
96
+
97
+ # split into entries, and normalize those entries
98
+ entries = section.split(/^\s\*\)\s/)
99
+ header = entries.shift
100
+ entries.map! {|entry| " *) " + entry.strip + "\n\n"}
101
+
102
+ # sort the entries
103
+ entries.sort_by! do |entry|
104
+ ASF::Person.sortable_name(entry[/\)\s(.*?)\s*(\/\*|$)/, 1])
105
+ end
106
+
107
+ header + entries.join
108
+ end
109
+
110
+ sections.join
111
+ end
112
+
113
+ # cache the contents of members.txt. Primary purpose isn't performance,
114
+ # but rather to have a local copy that can be updated and used until
115
+ # the svn working copy catches up
116
+ def self.text
117
+ foundation = ASF::SVN.find('private/foundation')
118
+ return nil unless foundation
119
+
120
+ begin
121
+ text = @@text[0..-1] if @@text
122
+ rescue WeakRef::RefError
123
+ @@mtime = 0
124
+ end
125
+
126
+ if File.mtime("#{foundation}/members.txt").to_i > @@mtime.to_i
127
+ @@mtime = File.mtime("#{foundation}/members.txt")
128
+ text = File.read("#{foundation}/members.txt")
129
+ @@text = WeakRef.new(text)
130
+ end
131
+
132
+ text
133
+ end
134
+
135
+ # update local copy of members.txt
136
+ def self.text=(text)
137
+ # normalize text: sort and update active count
138
+ text = ASF::Member.sort(text)
139
+ pattern = /^Active.*?^=+\n+(.*?)^Emeritus/m
140
+ text[/We now number (\d+) active members\./, 1] =
141
+ text[pattern].scan(/^\s\*\)\s/).length.to_s
142
+
143
+ # save
144
+ @@mtime = Time.now
145
+ @@text = WeakRef.new(text)
146
+ end
87
147
  end
88
148
 
89
149
  class Person
90
- def members_txt
150
+ def members_txt(full = false)
151
+ prefix, suffix = " *) ", "\n\n" if full
91
152
  @members_txt ||= ASF::Member.find_text_by_id(id)
153
+ "#{prefix}#{@members_txt}#{suffix}" if @members_txt
92
154
  end
93
155
 
94
156
  def member_emails
95
157
  ASF::Member.emails(members_txt)
96
158
  end
159
+
160
+ def member_name
161
+ members_txt[/(\w.*?)\s*(\/|$)/, 1] if members_txt
162
+ end
97
163
  end
98
164
  end
@@ -7,7 +7,7 @@ module ASF
7
7
  def self.member_nominees
8
8
  begin
9
9
  return Hash[@member_nominees.to_a] if @member_nominees
10
- rescue NoMethodError, WeakRef::RefError
10
+ rescue
11
11
  end
12
12
 
13
13
  meetings = ASF::SVN['private/foundation/Meetings']
@@ -0,0 +1,81 @@
1
+ #
2
+ # support for sorting of names
3
+ #
4
+
5
+ module ASF
6
+
7
+ class Person
8
+ # sort support
9
+
10
+ def self.asciize(name)
11
+ if name.match /[^\x00-\x7F]/
12
+ # digraphs. May be culturally sensitive
13
+ name.gsub! /\u00df/, 'ss'
14
+ name.gsub! /\u00e4|a\u0308/, 'ae'
15
+ name.gsub! /\u00e5|a\u030a/, 'aa'
16
+ name.gsub! /\u00e6/, 'ae'
17
+ name.gsub! /\u00f1|n\u0303/, 'ny'
18
+ name.gsub! /\u00f6|o\u0308/, 'oe'
19
+ name.gsub! /\u00fc|u\u0308/, 'ue'
20
+
21
+ # latin 1
22
+ name.gsub! /\u00c9/, 'e'
23
+ name.gsub! /\u00d3/, 'o'
24
+ name.gsub! /[\u00e0-\u00e5]/, 'a'
25
+ name.gsub! /\u00e7/, 'c'
26
+ name.gsub! /[\u00e8-\u00eb]/, 'e'
27
+ name.gsub! /[\u00ec-\u00ef]/, 'i'
28
+ name.gsub! /[\u00f2-\u00f6]|\u00f8/, 'o'
29
+ name.gsub! /[\u00f9-\u00fc]/, 'u'
30
+ name.gsub! /[\u00fd\u00ff]/, 'y'
31
+
32
+ # Latin Extended-A
33
+ name.gsub! /[\u0100-\u0105]/, 'a'
34
+ name.gsub! /[\u0106-\u010d]/, 'c'
35
+ name.gsub! /[\u010e-\u0111]/, 'd'
36
+ name.gsub! /[\u0112-\u011b]/, 'e'
37
+ name.gsub! /[\u011c-\u0123]/, 'g'
38
+ name.gsub! /[\u0124-\u0127]/, 'h'
39
+ name.gsub! /[\u0128-\u0131]/, 'i'
40
+ name.gsub! /[\u0132-\u0133]/, 'ij'
41
+ name.gsub! /[\u0134-\u0135]/, 'j'
42
+ name.gsub! /[\u0136-\u0138]/, 'k'
43
+ name.gsub! /[\u0139-\u0142]/, 'l'
44
+ name.gsub! /[\u0143-\u014b]/, 'n'
45
+ name.gsub! /[\u014C-\u0151]/, 'o'
46
+ name.gsub! /[\u0152-\u0153]/, 'oe'
47
+ name.gsub! /[\u0154-\u0159]/, 'r'
48
+ name.gsub! /[\u015a-\u0162]/, 's'
49
+ name.gsub! /[\u0162-\u0167]/, 't'
50
+ name.gsub! /[\u0168-\u0173]/, 'u'
51
+ name.gsub! /[\u0174-\u0175]/, 'w'
52
+ name.gsub! /[\u0176-\u0178]/, 'y'
53
+ name.gsub! /[\u0179-\u017e]/, 'z'
54
+
55
+ # denormalized diacritics
56
+ name.gsub! /[\u0300-\u036f]/, ''
57
+ end
58
+
59
+ name.strip.gsub /[^\w]+/, '-'
60
+ end
61
+
62
+ SUFFIXES = /^([Jj][Rr]\.?|I{2,3}|I?V|VI{1,3}|[A-Z]\.)$/
63
+
64
+ # rearrange line in an order suitable for sorting
65
+ def self.sortable_name(name)
66
+ name = name.split.reverse
67
+ suffix = (name.shift if name.first =~ SUFFIXES)
68
+ suffix += ' ' + name.shift if name.first =~ SUFFIXES
69
+ name << name.shift
70
+ # name << name.shift if name.first=='van'
71
+ name.last.sub! /^IJ/, 'Ij'
72
+ name.unshift(suffix) if suffix
73
+ name.map! {|word| asciize(word)}
74
+ name.reverse.join(' ').downcase
75
+ end
76
+
77
+ def sortable_name
78
+ Person.sortable_name(self.public_name)
79
+ end
80
+ end
81
+ end
@@ -1,10 +1,13 @@
1
1
  require 'nokogiri'
2
+ require 'date'
2
3
  require_relative '../asf'
3
4
 
4
5
  module ASF
5
- class Podlings
6
+ class Podling
6
7
  include Enumerable
8
+ attr_accessor :name, :status, :description, :mentors, :champion, :reporting
7
9
 
10
+ # three consecutive months, starting with this one
8
11
  def quarter
9
12
  [
10
13
  Date.today.strftime('%B'),
@@ -13,36 +16,116 @@ module ASF
13
16
  ]
14
17
  end
15
18
 
16
- def each
17
- incubator_content = ASF::SVN['asf/incubator/public/trunk/content']
18
- podlings = Nokogiri::XML(File.read("#{incubator_content}/podlings.xml"))
19
- podlings.search('podling').map do |node|
20
-
21
- reporting = node.at('reporting')
22
- if reporting
23
- group = reporting['group']
24
- monthly = reporting.text.split(/,\s*/) if reporting['monthly']
25
- reporting = %w(January April July October) if group == '1'
26
- reporting = %w(February May August November) if group == '2'
27
- reporting = %w(March June September December) if group == '3'
28
- reporting.rotate! until quarter.include? reporting.first
29
-
30
- if monthly
31
- monthly.shift until monthly.empty? or quarter.include? monthly.first
32
- reporting = (monthly + reporting).uniq
33
- end
19
+ # create a podling from a Nokogiri node built from podlings.xml
20
+ def initialize(node)
21
+ @name = node['name']
22
+ @resource = node['resource']
23
+ @status = node['status']
24
+ @enddate = node['enddate']
25
+ @startdate = node['startdate']
26
+ @description = node.at('description').text
27
+ @mentors = node.search('mentor').map {|mentor| mentor['username']}
28
+ @champion = node.at('champion')['availid'] if node.at('champion')
29
+
30
+ @reporting = node.at('reporting')
31
+ end
32
+
33
+ # map resource to name
34
+ def name
35
+ @resource
36
+ end
37
+
38
+ # also map resource to id
39
+ def id
40
+ @resource
41
+ end
42
+
43
+ # map name to display_name
44
+ def display_name
45
+ @name || @resource
46
+ end
47
+
48
+ # parse startdate
49
+ def startdate
50
+ return unless @startdate
51
+ return Date.parse("#@startdate-15") if @startdate.length < 8
52
+ Date.parse(@startdate)
53
+ rescue ArgumentError
54
+ nil
55
+ end
56
+
57
+ # parse enddate
58
+ def enddate
59
+ return unless @enddate
60
+ return Date.parse("#@enddate-15") if @enddate.length < 8
61
+ Date.parse(@enddate)
62
+ rescue ArgumentError
63
+ nil
64
+ end
65
+
66
+ # lazy evaluation of reporting
67
+ def reporting
68
+ if @reporting.instance_of? Nokogiri::XML::Element
69
+ group = @reporting['group']
70
+ monthly = @reporting.text.split(/,\s*/) if @reporting['monthly']
71
+ @reporting = %w(January April July October) if group == '1'
72
+ @reporting = %w(February May August November) if group == '2'
73
+ @reporting = %w(March June September December) if group == '3'
74
+ @reporting.rotate! until quarter.include? @reporting.first
75
+
76
+ if monthly
77
+ monthly.shift until monthly.empty? or quarter.include? monthly.first
78
+ @reporting = (monthly + @reporting).uniq
34
79
  end
80
+ end
81
+
82
+ @reporting
83
+ end
84
+
85
+ # list of podlings
86
+ def self.list
87
+ incubator_content = ASF::SVN['asf/incubator/public/trunk/content']
88
+ podlings_xml = "#{incubator_content}/podlings.xml"
35
89
 
36
- data = {
37
- name: node['name'],
38
- status: node['status'],
39
- reporting: reporting,
40
- description: node.at('description').text,
41
- mentors: node.search('mentor').map {|mentor| mentor['username']}
42
- }
43
- data[:champion] = node.at('champion')['availid'] if node.at('champion')
44
- yield node['resource'], data
90
+ if @mtime != File.mtime(podlings_xml)
91
+ @list = []
92
+ podlings = Nokogiri::XML(File.read(podlings_xml))
93
+ podlings.search('podling').map do |node|
94
+ @list << new(node)
95
+ end
45
96
  end
97
+
98
+ @list
99
+ end
100
+
101
+ # find a podling by name
102
+ def self.find(name)
103
+ list.find {|podling| podling.name == name}
104
+ end
105
+
106
+ # below is for backwards compatibility
107
+
108
+ # make class itself enumerable
109
+ class << self
110
+ include Enumerable
111
+ end
112
+
113
+ # return the entire list as a hash
114
+ def self.to_h
115
+ Hash[self.to_a]
116
+ end
117
+
118
+ # provide a list of podling names and descriptions
119
+ def self.each(&block)
120
+ list.each {|podling| block.call podling.name, podling}
121
+ end
122
+
123
+ # allow attributes to be accessed as hash
124
+ def [](name)
125
+ return self.send name if self.respond_to? name
46
126
  end
47
127
  end
128
+
129
+ # more backwards compatibility
130
+ Podlings = Podling
48
131
  end
@@ -6,15 +6,15 @@ require 'thread'
6
6
  module ASF
7
7
  module Auth
8
8
  DIRECTORS = {
9
- 'rbowen' => 'rb',
10
9
  'curcuru' => 'sc',
11
10
  'bdelacretaz' => 'bd',
11
+ 'isabel' => 'id',
12
+ 'marvin' => 'mh',
12
13
  'jim' => 'jj',
13
14
  'mattmann' => 'cm',
14
- 'ke4qqq' => 'dn',
15
15
  'brett' => 'bp',
16
- 'rubys' => 'sr',
17
- 'gstein' => 'gs'
16
+ 'gstein' => 'gs',
17
+ 'markt' => 'mt'
18
18
  }
19
19
 
20
20
  # decode HTTP authorization, when present
@@ -53,7 +53,7 @@ module ASF
53
53
  templates = ASF::SVN['asf/infrastructure/site/trunk/content']
54
54
  file = "#{templates}/index.html"
55
55
  if not File.exist?(file)
56
- Wunderbar.warn "Unable to find 'infrastructure/site/trunk/content'"
56
+ Wunderbar.error "Unable to find 'infrastructure/site/trunk/content'"
57
57
  return {}
58
58
  end
59
59
  return @@list if not @@list.empty? and File.mtime(file) == @@mtime
@@ -1,6 +1,8 @@
1
1
  require 'uri'
2
2
  require 'thread'
3
3
  require 'open3'
4
+ require 'fileutils'
5
+ require 'tmpdir'
4
6
 
5
7
  module ASF
6
8
 
@@ -30,6 +32,10 @@ module ASF
30
32
  end
31
33
 
32
34
  def self.[](name)
35
+ self.find!(name)
36
+ end
37
+
38
+ def self.find(name)
33
39
  return @testdata[name] if @testdata[name]
34
40
 
35
41
  result = repos[(@mock+name.sub('private/','')).to_s.sub(/\/*$/, '')] ||
@@ -39,13 +45,136 @@ module ASF
39
45
 
40
46
  # recursively try parent directory
41
47
  if name.include? '/'
42
- base = File.basename(name)
43
- result = self[File.dirname(name)]
48
+ base = File.basename(name).untaint
49
+ result = find(File.dirname(name))
44
50
  if result and File.exist?(File.join(result, base))
45
51
  File.join(result, base)
46
52
  end
47
53
  end
48
54
  end
55
+
56
+ def self.find!(name)
57
+ result = self.find(name)
58
+
59
+ if not result
60
+ raise Exception.new("Unable to find svn checkout for #{@base + name}")
61
+ end
62
+
63
+ result
64
+ end
65
+
66
+ # retrieve revision, content for a file in svn
67
+ def self.get(path, user=nil, password=nil)
68
+ # build svn info command
69
+ cmd = ['svn', 'info', path, '--non-interactive']
70
+
71
+ # password was supplied, add credentials
72
+ if password
73
+ cmd += ['--username', user, '--password', password, '--no-auth-cache']
74
+ end
75
+
76
+ # default the values to return
77
+ revision = '0'
78
+ content = nil
79
+
80
+ # issue svn info command
81
+ stdout, status = Open3.capture2(*cmd)
82
+ if status.success?
83
+ # extract revision number
84
+ revision = stdout[/^Revision: (\d+)/, 1]
85
+
86
+ # extract contents
87
+ cmd[1] = 'cat'
88
+ content, status = Open3.capture2(*cmd)
89
+ end
90
+
91
+ # return results
92
+ return revision, content
93
+ end
94
+
95
+ # update a file or directory in SVN, working entirely in a temporary
96
+ # directory
97
+ def self.update(path, msg, env, _, options={})
98
+ if File.directory? path
99
+ dir = path
100
+ basename = nil
101
+ else
102
+ dir = File.dirname(path)
103
+ basename = File.basename(path)
104
+ end
105
+
106
+ if path.start_with? '/' and not path.include? '..' and File.exist?(path)
107
+ dir.untaint
108
+ basename.untaint
109
+ end
110
+
111
+ tmpdir = Dir.mktmpdir.untaint
112
+
113
+ begin
114
+ # create an empty checkout
115
+ _.system ['svn', 'checkout', '--depth', 'empty',
116
+ ['--username', env.user, '--password', env.password],
117
+ `svn info #{dir}`[/URL: (.*)/, 1], tmpdir]
118
+
119
+ # retrieve the file to be updated (may not exist)
120
+ if basename
121
+ tmpfile = File.join(tmpdir, basename).untaint
122
+ _.system ['svn', 'update',
123
+ ['--username', env.user, '--password', env.password],
124
+ tmpfile]
125
+ else
126
+ tmpfile = nil
127
+ end
128
+
129
+ # determine the new contents
130
+ if not tmpfile
131
+ # updating a directory
132
+ previous_contents = contents = nil
133
+ yield tmpdir, ''
134
+ elsif File.file? tmpfile
135
+ # updating an existing file
136
+ previous_contents = File.read(tmpfile)
137
+ contents = yield tmpdir, File.read(tmpfile)
138
+ else
139
+ # updating a new file
140
+ previous_contents = nil
141
+ contents = yield tmpdir, ''
142
+ previous_contents = File.read(tmpfile) if File.file? tmpfile
143
+ end
144
+
145
+ # create/update the temporary copy
146
+ if contents and not contents.empty?
147
+ File.write tmpfile, contents
148
+ if not previous_contents
149
+ _.system ['svn', 'add',
150
+ ['--username', env.user, '--password', env.password],
151
+ tmpfile]
152
+ end
153
+ elsif tmpfile and File.file? tmpfile
154
+ File.unlink tmpfile
155
+ _.system ['svn', 'delete',
156
+ ['--username', env.user, '--password', env.password],
157
+ tmpfile]
158
+ end
159
+
160
+ if options[:dryrun]
161
+ # show what would have been committed
162
+ rc = _.system ['svn', 'diff', tmpfile]
163
+ else
164
+ # commit the changes
165
+ rc = _.system ['svn', 'commit', tmpfile || tmpdir,
166
+ ['--username', env.user, '--password', env.password],
167
+ '--message', msg.untaint]
168
+ end
169
+
170
+ # fail if there are pending changes
171
+ unless rc == 0 and `svn st #{tmpfile || tmpdir}`.empty?
172
+ raise "svn failure #{path.inspect}"
173
+ end
174
+ ensure
175
+ FileUtils.rm_rf tmpdir
176
+ end
177
+ end
49
178
  end
50
179
 
51
180
  end