hen 0.2.7 → 0.3.2
Sign up to get free protection for your applications and to get access to all the features.
- data/ChangeLog +37 -0
- data/README +47 -2
- data/Rakefile +5 -4
- data/bin/hen +21 -28
- data/example/_henrc +7 -0
- data/example/project/COPYING +1 -1
- data/example/project/ChangeLog +1 -1
- data/example/project/README +6 -6
- data/example/project/Rakefile +8 -10
- data/example/project/_gitignore +6 -0
- data/example/project/lib/__progname__.rb +1 -1
- data/example/project/lib/__progname__/version.rb +1 -1
- data/lib/hen.rb +111 -77
- data/lib/hen/cli.rb +113 -38
- data/lib/hen/dsl.rb +207 -100
- data/lib/hen/version.rb +2 -2
- data/lib/hens/gem.rake +84 -45
- data/lib/hens/rdoc.rake +83 -42
- data/lib/hens/spec.rake +29 -18
- data/lib/hens/test.rake +8 -3
- metadata +20 -38
- data/example/.henrc +0 -27
data/lib/hen/cli.rb
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
# #
|
4
4
|
# hen -- Just a Rake helper #
|
5
5
|
# #
|
6
|
-
# Copyright (C) 2007-
|
6
|
+
# Copyright (C) 2007-2011 University of Cologne, #
|
7
7
|
# Albertus-Magnus-Platz, #
|
8
8
|
# 50923 Cologne, Germany #
|
9
9
|
# #
|
@@ -26,60 +26,135 @@
|
|
26
26
|
###############################################################################
|
27
27
|
#++
|
28
28
|
|
29
|
+
require 'hen'
|
30
|
+
require 'etc'
|
29
31
|
require 'erb'
|
30
|
-
|
31
|
-
require 'rubygems'
|
32
32
|
require 'highline/import'
|
33
33
|
|
34
|
-
|
34
|
+
class Hen
|
35
35
|
|
36
|
-
#
|
37
|
-
|
36
|
+
# Some helper methods used by the Hen executable. Also available
|
37
|
+
# for use in custom project skeletons.
|
38
38
|
|
39
|
-
|
39
|
+
module CLI
|
40
40
|
|
41
|
-
|
42
|
-
# already stored answer if present, unless +cached+ is false.
|
43
|
-
def ask(key, config_key = nil, cached = true, &block)
|
44
|
-
@@values[key] = nil unless cached
|
41
|
+
@answers = {}
|
45
42
|
|
46
|
-
|
47
|
-
original_ask("Please enter your #{key}: ", &block)
|
48
|
-
rescue Interrupt
|
49
|
-
abort ''
|
50
|
-
end
|
43
|
+
class << self
|
51
44
|
|
52
|
-
|
53
|
-
|
54
|
-
msg = "#{key} is required! Please enter a non-empty value."
|
55
|
-
max = 3
|
45
|
+
# Collect user's answers by key, so we don't have to ask again.
|
46
|
+
attr_reader :answers
|
56
47
|
|
57
|
-
|
58
|
-
value = ask(key, config_key, i.zero?, &block)
|
59
|
-
return value unless value.empty?
|
48
|
+
end
|
60
49
|
|
61
|
-
|
62
|
-
|
50
|
+
# Renders the contents of +sample+ as an ERb template,
|
51
|
+
# storing the result in +target+. Returns the content.
|
52
|
+
def render(sample, target)
|
53
|
+
abort "Sample file not found: #{sample}" unless File.readable?(sample)
|
63
54
|
|
64
|
-
|
65
|
-
|
55
|
+
if File.readable?(target)
|
56
|
+
abort unless agree("Target file already exists: #{target}. Overwrite? ")
|
57
|
+
FileUtils.cp(target, "#{target}.bak-#{Time.now.to_i}")
|
58
|
+
end
|
59
|
+
|
60
|
+
content = ERB.new(File.read(sample)).result(binding)
|
61
|
+
|
62
|
+
File.open(target, 'w') { |f| f.puts content unless content.empty? }
|
63
|
+
|
64
|
+
content
|
65
|
+
end
|
66
|
+
|
67
|
+
# The project name. (Required)
|
68
|
+
#
|
69
|
+
# Quoting the {Ruby Packaging Standard}[http://chneukirchen.github.com/rps/]:
|
70
|
+
#
|
71
|
+
# Project names SHOULD only contain underscores as separators
|
72
|
+
# in their names.
|
73
|
+
#
|
74
|
+
# If a project is an enhancement, plugin, extension, etc. for
|
75
|
+
# some other project it SHOULD contain a dash in the name
|
76
|
+
# between the original name and the project's name.
|
77
|
+
def progname(default = nil)
|
78
|
+
ask!("Project's name", default)
|
79
|
+
end
|
80
|
+
|
81
|
+
# The project's namespace. (Required)
|
82
|
+
#
|
83
|
+
# Namespaces SHOULD match the project name in SnakeCase.
|
84
|
+
def classname(default = default_classname)
|
85
|
+
ask!("Module's/Class's name", default)
|
86
|
+
end
|
87
|
+
|
88
|
+
# The author's full name. (Required)
|
89
|
+
def fullname(default = default_fullname)
|
90
|
+
ask!('Full name', default)
|
91
|
+
end
|
92
|
+
|
93
|
+
# The author's e-mail address. (Optional, but highly recommended)
|
94
|
+
def emailaddress(default = default_emailaddress)
|
95
|
+
ask('E-mail address', default)
|
96
|
+
end
|
97
|
+
|
98
|
+
# A short one-line summary of the project's description. (Required)
|
99
|
+
def progdesc(default = nil)
|
100
|
+
ask!("Program's description summary", default)
|
101
|
+
end
|
102
|
+
|
103
|
+
private
|
66
104
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
105
|
+
# Determine a suitable default namespace from the project name.
|
106
|
+
def default_classname
|
107
|
+
pname = progname
|
108
|
+
pname.gsub(/(?:\A|_)(.)/) { $1.upcase } if pname && !pname.empty?
|
109
|
+
end
|
110
|
+
|
111
|
+
# Determine a default name from the global config or, if available,
|
112
|
+
# from the {GECOS field}[http://en.wikipedia.org/wiki/Gecos_field]
|
113
|
+
# in the <tt>/etc/passwd</tt> file.
|
114
|
+
def default_fullname
|
115
|
+
author = Hen.config('gem/author')
|
116
|
+
return author if author && !author.empty?
|
117
|
+
|
118
|
+
pwent = Etc.getpwuid(Process.euid)
|
119
|
+
gecos = pwent.gecos if pwent
|
120
|
+
gecos[/[^,]*/] if gecos && !gecos.empty?
|
121
|
+
end
|
71
122
|
|
72
|
-
|
73
|
-
|
123
|
+
# Determine a default e-mail address from the global config.
|
124
|
+
def default_emailaddress
|
125
|
+
email = Hen.config('gem/email')
|
126
|
+
return email if email && !email.empty?
|
74
127
|
end
|
75
128
|
|
76
|
-
|
129
|
+
alias_method :_hen_original_ask, :ask
|
77
130
|
|
78
|
-
|
79
|
-
|
80
|
-
|
131
|
+
# Ask the user to enter an appropriate value for +key+. Uses
|
132
|
+
# already stored answer if present, unless +cached+ is false.
|
133
|
+
def ask(key, default = nil, cached = true)
|
134
|
+
CLI.answers[key] = nil unless cached
|
135
|
+
|
136
|
+
CLI.answers[key] ||= _hen_original_ask("Please enter your #{key}: ") { |q|
|
137
|
+
q.default = default if default
|
138
|
+
}
|
139
|
+
rescue Interrupt
|
140
|
+
abort ''
|
141
|
+
end
|
142
|
+
|
143
|
+
# Same as #ask, but requires a non-empty value to be entered.
|
144
|
+
def ask!(key, default = nil, max = 3)
|
145
|
+
msg = "#{key} is required! Please enter a non-empty value."
|
146
|
+
|
147
|
+
max.times { |i|
|
148
|
+
value = ask(key, default, i.zero?)
|
149
|
+
return value unless value.empty?
|
150
|
+
|
151
|
+
puts msg
|
152
|
+
}
|
153
|
+
|
154
|
+
warn "You had #{max} tries now -- giving up..."
|
155
|
+
'' # default value
|
156
|
+
end
|
81
157
|
|
82
|
-
content
|
83
158
|
end
|
84
159
|
|
85
160
|
end
|
data/lib/hen/dsl.rb
CHANGED
@@ -26,35 +26,35 @@
|
|
26
26
|
###############################################################################
|
27
27
|
#++
|
28
28
|
|
29
|
+
require 'hen'
|
29
30
|
require 'nuggets/file/which'
|
30
31
|
|
31
32
|
class Hen
|
32
33
|
|
33
34
|
# Some helper methods for use inside of a Hen definition.
|
35
|
+
|
34
36
|
module DSL
|
35
37
|
|
36
38
|
extend self
|
37
39
|
|
38
40
|
# The Hen configuration.
|
39
41
|
def config
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
config
|
42
|
+
extend_object(Hen.config.dup) {
|
43
|
+
# Always return a duplicate for a value,
|
44
|
+
# hence making the configuration immutable
|
45
|
+
def [](key) # :nodoc:
|
46
|
+
fetch(key).dup
|
47
|
+
rescue IndexError
|
48
|
+
{}
|
49
|
+
end
|
50
|
+
}
|
51
51
|
end
|
52
52
|
|
53
53
|
# Define task +t+, but overwrite any existing task of that name!
|
54
|
-
# (Rake usually just adds them up)
|
55
|
-
def task!(t,
|
54
|
+
# (Rake usually just adds them up.)
|
55
|
+
def task!(t, *args)
|
56
56
|
Rake.application.instance_variable_get(:@tasks).delete(t.to_s)
|
57
|
-
task(t, &
|
57
|
+
task(t, *args, &block_given? ? Proc.new : nil)
|
58
58
|
end
|
59
59
|
|
60
60
|
# Find a command that is executable and run it. Intended for
|
@@ -71,149 +71,256 @@ class Hen
|
|
71
71
|
end
|
72
72
|
end
|
73
73
|
|
74
|
-
#
|
75
|
-
#
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
74
|
+
# Clean up the file lists in +args+ by removing duplicates and either
|
75
|
+
# deleting any files that are not managed by the source code management
|
76
|
+
# system (untracked files) or, if the project is not version-controlled
|
77
|
+
# or the SCM is not recognized, deleting any files that don't exist.
|
78
|
+
#
|
79
|
+
# The return value indicates whether source control is in effect.
|
80
|
+
#
|
81
|
+
# Currently supported SCM's (in that order): Git[http://git-scm.com],
|
82
|
+
# SVN[http://subversion.tigris.org].
|
83
|
+
def mangle_files!(*args)
|
84
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
85
|
+
|
86
|
+
managed_files = [:git, :svn].find { |scm|
|
87
|
+
res = send(scm) { |scm_obj| scm_obj.managed_files }
|
88
|
+
break res if res
|
89
|
+
} if !options.has_key?(:managed) || options[:managed]
|
90
|
+
|
91
|
+
args.each { |files|
|
92
|
+
files ? files.uniq! : next
|
93
|
+
|
94
|
+
if managed_files
|
95
|
+
files.replace(files & managed_files)
|
96
|
+
else
|
97
|
+
files.delete_if { |file| !File.readable?(file) }
|
98
|
+
end
|
99
|
+
}
|
81
100
|
|
82
|
-
|
101
|
+
!!managed_files
|
83
102
|
end
|
84
103
|
|
85
|
-
# Encapsulates tasks targeting at
|
86
|
-
#
|
104
|
+
# Encapsulates tasks targeting at RubyForge, skipping those if no
|
105
|
+
# RubyForge project is defined. Yields the RubyForge configuration
|
87
106
|
# hash and, optionally, a proc to obtain RubyForge objects from (via
|
88
|
-
# +call+; reaching out to init_rubyforge).
|
89
|
-
def rubyforge
|
107
|
+
# +call+; reaching out to #init_rubyforge).
|
108
|
+
def rubyforge
|
90
109
|
rf_config = config[:rubyforge]
|
91
110
|
rf_project = rf_config[:project]
|
92
111
|
|
93
|
-
|
112
|
+
if rf_project && !rf_project.empty? && have_rubyforge?
|
113
|
+
rf_config[:package] ||= rf_project
|
94
114
|
|
95
|
-
|
115
|
+
call_block(Proc.new, rf_config) { |*args|
|
116
|
+
init_rubyforge(args.empty? || args.first)
|
117
|
+
}
|
118
|
+
else
|
119
|
+
skipping 'RubyForge'
|
120
|
+
end
|
121
|
+
end
|
96
122
|
|
97
|
-
|
123
|
+
# Encapsulates tasks targeting at RubyGems.org, skipping those if
|
124
|
+
# RubyGem's 'push' command is not available. Yields an optional
|
125
|
+
# proc to obtain RubyGems (pseudo-)objects from (via +call+;
|
126
|
+
# reaching out to #init_rubygems).
|
127
|
+
def rubygems
|
128
|
+
if have_rubygems?
|
129
|
+
call_block(Proc.new) { |*args| init_rubygems }
|
130
|
+
else
|
131
|
+
skipping 'RubyGems'
|
132
|
+
end
|
133
|
+
end
|
98
134
|
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
135
|
+
# DEPRECATED: Use #rubygems instead.
|
136
|
+
def gemcutter
|
137
|
+
warn "#{self}#gemcutter is deprecated; use `rubygems' instead."
|
138
|
+
rubygems(&block_given? ? Proc.new : nil)
|
139
|
+
end
|
103
140
|
|
104
|
-
|
141
|
+
# Encapsulates tasks targeting at Git, skipping those if the current
|
142
|
+
# project us not controlled by Git. Yields a Git object via #init_git.
|
143
|
+
def git
|
144
|
+
have_git? ? yield(init_git) : skipping('Git')
|
105
145
|
end
|
106
146
|
|
107
|
-
#
|
108
|
-
|
109
|
-
|
147
|
+
# Encapsulates tasks targeting at SVN, skipping those if the current
|
148
|
+
# project us not controlled by SVN. Yields an SVN object via #init_svn.
|
149
|
+
def svn
|
150
|
+
have_svn? ? yield(init_svn) : skipping('SVN')
|
151
|
+
end
|
110
152
|
|
111
|
-
|
153
|
+
private
|
112
154
|
|
113
|
-
|
114
|
-
|
155
|
+
# Warn about skipping tasks for +name+ (if +do_warn+ is true) and return nil.
|
156
|
+
def skipping(name, do_warn = Hen.verbose)
|
157
|
+
warn "Skipping #{name} tasks." if do_warn
|
158
|
+
nil
|
159
|
+
end
|
160
|
+
|
161
|
+
# Warn about missing library +lib+ (if +do_warn+ is true) and return false.
|
162
|
+
def missing_lib(lib, do_warn = $DEBUG)
|
163
|
+
warn "Please install the `#{lib}' library for additional tasks." if do_warn
|
164
|
+
false
|
165
|
+
end
|
166
|
+
|
167
|
+
# Loads the RubyForge library, giving a
|
168
|
+
# nicer error message if it's not found.
|
169
|
+
def have_rubyforge?
|
170
|
+
require 'rubyforge'
|
171
|
+
true
|
172
|
+
rescue LoadError
|
173
|
+
missing_lib 'rubyforge'
|
174
|
+
end
|
175
|
+
|
176
|
+
# Loads the RubyGems +push+ command, giving
|
177
|
+
# a nicer error message if it's not found.
|
178
|
+
def have_rubygems?
|
179
|
+
begin
|
180
|
+
require 'rubygems/command_manager'
|
181
|
+
require 'rubygems/commands/push_command'
|
182
|
+
rescue LoadError
|
183
|
+
# rubygems < 1.3.6, gemcutter < 0.4.0
|
184
|
+
require 'commands/abstract_command'
|
185
|
+
require 'commands/push'
|
115
186
|
end
|
116
187
|
|
117
|
-
|
188
|
+
Gem::Commands::PushCommand
|
189
|
+
rescue LoadError, NameError
|
190
|
+
missing_lib 'gemcutter'
|
191
|
+
end
|
192
|
+
|
193
|
+
# Checks whether the current project is managed by Git.
|
194
|
+
def have_git?
|
195
|
+
File.directory?('.git')
|
118
196
|
end
|
119
197
|
|
120
|
-
#
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
def gemcutter(&block)
|
125
|
-
raise 'Skipping Gemcutter tasks' unless require_gemcutter
|
198
|
+
# Checks whether the current project is managed by SVN.
|
199
|
+
def have_svn?
|
200
|
+
File.directory?('.svn')
|
201
|
+
end
|
126
202
|
|
127
|
-
|
203
|
+
# Prepare the use of RubyForge, optionally logging
|
204
|
+
# in right away. Returns the RubyForge object.
|
205
|
+
def init_rubyforge(login = true)
|
206
|
+
return unless have_rubyforge?
|
128
207
|
|
129
|
-
|
130
|
-
|
131
|
-
init_gemcutter
|
132
|
-
} if block.arity > 0
|
208
|
+
rf = RubyForge.new.configure
|
209
|
+
rf.login if login
|
133
210
|
|
134
|
-
|
211
|
+
rf
|
135
212
|
end
|
136
213
|
|
137
|
-
|
138
|
-
|
214
|
+
# Prepare the use of RubyGems.org. Returns the RubyGems
|
215
|
+
# (pseudo-)object.
|
216
|
+
def init_rubygems
|
217
|
+
pseudo_object {
|
218
|
+
def method_missing(cmd, *args) # :nodoc:
|
219
|
+
run(cmd, *args)
|
220
|
+
end
|
139
221
|
|
140
|
-
|
222
|
+
def run(cmd, *args) # :nodoc:
|
223
|
+
Gem::CommandManager.instance.run([cmd.to_s.tr('_', '-'), *args])
|
224
|
+
end
|
225
|
+
} if have_rubygems?
|
141
226
|
end
|
142
227
|
|
228
|
+
# Prepare the use of Git. Returns the Git (pseudo-)object.
|
143
229
|
def init_git
|
144
|
-
|
230
|
+
pseudo_object {
|
231
|
+
def method_missing(cmd, *args) # :nodoc:
|
232
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
233
|
+
options[:verbose] = Hen.verbose unless options.has_key?(:verbose)
|
145
234
|
|
146
|
-
|
147
|
-
undef_method(method) unless method =~ /\A__/
|
148
|
-
}
|
149
|
-
|
150
|
-
def method_missing(cmd, *args)
|
151
|
-
sh 'git', cmd.to_s.tr('_', '-'), *args
|
235
|
+
sh('git', cmd.to_s.tr('_', '-'), *args << options)
|
152
236
|
end
|
153
237
|
|
154
|
-
|
155
|
-
|
156
|
-
def run(*args)
|
157
|
-
%x{#{args.unshift('git').join(' ')}}
|
238
|
+
def run(cmd, *args) # :nodoc:
|
239
|
+
%x{git #{args.unshift(cmd.to_s.tr('_', '-')).join(' ')}}
|
158
240
|
end
|
159
241
|
|
160
|
-
def remote_for_branch(branch)
|
161
|
-
run(:branch, '-r')[
|
242
|
+
def remote_for_branch(branch) # :nodoc:
|
243
|
+
run(:branch, '-r')[%r{(\S+)/#{Regexp.escape(branch)}$}, 1]
|
162
244
|
end
|
163
245
|
|
164
|
-
def url_for_remote(remote)
|
165
|
-
run(:remote, '-v')[
|
246
|
+
def url_for_remote(remote) # :nodoc:
|
247
|
+
run(:remote, '-v')[%r{\A#{Regexp.escape(remote)}\s+(\S+)}, 1]
|
166
248
|
end
|
167
249
|
|
168
|
-
def find_remote(regexp)
|
250
|
+
def find_remote(regexp) # :nodoc:
|
169
251
|
run(:remote, '-v').split($/).grep(regexp).first
|
170
252
|
end
|
171
253
|
|
172
|
-
def easy_clone(url, dir = '.', remote = 'origin')
|
254
|
+
def easy_clone(url, dir = '.', remote = 'origin') # :nodoc:
|
173
255
|
clone '-n', '-o', remote, url, dir
|
174
256
|
end
|
175
257
|
|
176
|
-
def checkout_remote_branch(remote, branch = 'master')
|
258
|
+
def checkout_remote_branch(remote, branch = 'master') # :nodoc:
|
177
259
|
checkout '-b', branch, "#{remote}/#{branch}"
|
178
260
|
end
|
179
261
|
|
180
|
-
def add_and_commit(msg)
|
262
|
+
def add_and_commit(msg) # :nodoc:
|
181
263
|
add '.'
|
182
264
|
commit '-m', msg
|
183
265
|
end
|
184
266
|
|
185
|
-
|
267
|
+
def managed_files # :nodoc:
|
268
|
+
run(:ls_files).split($/)
|
269
|
+
end
|
270
|
+
} if have_git?
|
271
|
+
end
|
272
|
+
|
273
|
+
# Prepare the use of SVN. Returns the SVN (pseudo-)object.
|
274
|
+
def init_svn
|
275
|
+
pseudo_object {
|
276
|
+
def method_missing(cmd, *args) # :nodoc:
|
277
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
278
|
+
options[:verbose] = Hen.verbose unless options.has_key?(:verbose)
|
186
279
|
|
187
|
-
|
280
|
+
sh('svn', cmd.to_s.tr('_', '-'), *args << options)
|
281
|
+
end
|
282
|
+
|
283
|
+
def run(cmd, *args) # :nodoc:
|
284
|
+
%x{svn #{args.unshift(cmd.to_s.tr('_', '-')).join(' ')}}
|
285
|
+
end
|
286
|
+
|
287
|
+
def version # :nodoc:
|
288
|
+
%x{svnversion}[/\d+/]
|
289
|
+
end
|
290
|
+
|
291
|
+
def managed_files # :nodoc:
|
292
|
+
run(:list, '--recursive').split($/)
|
293
|
+
end
|
294
|
+
} if have_svn?
|
188
295
|
end
|
189
296
|
|
190
|
-
|
297
|
+
# Extend +object+ with given +blocks+.
|
298
|
+
def extend_object(object, *blocks)
|
299
|
+
blocks << Proc.new if block_given?
|
191
300
|
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
end
|
301
|
+
singleton_class = class << object; self; end
|
302
|
+
|
303
|
+
blocks.compact.reverse_each { |block|
|
304
|
+
singleton_class.class_eval(&block)
|
305
|
+
}
|
306
|
+
|
307
|
+
object
|
200
308
|
end
|
201
309
|
|
202
|
-
#
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
require 'commands/abstract_command'
|
211
|
-
require 'commands/push'
|
212
|
-
end
|
310
|
+
# Create a (pseudo-)object.
|
311
|
+
def pseudo_object
|
312
|
+
extend_object(Object.new, block_given? ? Proc.new : nil) {
|
313
|
+
instance_methods.each { |method|
|
314
|
+
undef_method(method) unless method =~ /\A__/
|
315
|
+
}
|
316
|
+
}
|
317
|
+
end
|
213
318
|
|
214
|
-
|
215
|
-
|
216
|
-
|
319
|
+
# Calls block +block+ with +args+, appending an
|
320
|
+
# optional passed block if requested by +block+.
|
321
|
+
def call_block(block, *args)
|
322
|
+
args << Proc.new if block.arity > args.size
|
323
|
+
block[*args]
|
217
324
|
end
|
218
325
|
|
219
326
|
end
|