whimsy-asf 0.0.76 → 0.0.77

Sign up to get free protection for your applications and to get access to all the features.
@@ -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