rhype 0.1.0
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/COPYING +682 -0
- data/HISTORY +0 -0
- data/MANIFEST +25 -0
- data/NEWS +22 -0
- data/README +13 -0
- data/VERSION +1 -0
- data/bin/rhype +6 -0
- data/lib/rhype/command.help +29 -0
- data/lib/rhype/command.rb +82 -0
- data/lib/rhype/controller.rb +71 -0
- data/lib/rhype/host.rb +55 -0
- data/lib/rhype/hosts/gforge.rb +755 -0
- data/lib/rhype/hosts/rubyforge.rb +876 -0
- data/lib/rhype/index.rb +4 -0
- data/lib/rhype/metadata.rb +77 -0
- data/lib/rhype/service.rb +90 -0
- data/meta/author +1 -0
- data/meta/contact +1 -0
- data/meta/description +2 -0
- data/meta/homepage +1 -0
- data/meta/unixname +1 -0
- metadata +87 -0
data/HISTORY
ADDED
File without changes
|
data/MANIFEST
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
bin
|
2
|
+
lib
|
3
|
+
meta
|
4
|
+
README
|
5
|
+
HISTORY
|
6
|
+
VERSION
|
7
|
+
NEWS
|
8
|
+
COPYING
|
9
|
+
bin/rhype
|
10
|
+
lib/rhype
|
11
|
+
lib/rhype/command.rb
|
12
|
+
lib/rhype/host.rb
|
13
|
+
lib/rhype/command.help
|
14
|
+
lib/rhype/controller.rb
|
15
|
+
lib/rhype/service.rb
|
16
|
+
lib/rhype/metadata.rb
|
17
|
+
lib/rhype/index.rb
|
18
|
+
lib/rhype/hosts
|
19
|
+
lib/rhype/hosts/rubyforge.rb
|
20
|
+
lib/rhype/hosts/gforge.rb
|
21
|
+
meta/homepage
|
22
|
+
meta/unixname
|
23
|
+
meta/author
|
24
|
+
meta/description
|
25
|
+
meta/contact
|
data/NEWS
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
I didn't really wnat to create this project. Honest.
|
2
|
+
But there were a couple of things I didn't like about
|
3
|
+
the rubyforge gem: it dummped config info into ones
|
4
|
+
home directory (without using the XDG standard) and
|
5
|
+
it couldn't automatically login.
|
6
|
+
|
7
|
+
Besides that I figure I could probably make the tool
|
8
|
+
a little more flexible and support additional hosts
|
9
|
+
as needed.
|
10
|
+
|
11
|
+
So that's how RHype was born. Originally this code
|
12
|
+
has been dwelling as a support library in the Reap
|
13
|
+
project. Hey, now it's free to live it's own life :)
|
14
|
+
|
15
|
+
Acknowlegement go to Ara T. Howard who wrote the
|
16
|
+
original rubyfoorge.rb script on which this code is
|
17
|
+
largely based.
|
18
|
+
|
19
|
+
### 0.1.0 / 2008-11-24
|
20
|
+
|
21
|
+
* Initial Release.
|
22
|
+
|
data/README
ADDED
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rhype 0.1.0 beta (2008-11-23)
|
data/bin/rhype
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
Usage: hype <action>[:host] [options]
|
2
|
+
|
3
|
+
Actions can very from host to host, but hosts generally
|
4
|
+
offer one or more of the following services:
|
5
|
+
|
6
|
+
publish Publish website
|
7
|
+
release Release package
|
8
|
+
announce Announce release
|
9
|
+
touch Check host availability
|
10
|
+
|
11
|
+
Specifying a host name limits the action to that specific host.
|
12
|
+
If no host name is given, then each host that responds to the
|
13
|
+
given action will be invoked in turn. A confirmation prompt will
|
14
|
+
appear for each host unless --force is used.
|
15
|
+
|
16
|
+
Action and host options can be given on the command line using
|
17
|
+
opt=arg notation. To facilitate easy reuse, however, place options
|
18
|
+
in the hype configuration file.
|
19
|
+
|
20
|
+
The following options are for the commandline only and are common
|
21
|
+
to all commands.
|
22
|
+
|
23
|
+
-n --dryrun Only pretend to do action
|
24
|
+
-d --debug Output detailed error reports
|
25
|
+
-f --force Automatically answer 'y' to queires
|
26
|
+
-h --help Display help information
|
27
|
+
|
28
|
+
See API documentation to learn the options for each action and host.
|
29
|
+
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'facets/argvector'
|
2
|
+
require 'rhype/controller'
|
3
|
+
|
4
|
+
module RHype
|
5
|
+
|
6
|
+
# = Command
|
7
|
+
#
|
8
|
+
# Provides CLI interface to RHype Controller.
|
9
|
+
class Command
|
10
|
+
|
11
|
+
HELPFILE = File.join(File.dirname(__FILE__), 'command.help')
|
12
|
+
|
13
|
+
attr :hostname
|
14
|
+
|
15
|
+
attr :action
|
16
|
+
|
17
|
+
attr :arguments
|
18
|
+
|
19
|
+
attr :options
|
20
|
+
|
21
|
+
#
|
22
|
+
def initialize
|
23
|
+
argv = Argvector.new
|
24
|
+
|
25
|
+
args, opts = *argv.parameters
|
26
|
+
cmd = args.shift
|
27
|
+
|
28
|
+
opts.each do |key, val|
|
29
|
+
case key
|
30
|
+
when 'help', 'h'
|
31
|
+
puts File.read(HELPFILE)
|
32
|
+
exit 0
|
33
|
+
when 'dryrun'
|
34
|
+
$DRYRUN = true
|
35
|
+
when 'quiet'
|
36
|
+
$QUIET = true
|
37
|
+
when 'force'
|
38
|
+
$FORCE = true
|
39
|
+
when 'trace'
|
40
|
+
$TRACE = true
|
41
|
+
when 'debug'
|
42
|
+
$DEBUG = true
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
unless cmd
|
47
|
+
puts "No command given."
|
48
|
+
exit -1
|
49
|
+
end
|
50
|
+
|
51
|
+
action, hostname = *cmd.split(':')
|
52
|
+
|
53
|
+
@hostname = hostname
|
54
|
+
@action = action
|
55
|
+
@arguments = args
|
56
|
+
@options = opts
|
57
|
+
end
|
58
|
+
|
59
|
+
#
|
60
|
+
def call
|
61
|
+
begin
|
62
|
+
app = Controller.new(hostname, options)
|
63
|
+
app.call(action) #arguments)
|
64
|
+
rescue => e
|
65
|
+
if $TRACE || $DEBUG
|
66
|
+
raise e
|
67
|
+
else
|
68
|
+
puts e.to_s
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# class methods ------------------------------------------------
|
74
|
+
|
75
|
+
def self.start
|
76
|
+
new.call
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'rhype/hosts/gforge'
|
2
|
+
require 'rhype/hosts/rubyforge'
|
3
|
+
#require 'rhype/hosts/email'
|
4
|
+
|
5
|
+
module RHype
|
6
|
+
|
7
|
+
class Controller
|
8
|
+
|
9
|
+
CONFIG_FILE = '.config/rhype/config.yaml'
|
10
|
+
|
11
|
+
attr :hosts
|
12
|
+
|
13
|
+
attr :config
|
14
|
+
|
15
|
+
attr :options
|
16
|
+
|
17
|
+
# New RHype Controller.
|
18
|
+
def initialize(hostname, options)
|
19
|
+
@options = options.rekey(&:to_s)
|
20
|
+
|
21
|
+
load_configuration
|
22
|
+
|
23
|
+
if hostname
|
24
|
+
@hosts = [hostname.to_s.downcase]
|
25
|
+
else
|
26
|
+
@hosts = config.keys
|
27
|
+
end
|
28
|
+
|
29
|
+
@host_cache = {}
|
30
|
+
end
|
31
|
+
|
32
|
+
# Load configuration.
|
33
|
+
def load_configuration
|
34
|
+
file = Dir.glob(CONFIG_FILE, File::FNM_CASEFOLD).first
|
35
|
+
if file
|
36
|
+
@config = YAML::load(File.new(file))
|
37
|
+
else
|
38
|
+
#raise "#{CONFIG_FILE} not found"
|
39
|
+
@config = {}
|
40
|
+
RHype.register.each do |key, klass|
|
41
|
+
@config[key] = {'type' => key}
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Invoke action.
|
47
|
+
def call(action) #, arguments)
|
48
|
+
raise "No hosts." if hosts.empty?
|
49
|
+
# loop thru hosts and invoke action.
|
50
|
+
hosts.each do |name|
|
51
|
+
opts = config[name] || {}
|
52
|
+
type = opts.delete('type')
|
53
|
+
opts = opts.merge(options)
|
54
|
+
host(name, type, opts).__send__(action, options)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
#
|
59
|
+
def host(name, type, options)
|
60
|
+
name = name.to_sym
|
61
|
+
@host_cache[name] ||= (
|
62
|
+
klass = Host[type]
|
63
|
+
raise "Undefined host name: #{name}" unless klass
|
64
|
+
klass.new(options)
|
65
|
+
)
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
|
data/lib/rhype/host.rb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'rhype/service'
|
2
|
+
|
3
|
+
module RHype
|
4
|
+
|
5
|
+
def self.register
|
6
|
+
@register ||= {}
|
7
|
+
end
|
8
|
+
|
9
|
+
# = Host
|
10
|
+
#
|
11
|
+
# Base class for Host Services.
|
12
|
+
#
|
13
|
+
class Host < Service
|
14
|
+
|
15
|
+
def self.[](name)
|
16
|
+
RHype.register[name.to_s.downcase]
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.inherited(base)
|
20
|
+
name = base.to_s.split('::').last.downcase
|
21
|
+
RHype.register[name] = base
|
22
|
+
end
|
23
|
+
|
24
|
+
# Generic announce confirmation.
|
25
|
+
# "Release to #{self.class.basename.downcase}?"
|
26
|
+
def confirm?(message, options={})
|
27
|
+
return true if force?
|
28
|
+
ans = ask(message, "yN")
|
29
|
+
case ans.downcase
|
30
|
+
when 'y', 'yes'
|
31
|
+
true
|
32
|
+
else
|
33
|
+
false
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
DEFAULT_ANNOUNCEMENT = "doc/ann{,ounce}{.txt,.rdoc}"
|
38
|
+
|
39
|
+
#ans = ask("Announce to #{self.class.basename.downcase}?", "yN")
|
40
|
+
|
41
|
+
#
|
42
|
+
|
43
|
+
def announcement(file=nil)
|
44
|
+
template = file || DEFAULT_ANNOUNCEMENT
|
45
|
+
project.generate('templates'=>template)
|
46
|
+
file = Dir.glob(template, File::FNM_CASEFOLD).first
|
47
|
+
text = File.read(file)
|
48
|
+
text = unfold_paragraphs(text)
|
49
|
+
text
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
|
@@ -0,0 +1,755 @@
|
|
1
|
+
# TODO: remove these two dependencies
|
2
|
+
#require 'facets' #/hash/rekey'
|
3
|
+
require 'facets/kernel/ask'
|
4
|
+
|
5
|
+
require 'ostruct'
|
6
|
+
require 'httpclient'
|
7
|
+
|
8
|
+
require 'rhype/host'
|
9
|
+
|
10
|
+
module RHype
|
11
|
+
|
12
|
+
# = GForge
|
13
|
+
#
|
14
|
+
# Interface with the GForge based hosting services.
|
15
|
+
# Supports the following tasks:
|
16
|
+
#
|
17
|
+
# * release - Upload release packages
|
18
|
+
# * publish - Publish website
|
19
|
+
# * announce - Post news announcement
|
20
|
+
# * touch - Test connection
|
21
|
+
#
|
22
|
+
class GForge < Host
|
23
|
+
|
24
|
+
#HOME = ENV["HOME"] || ENV["HOMEPATH"] || File.expand_path("~")
|
25
|
+
COOKIEJAR = File::join(Dir.tmpdir, 'reap', 'cookie.dat')
|
26
|
+
REPORT = /<h\d><span style="color:red">(.*?)<\/span><\/h\d>/
|
27
|
+
|
28
|
+
# Project unixname.
|
29
|
+
attr_accessor :unixname
|
30
|
+
|
31
|
+
# Project name.
|
32
|
+
attr_accessor :version
|
33
|
+
|
34
|
+
# Project's group id number.
|
35
|
+
attr_accessor :group_id
|
36
|
+
|
37
|
+
alias_method :group, :group_id
|
38
|
+
|
39
|
+
# Username for project account.
|
40
|
+
attr_accessor :username
|
41
|
+
|
42
|
+
# Password for project account.
|
43
|
+
attr_accessor :password
|
44
|
+
|
45
|
+
#
|
46
|
+
attr_accessor :domain
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def initialize(options) #, spec, mode)
|
51
|
+
#@unixname = unixname
|
52
|
+
|
53
|
+
options.each do |k,v|
|
54
|
+
send("#{k}=", v) if respond_to?("#{k}=")
|
55
|
+
end
|
56
|
+
|
57
|
+
raise "missing unixname in #{self.class.name}" unless @unixname
|
58
|
+
raise "missing domain in #{self.class.name}" unless @domain
|
59
|
+
|
60
|
+
@package_ids = {}
|
61
|
+
@release_ids = {}
|
62
|
+
@file_ids = {}
|
63
|
+
|
64
|
+
FileUtils.mkdir_p(File.dirname(COOKIEJAR))
|
65
|
+
end
|
66
|
+
|
67
|
+
#def initialize_defaults
|
68
|
+
# @unixname = metadata.unixname
|
69
|
+
# @version = metadata.version
|
70
|
+
#end
|
71
|
+
|
72
|
+
public
|
73
|
+
|
74
|
+
# URI = http:// + domain name
|
75
|
+
#
|
76
|
+
# TODO: Deal with https, and possible other protocols too.
|
77
|
+
|
78
|
+
def uri
|
79
|
+
@uri ||= URI.parse("http://" + domain)
|
80
|
+
end
|
81
|
+
|
82
|
+
#
|
83
|
+
|
84
|
+
def cookie_jar
|
85
|
+
COOKIEJAR
|
86
|
+
end
|
87
|
+
|
88
|
+
public
|
89
|
+
|
90
|
+
# Website location on server.
|
91
|
+
def siteroot
|
92
|
+
"/var/www/gforge-projects"
|
93
|
+
end
|
94
|
+
|
95
|
+
# What commands does this host support.
|
96
|
+
|
97
|
+
def commands
|
98
|
+
%w{ touch release publish post }
|
99
|
+
end
|
100
|
+
|
101
|
+
|
102
|
+
# Login to website.
|
103
|
+
|
104
|
+
def login # :yield:
|
105
|
+
load_project_cached
|
106
|
+
|
107
|
+
page = @uri + "/account/login.php"
|
108
|
+
page.scheme = 'https'
|
109
|
+
page = URI.parse(page.to_s) # set SSL port correctly
|
110
|
+
|
111
|
+
form = {
|
112
|
+
"return_to" => "",
|
113
|
+
"form_loginname" => username,
|
114
|
+
"form_pw" => password,
|
115
|
+
"login" => "Login with SSL"
|
116
|
+
}
|
117
|
+
html = http_post(page, form)
|
118
|
+
|
119
|
+
if not html[/Personal Page/]
|
120
|
+
puts "Login failed."
|
121
|
+
re1 = Regexp.escape(%{<h2 style="color:red">})
|
122
|
+
re2 = Regexp.escape(%{</h2>})
|
123
|
+
html[/#{re1}(.*?)#{re2}/]
|
124
|
+
raise $1
|
125
|
+
else
|
126
|
+
@printed_project_name ||= (puts "Project: #{unixname}"; true)
|
127
|
+
end
|
128
|
+
|
129
|
+
if block_given?
|
130
|
+
begin
|
131
|
+
yield
|
132
|
+
ensure
|
133
|
+
logout
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# Logout of website.
|
139
|
+
|
140
|
+
def logout
|
141
|
+
page = "/account/logout.php"
|
142
|
+
form = {}
|
143
|
+
http_post(page, form)
|
144
|
+
end
|
145
|
+
|
146
|
+
# Touch base with server -- login and logout.
|
147
|
+
|
148
|
+
def touch(options={})
|
149
|
+
login
|
150
|
+
puts "Group ID: #{group_id}"
|
151
|
+
puts "Login/Logout successful."
|
152
|
+
logout
|
153
|
+
end
|
154
|
+
|
155
|
+
# Upload release packages to hosting service.
|
156
|
+
#
|
157
|
+
# This task releases files to RubyForge --it should work with other
|
158
|
+
# GForge instaces or SourceForge clones too.
|
159
|
+
#
|
160
|
+
# While defaults are nice, you may want a little more control. You can
|
161
|
+
# specify additional attributes:
|
162
|
+
#
|
163
|
+
# files package files to release.
|
164
|
+
# exclude Package formats to exclude from files.
|
165
|
+
# (from those created by pack)
|
166
|
+
# unixname Project name on host.
|
167
|
+
# package Package to which this release belongs (defaults to project)
|
168
|
+
# release Release name (default is version number)
|
169
|
+
# version Version of release
|
170
|
+
# date Date of release (defaults to Time.now)
|
171
|
+
# processor Processor/Architecture (any, i386, PPC, etc.)
|
172
|
+
# is_public Public release? (defualts to true)
|
173
|
+
# changelog Change log file
|
174
|
+
# notelog Release notes file
|
175
|
+
#
|
176
|
+
# The release option can be a template by using %s in the
|
177
|
+
# string. The version number of your project will be sub'd
|
178
|
+
# in for the %s. This saves you from having to update
|
179
|
+
# the release name before every release.
|
180
|
+
#
|
181
|
+
#--
|
182
|
+
# What about releasing a pacman PKGBUILD?
|
183
|
+
#++
|
184
|
+
|
185
|
+
def release(options)
|
186
|
+
options = options.rekey
|
187
|
+
|
188
|
+
version = options[:version] || metadata.version
|
189
|
+
changelog = options[:changelog]
|
190
|
+
notelog = options[:notelog]
|
191
|
+
|
192
|
+
unixname = options[:unixname] || unixname()
|
193
|
+
package = options[:package] || unixname()
|
194
|
+
release = options[:release] || version()
|
195
|
+
name = options[:name] || package
|
196
|
+
files = options[:file] || []
|
197
|
+
date = options[:date] || Time::now.strftime('%Y-%m-%d %H:%M')
|
198
|
+
processor = options[:processor] || 'Any'
|
199
|
+
store = options[:store] || 'pkg'
|
200
|
+
|
201
|
+
is_public = options[:is_public].nil? ? true : options[:is_public]
|
202
|
+
|
203
|
+
raise ArgumentError, "missing unixname" unless unixname
|
204
|
+
raise ArgumentError, "missing package" unless package
|
205
|
+
raise ArgumentError, "missing release" unless release
|
206
|
+
|
207
|
+
if files.empty?
|
208
|
+
files = Dir[File.join(store, '*')].select do |file|
|
209
|
+
/#{version}[.]/ =~ file
|
210
|
+
end
|
211
|
+
#files = Dir.glob(File.join(store,"#{name}-#{version}*"))
|
212
|
+
end
|
213
|
+
|
214
|
+
files = files.select{ |f| File.file?(f) }
|
215
|
+
|
216
|
+
abort "No package files." if files.empty?
|
217
|
+
|
218
|
+
files.each do |file|
|
219
|
+
abort "Not a file -- #{file}" unless File.exist?(file)
|
220
|
+
puts "Release file: #{file}"
|
221
|
+
end
|
222
|
+
|
223
|
+
# which package types
|
224
|
+
#rtypes = [ 'tgz', 'tbz', 'tar.gz', 'tar.bz2', 'deb', 'gem', 'ebuild', 'zip' ]
|
225
|
+
#rtypes -= exclude
|
226
|
+
#rtypes = rtypes.collect{ |rt| Regexp.escape( rt ) }
|
227
|
+
#re_rtypes = Regexp.new('[.](' << rtypes.join('|') << ')$')
|
228
|
+
|
229
|
+
puts "Releasing #{package} #{release}..." #unless options['quiet']
|
230
|
+
|
231
|
+
login do
|
232
|
+
|
233
|
+
raise ArgumentError, "missing group_id" unless group_id
|
234
|
+
|
235
|
+
unless package_id = package?(package)
|
236
|
+
if dryrun?
|
237
|
+
puts "Package '#{package}' does not exist."
|
238
|
+
puts "Create package #{package}."
|
239
|
+
abort "Cannot continue in dryrun mode."
|
240
|
+
else
|
241
|
+
#unless options['force']
|
242
|
+
q = "Package '#{package}' does not exist. Create?"
|
243
|
+
a = ask(q, 'yN')
|
244
|
+
abort "Task canceled." unless ['y', 'yes', 'okay'].include?(a.downcase)
|
245
|
+
#end
|
246
|
+
puts "Creating package #{package}..."
|
247
|
+
create_package(package, is_public)
|
248
|
+
unless package_id = package?(package)
|
249
|
+
raise "Package creation failed."
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
253
|
+
if release_id = release?(release, package_id)
|
254
|
+
#unless options[:force]
|
255
|
+
if dryrun?
|
256
|
+
puts "Release #{release} already exists."
|
257
|
+
else
|
258
|
+
q = "Release #{release} already exists. Re-release?"
|
259
|
+
a = ask(q, 'yN')
|
260
|
+
abort "Task canceled." unless ['y', 'yes', 'okay'].include?(a.downcase)
|
261
|
+
#puts "Use -f option to force re-release."
|
262
|
+
#return
|
263
|
+
end
|
264
|
+
files.each do |file|
|
265
|
+
fname = File.basename(file)
|
266
|
+
if file_id = file?(fname, package)
|
267
|
+
if dryrun?
|
268
|
+
puts "Remove file #{fname}."
|
269
|
+
else
|
270
|
+
puts "Removing file #{fname}..."
|
271
|
+
remove_file(file_id, release_id, package_id)
|
272
|
+
end
|
273
|
+
end
|
274
|
+
if dryrun?
|
275
|
+
puts "Add file #{fname}."
|
276
|
+
else
|
277
|
+
puts "Adding file #{fname}..."
|
278
|
+
add_file(file, release_id, package_id, processor)
|
279
|
+
end
|
280
|
+
end
|
281
|
+
else
|
282
|
+
if dryrun?
|
283
|
+
puts "Add release #{release}."
|
284
|
+
else
|
285
|
+
puts "Adding release #{release}..."
|
286
|
+
add_release(release, package_id, files,
|
287
|
+
:processor => processor,
|
288
|
+
:release_date => date,
|
289
|
+
:release_changes => changelog,
|
290
|
+
:release_notes => notelog,
|
291
|
+
:preformatted => '1'
|
292
|
+
)
|
293
|
+
unless release_id = release?(release, package_id)
|
294
|
+
raise "Release creation failed."
|
295
|
+
end
|
296
|
+
end
|
297
|
+
#files.each do |file|
|
298
|
+
# puts "Added file #{File.basename(file)}."
|
299
|
+
#end
|
300
|
+
end
|
301
|
+
end
|
302
|
+
puts "Release complete!" unless dryrun?
|
303
|
+
end
|
304
|
+
|
305
|
+
# #
|
306
|
+
# # Publish documents to website.
|
307
|
+
# #
|
308
|
+
# # TODO Fix publish method for Rubyforge tool.
|
309
|
+
#
|
310
|
+
# def publish(options)
|
311
|
+
# options = options.rekey
|
312
|
+
#
|
313
|
+
# #domain = options[:domain] || DOMAIN
|
314
|
+
# root = File.join(siteroot, unixname)
|
315
|
+
# root = File.join(root, options[:root]) if options[:root]
|
316
|
+
#
|
317
|
+
# options.update(
|
318
|
+
# :host => domain,
|
319
|
+
# :root => root
|
320
|
+
# )
|
321
|
+
#
|
322
|
+
# UploadUtils.rsync(options)
|
323
|
+
# end
|
324
|
+
|
325
|
+
# Submit a news item.
|
326
|
+
|
327
|
+
def announce(options)
|
328
|
+
options = options.rekey
|
329
|
+
|
330
|
+
if file = options[:file]
|
331
|
+
text = File.read(file).strip
|
332
|
+
i = text.index("\n")
|
333
|
+
subject = text[0...i].strip
|
334
|
+
message = text[i..-1].strip
|
335
|
+
else
|
336
|
+
subject = options[:subject]
|
337
|
+
message = options[:message] || options[:body]
|
338
|
+
end
|
339
|
+
|
340
|
+
if dryrun?
|
341
|
+
puts "announce-rubyforge: #{subject}"
|
342
|
+
else
|
343
|
+
post_news(subject, message)
|
344
|
+
puts "News item posted!"
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
|
349
|
+
private
|
350
|
+
|
351
|
+
# HTTP POST transaction.
|
352
|
+
|
353
|
+
def http_post(page, form, extheader={})
|
354
|
+
client = HTTPClient::new ENV["HTTP_PROXY"]
|
355
|
+
client.debug_dev = STDERR if ENV["REAP_DEBUG"] || $DEBUG
|
356
|
+
client.set_cookie_store(cookie_jar)
|
357
|
+
client.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
358
|
+
|
359
|
+
# HACK to fix http-client redirect bug/feature
|
360
|
+
client.redirect_uri_callback = lambda do |uri, res|
|
361
|
+
page = res.header['location'].first
|
362
|
+
page =~ %r/http/ ? page : @uri + page
|
363
|
+
end
|
364
|
+
|
365
|
+
uri = @uri + page
|
366
|
+
if $DEBUG then
|
367
|
+
puts "POST #{uri.inspect}"
|
368
|
+
puts "#{form.inspect}"
|
369
|
+
puts "#{extheader.inspect}" unless extheader.empty?
|
370
|
+
puts
|
371
|
+
end
|
372
|
+
|
373
|
+
response = client.post_content uri, form, extheader
|
374
|
+
|
375
|
+
if response[REPORT]
|
376
|
+
puts "(" + $1 + ")"
|
377
|
+
end
|
378
|
+
|
379
|
+
client.save_cookie_store
|
380
|
+
|
381
|
+
return response
|
382
|
+
end
|
383
|
+
|
384
|
+
#
|
385
|
+
|
386
|
+
def load_project_cached
|
387
|
+
@load_project_cache ||= load_project
|
388
|
+
end
|
389
|
+
|
390
|
+
# Loads information for project: group_id, package_ids and release_ids.
|
391
|
+
|
392
|
+
def load_project
|
393
|
+
html = URI.parse("http://#{domain}/projects/#{unixname}/index.html").read
|
394
|
+
|
395
|
+
group_id = html[/(frs|tracker)\/\?group_id=\d+/][/\d+/].to_i
|
396
|
+
@group_id = group_id
|
397
|
+
|
398
|
+
if $DEBUG
|
399
|
+
puts "GROUP_ID = #{group_id}"
|
400
|
+
end
|
401
|
+
|
402
|
+
html = URI.parse("http://rubyforge.org/frs/?group_id=#{group_id}").read
|
403
|
+
|
404
|
+
package = nil
|
405
|
+
html.scan(/<h3>[^<]+|release_id=\d+">[^>]+|filemodule_id=\d+/).each do |s|
|
406
|
+
case s
|
407
|
+
when /<h3>([^<]+)/ then
|
408
|
+
package = $1.strip
|
409
|
+
when /filemodule_id=(\d+)/ then
|
410
|
+
@package_ids[package] = $1.to_i
|
411
|
+
when /release_id=(\d+)">([^<]+)/ then
|
412
|
+
package_id = @package_ids[package]
|
413
|
+
@release_ids[[package_id,$2]] = $1.to_i
|
414
|
+
end
|
415
|
+
end
|
416
|
+
|
417
|
+
if $DEBUG
|
418
|
+
p @package_ids, @release_ids
|
419
|
+
end
|
420
|
+
end
|
421
|
+
|
422
|
+
# Returns password. If not already set, will ask for it.
|
423
|
+
|
424
|
+
def password
|
425
|
+
@password ||= ENV['RUBYFORGE_PASSWORD']
|
426
|
+
@password ||= (
|
427
|
+
print "Password for #{username}: "
|
428
|
+
until inp = $stdin.gets ; sleep 1 ; end ; puts
|
429
|
+
inp.strip
|
430
|
+
)
|
431
|
+
end
|
432
|
+
|
433
|
+
# Package exists? Returns package-id number.
|
434
|
+
|
435
|
+
def package?(package_name)
|
436
|
+
id = @package_ids[package_name]
|
437
|
+
return id if id
|
438
|
+
|
439
|
+
package_id = nil
|
440
|
+
|
441
|
+
page = "/frs/"
|
442
|
+
|
443
|
+
form = {
|
444
|
+
"group_id" => group_id
|
445
|
+
}
|
446
|
+
scrape = http_post(page, form)
|
447
|
+
|
448
|
+
restr = ''
|
449
|
+
restr << Regexp.escape( package_name )
|
450
|
+
restr << '\s*'
|
451
|
+
restr << Regexp.escape( '<a href="/frs/monitor.php?filemodule_id=' )
|
452
|
+
restr << '(\d+)'
|
453
|
+
restr << Regexp.escape( %{&group_id=#{group_id}} )
|
454
|
+
re = Regexp.new( restr )
|
455
|
+
|
456
|
+
md = re.match( scrape )
|
457
|
+
if md
|
458
|
+
package_id = md[1]
|
459
|
+
end
|
460
|
+
|
461
|
+
@package_ids[package_name] = package_id
|
462
|
+
end
|
463
|
+
|
464
|
+
# Create a new package.
|
465
|
+
|
466
|
+
def create_package( package_name, is_public=true )
|
467
|
+
page = "/frs/admin/index.php"
|
468
|
+
|
469
|
+
form = {
|
470
|
+
"func" => "add_package",
|
471
|
+
"group_id" => group_id,
|
472
|
+
"package_name" => package_name,
|
473
|
+
"is_public" => (is_public ? 1 : 0),
|
474
|
+
"submit" => "Create This Package"
|
475
|
+
}
|
476
|
+
|
477
|
+
http_post(page, form)
|
478
|
+
end
|
479
|
+
|
480
|
+
# Delete package.
|
481
|
+
|
482
|
+
def delete_package(package_id)
|
483
|
+
page = "/frs/admin/index.php"
|
484
|
+
|
485
|
+
form = {
|
486
|
+
"func" => "delete_package",
|
487
|
+
"group_id" => group_id,
|
488
|
+
"package_id" => package_id,
|
489
|
+
"sure" => "1",
|
490
|
+
"really_sure" => "1",
|
491
|
+
"submit" => "Delete",
|
492
|
+
}
|
493
|
+
|
494
|
+
http_post(page, form)
|
495
|
+
end
|
496
|
+
|
497
|
+
# Release exits? Returns release-id number.
|
498
|
+
|
499
|
+
def release?(release_name, package_id)
|
500
|
+
id = @release_ids[[release_name,package_id]]
|
501
|
+
return id if id
|
502
|
+
|
503
|
+
release_id = nil
|
504
|
+
|
505
|
+
page = "/frs/admin/showreleases.php"
|
506
|
+
|
507
|
+
form = {
|
508
|
+
"package_id" => package_id,
|
509
|
+
"group_id" => group_id
|
510
|
+
}
|
511
|
+
scrape = http_post( page, form )
|
512
|
+
|
513
|
+
restr = ''
|
514
|
+
restr << Regexp.escape( %{"editrelease.php?group_id=#{group_id}} )
|
515
|
+
restr << Regexp.escape( %{&package_id=#{package_id}} )
|
516
|
+
restr << Regexp.escape( %{&release_id=} )
|
517
|
+
restr << '(\d+)'
|
518
|
+
restr << Regexp.escape( %{">#{release_name}} )
|
519
|
+
re = Regexp.new( restr )
|
520
|
+
|
521
|
+
md = re.match( scrape )
|
522
|
+
if md
|
523
|
+
release_id = md[1]
|
524
|
+
end
|
525
|
+
|
526
|
+
@release_ids[[release_name,package_id]] = release_id
|
527
|
+
end
|
528
|
+
|
529
|
+
# Add a new release.
|
530
|
+
|
531
|
+
def add_release(release_name, package_id, *files)
|
532
|
+
page = "/frs/admin/qrs.php"
|
533
|
+
|
534
|
+
options = (Hash===files.last ? files.pop : {}).rekey
|
535
|
+
files = files.flatten
|
536
|
+
|
537
|
+
processor = options[:processor]
|
538
|
+
release_date = options[:release_date]
|
539
|
+
release_changes = options[:release_changes]
|
540
|
+
release_notes = options[:release_notes]
|
541
|
+
|
542
|
+
release_date ||= Time::now.strftime("%Y-%m-%d %H:%M")
|
543
|
+
|
544
|
+
file = files.shift
|
545
|
+
puts "Adding file #{File.basename(file)}..."
|
546
|
+
userfile = open(file, 'rb')
|
547
|
+
|
548
|
+
type_id = userfile.path[%r|\.[^\./]+$|]
|
549
|
+
type_id = FILETYPES[type_id]
|
550
|
+
processor_id = PROCESSORS[processor.downcase]
|
551
|
+
|
552
|
+
# TODO IS THIS WORKING?
|
553
|
+
release_notes = IO::read(release_notes) if release_notes and test(?f, release_notes)
|
554
|
+
release_changes = IO::read(release_changes) if release_changes and test(?f, release_changes)
|
555
|
+
|
556
|
+
preformatted = '1'
|
557
|
+
|
558
|
+
form = {
|
559
|
+
"group_id" => group_id,
|
560
|
+
"package_id" => package_id,
|
561
|
+
"release_name" => release_name,
|
562
|
+
"release_date" => release_date,
|
563
|
+
"type_id" => type_id,
|
564
|
+
"processor_id" => processor_id,
|
565
|
+
"release_notes" => release_notes,
|
566
|
+
"release_changes" => release_changes,
|
567
|
+
"preformatted" => preformatted,
|
568
|
+
"userfile" => userfile,
|
569
|
+
"submit" => "Release File"
|
570
|
+
}
|
571
|
+
|
572
|
+
boundary = Array::new(8){ "%2.2d" % rand(42) }.join('__')
|
573
|
+
boundary = "multipart/form-data; boundary=___#{ boundary }___"
|
574
|
+
|
575
|
+
html = http_post(page, form, 'content-type' => boundary)
|
576
|
+
|
577
|
+
release_id = html[/release_id=\d+/][/\d+/].to_i
|
578
|
+
puts "RELEASE ID = #{release_id}" if $DEBUG
|
579
|
+
|
580
|
+
files.each do |file|
|
581
|
+
puts "Adding file #{File.basename(file)}..."
|
582
|
+
add_file(file, release_id, package_id, processor)
|
583
|
+
end
|
584
|
+
|
585
|
+
release_id
|
586
|
+
end
|
587
|
+
|
588
|
+
# File exists?
|
589
|
+
#
|
590
|
+
# NOTE this is a bit fragile. If two releases have the same exact
|
591
|
+
# file name in them there could be a problem --that's probably not
|
592
|
+
# likely, but I can't yet rule it out.
|
593
|
+
#
|
594
|
+
# TODO Remove package argument, it is no longer needed.
|
595
|
+
|
596
|
+
def file?(file, package)
|
597
|
+
id = @file_ids[[file, package]]
|
598
|
+
return id if id
|
599
|
+
|
600
|
+
file_id = nil
|
601
|
+
|
602
|
+
page = "/frs/"
|
603
|
+
|
604
|
+
form = {
|
605
|
+
"group_id" => group_id
|
606
|
+
}
|
607
|
+
scrape = http_post(page, form)
|
608
|
+
|
609
|
+
restr = ''
|
610
|
+
#restr << Regexp.escape( package )
|
611
|
+
#restr << '\s*'
|
612
|
+
restr << Regexp.escape( '<a href="/frs/download.php/' )
|
613
|
+
restr << '(\d+)'
|
614
|
+
restr << Regexp.escape( %{/#{file}} )
|
615
|
+
re = Regexp.new(restr)
|
616
|
+
|
617
|
+
md = re.match(scrape)
|
618
|
+
if md
|
619
|
+
file_id = md[1]
|
620
|
+
end
|
621
|
+
|
622
|
+
@file_ids[[file, package]] = file_id
|
623
|
+
end
|
624
|
+
|
625
|
+
# Remove file from release.
|
626
|
+
|
627
|
+
def remove_file(file_id, release_id, package_id)
|
628
|
+
page="/frs/admin/editrelease.php"
|
629
|
+
|
630
|
+
form = {
|
631
|
+
"group_id" => group_id,
|
632
|
+
"package_id" => package_id,
|
633
|
+
"release_id" => release_id,
|
634
|
+
"file_id" => file_id,
|
635
|
+
"step3" => "Delete File",
|
636
|
+
"im_sure" => '1',
|
637
|
+
"submit" => "Delete File "
|
638
|
+
}
|
639
|
+
|
640
|
+
http_post(page, form)
|
641
|
+
end
|
642
|
+
|
643
|
+
#
|
644
|
+
# Add file to release.
|
645
|
+
#
|
646
|
+
|
647
|
+
def add_file(file, release_id, package_id, processor=nil)
|
648
|
+
page = '/frs/admin/editrelease.php'
|
649
|
+
|
650
|
+
userfile = open file, 'rb'
|
651
|
+
|
652
|
+
type_id = userfile.path[%r|\.[^\./]+$|]
|
653
|
+
type_id = FILETYPES[type_id]
|
654
|
+
processor_id = PROCESSORS[processor.downcase]
|
655
|
+
|
656
|
+
form = {
|
657
|
+
"step2" => '1',
|
658
|
+
"group_id" => group_id,
|
659
|
+
"package_id" => package_id,
|
660
|
+
"release_id" => release_id,
|
661
|
+
"userfile" => userfile,
|
662
|
+
"type_id" => type_id,
|
663
|
+
"processor_id" => processor_id,
|
664
|
+
"submit" => "Add This File"
|
665
|
+
}
|
666
|
+
|
667
|
+
boundary = Array::new(8){ "%2.2d" % rand(42) }.join('__')
|
668
|
+
boundary = "multipart/form-data; boundary=___#{ boundary }___"
|
669
|
+
|
670
|
+
http_post(page, form, 'content-type' => boundary)
|
671
|
+
end
|
672
|
+
|
673
|
+
# Posts news item to +group_id+ (can be name) with +subject+ and +body+
|
674
|
+
|
675
|
+
def post_news(subject, body)
|
676
|
+
page = "/news/submit.php"
|
677
|
+
|
678
|
+
subject % [unixname, version]
|
679
|
+
|
680
|
+
form = {
|
681
|
+
"group_id" => group_id,
|
682
|
+
"post_changes" => "y",
|
683
|
+
"summary" => subject,
|
684
|
+
"details" => body,
|
685
|
+
"submit" => "Submit"
|
686
|
+
}
|
687
|
+
|
688
|
+
login do
|
689
|
+
http_post(page, form)
|
690
|
+
end
|
691
|
+
end
|
692
|
+
|
693
|
+
# Constant for file types accepted by Rubyforge
|
694
|
+
|
695
|
+
FILETYPES = {
|
696
|
+
".deb" => 1000,
|
697
|
+
".rpm" => 2000,
|
698
|
+
".zip" => 3000,
|
699
|
+
".bz2" => 3100,
|
700
|
+
".gz" => 3110,
|
701
|
+
".src.zip" => 5000,
|
702
|
+
".src.bz2" => 5010,
|
703
|
+
".src.tar.bz2" => 5010,
|
704
|
+
".src.gz" => 5020,
|
705
|
+
".src.tar.gz" => 5020,
|
706
|
+
".src.rpm" => 5100,
|
707
|
+
".src" => 5900,
|
708
|
+
".jpg" => 8000,
|
709
|
+
".txt" => 8100,
|
710
|
+
".text" => 8100,
|
711
|
+
".htm" => 8200,
|
712
|
+
".html" => 8200,
|
713
|
+
".pdf" => 8300,
|
714
|
+
".oth" => 9999,
|
715
|
+
".ebuild" => 1300,
|
716
|
+
".exe" => 1100,
|
717
|
+
".dmg" => 1200,
|
718
|
+
".tar.gz" => 3110,
|
719
|
+
".tgz" => 3110,
|
720
|
+
".gem" => 1400,
|
721
|
+
".pgp" => 8150,
|
722
|
+
".sig" => 8150
|
723
|
+
}
|
724
|
+
|
725
|
+
# Constant for processor types accepted by Rubyforge
|
726
|
+
|
727
|
+
PROCESSORS = {
|
728
|
+
"i386" => 1000,
|
729
|
+
"IA64" => 6000,
|
730
|
+
"Alpha" => 7000,
|
731
|
+
"Any" => 8000,
|
732
|
+
"PPC" => 2000,
|
733
|
+
"MIPS" => 3000,
|
734
|
+
"Sparc" => 4000,
|
735
|
+
"UltraSparc" => 5000,
|
736
|
+
"Other" => 9999,
|
737
|
+
|
738
|
+
"i386" => 1000,
|
739
|
+
"ia64" => 6000,
|
740
|
+
"alpha" => 7000,
|
741
|
+
"any" => 8000,
|
742
|
+
"ppc" => 2000,
|
743
|
+
"mips" => 3000,
|
744
|
+
"sparc" => 4000,
|
745
|
+
"ultrasparc" => 5000,
|
746
|
+
"other" => 9999,
|
747
|
+
|
748
|
+
"all" => 8000,
|
749
|
+
nil => 8000
|
750
|
+
}
|
751
|
+
|
752
|
+
end
|
753
|
+
|
754
|
+
end
|
755
|
+
|