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/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