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/lib/hen/cli.rb CHANGED
@@ -3,7 +3,7 @@
3
3
  # #
4
4
  # hen -- Just a Rake helper #
5
5
  # #
6
- # Copyright (C) 2007-2008 University of Cologne, #
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
- module Hen::CLI
34
+ class Hen
35
35
 
36
- # Collect user's answers by key, so we don't have to ask again.
37
- @@values = {}
36
+ # Some helper methods used by the Hen executable. Also available
37
+ # for use in custom project skeletons.
38
38
 
39
- alias_method :original_ask, :ask
39
+ module CLI
40
40
 
41
- # Ask the user to enter an appropriate value for +key+. Uses
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
- @@values[key] ||= config_key && Hen.config(config_key) ||
47
- original_ask("Please enter your #{key}: ", &block)
48
- rescue Interrupt
49
- abort ''
50
- end
43
+ class << self
51
44
 
52
- # Same as #ask, but requires a non-empty value to be entered.
53
- def ask!(key, config_key = nil, &block)
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
- max.times { |i|
58
- value = ask(key, config_key, i.zero?, &block)
59
- return value unless value.empty?
48
+ end
60
49
 
61
- puts msg
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
- abort "You had #{max} tries -- now be gone!"
65
- end
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
- # Renders the contents of +sample+ as an ERb template,
68
- # storing the result in +target+. Returns the content.
69
- def render(sample, target)
70
- abort "Sample file not found: #{sample}" unless File.readable?(sample)
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
- if File.readable?(target)
73
- abort unless agree("Target file already exists: #{target}. Overwrite? ")
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
- content = ERB.new(File.read(sample)).result(binding)
129
+ alias_method :_hen_original_ask, :ask
77
130
 
78
- File.open(target, 'w') { |f|
79
- f.puts content unless content.empty?
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
- config = Hen.config
41
-
42
- # always return a duplicate for a value, hence making the
43
- # configuration immutable
44
- def config.[](key)
45
- fetch(key).dup
46
- rescue IndexError
47
- {}
48
- end
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, &block)
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, &block)
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
- # Prepare the use of Rubyforge, optionally logging in right away.
75
- # Returns the RubyForge object.
76
- def init_rubyforge(login = true)
77
- require_rubyforge
78
-
79
- rf = RubyForge.new.configure
80
- rf.login if login
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
- rf
101
+ !!managed_files
83
102
  end
84
103
 
85
- # Encapsulates tasks targeting at Rubyforge, skipping those if no
86
- # Rubyforge project is defined. Yields the Rubyforge configuration
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(&block)
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
- raise 'Skipping Rubyforge tasks' if rf_project.nil? || rf_project.empty?
112
+ if rf_project && !rf_project.empty? && have_rubyforge?
113
+ rf_config[:package] ||= rf_project
94
114
 
95
- require_rubyforge
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
- raise LocalJumpError, 'no block given' unless block
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
- block_args = [rf_config]
100
- block_args << lambda { |*args|
101
- init_rubyforge(args.empty? || args.first)
102
- } if block.arity > 1
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
- block[*block_args]
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
- # Prepare the use of Gemcutter. Returns the Gemcutter (pseudo-)object.
108
- def init_gemcutter
109
- require_gemcutter(false)
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
- gc = Object.new
153
+ private
112
154
 
113
- def gc.push(gem)
114
- Gem::CommandManager.instance.run(['push', gem])
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
- gc
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
- # Encapsulates tasks targeting at Gemcutter, skipping those if
121
- # Gemcutter's 'push' command is not available. Yields an optional
122
- # proc to obtain Gemcutter objects from (via +call+; reaching out
123
- # to init_gemcutter).
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
- raise LocalJumpError, 'no block given' unless block
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
- block_args = []
130
- block_args << lambda { |*args|
131
- init_gemcutter
132
- } if block.arity > 0
208
+ rf = RubyForge.new.configure
209
+ rf.login if login
133
210
 
134
- block[*block_args]
211
+ rf
135
212
  end
136
213
 
137
- def git
138
- raise 'Skipping Git tasks' unless File.directory?('.git')
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
- yield init_git
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
- class << git = Object.new
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
- instance_methods.each { |method|
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
- #alias_method :sh, :system
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')[/(\S+)\/#{Regexp.escape(branch)}$/, 1]
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')[/\A#{Regexp.escape(remote)}\s+(\S+)/, 1]
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
- end
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
- git
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
- private
297
+ # Extend +object+ with given +blocks+.
298
+ def extend_object(object, *blocks)
299
+ blocks << Proc.new if block_given?
191
300
 
192
- # Loads the Rubyforge library, giving a
193
- # nicer error message if it's not found.
194
- def require_rubyforge
195
- begin
196
- require 'rubyforge'
197
- rescue LoadError
198
- raise "Please install the `rubyforge' gem first."
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
- # Loads the Gemcutter 'push' command, giving
203
- # a nicer error message if it's not found.
204
- def require_gemcutter(relax = true)
205
- begin
206
- require 'rubygems/command_manager'
207
- require 'rubygems/commands/push_command'
208
- rescue LoadError
209
- # rubygems < 1.3.6, gemcutter < 0.4.0
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
- Gem::Commands::PushCommand
215
- rescue LoadError, NameError
216
- raise "Please install the `gemcutter' gem first." unless relax
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