hen 0.2.7 → 0.3.2
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/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
|