-
Notifications
You must be signed in to change notification settings - Fork 21.4k
/
actions.rb
528 lines (484 loc) · 17.3 KB
/
actions.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
# frozen_string_literal: true
require "shellwords"
require "active_support/core_ext/kernel/reporting"
require "active_support/core_ext/string/strip"
module Rails
module Generators
module Actions
def initialize(*) # :nodoc:
super
@indentation = 0
end
# Adds a +gem+ declaration to the +Gemfile+ for the specified gem.
#
# gem "rspec", group: :test
# gem "technoweenie-restful-authentication", lib: "restful-authentication", source: "http://gems.github.com/"
# gem "rails", "3.0", git: "https://github.com/rails/rails"
# gem "RedCloth", ">= 4.1.0", "< 4.2.0"
# gem "rspec", comment: "Put this comment above the gem declaration"
#
# Note that this method only adds the gem to the +Gemfile+; it does not
# install the gem.
#
# ==== Options
#
# [+:version+]
# The version constraints for the gem, specified as a string or an
# array of strings:
#
# gem "my_gem", version: "~> 1.1"
# gem "my_gem", version: [">= 1.1", "< 2.0"]
#
# Alternatively, can be specified as one or more arguments following the
# gem name:
#
# gem "my_gem", ">= 1.1", "< 2.0"
#
# [+:comment+]
# Outputs a comment above the +gem+ declaration in the +Gemfile+.
#
# gem "my_gem", comment: "First line.\nSecond line."
#
# Outputs:
#
# # First line.
# # Second line.
# gem "my_gem"
#
# [+:group+]
# The gem group in the +Gemfile+ that the gem belongs to.
#
# [+:git+]
# The URL of the git repository for the gem.
#
# Any additional options passed to this method will be appended to the
# +gem+ declaration in the +Gemfile+. For example:
#
# gem "my_gem", comment: "Edge my_gem", git: "https://example.com/my_gem.git", branch: "master"
#
# Outputs:
#
# # Edge my_gem
# gem "my_gem", git: "https://example.com/my_gem.git", branch: "master"
#
def gem(*args)
options = args.extract_options!
name, *versions = args
# Set the message to be shown in logs. Uses the git repo if one is given,
# otherwise use name (version).
parts, message = [ quote(name) ], name.dup
# Output a comment above the gem declaration.
comment = options.delete(:comment)
if versions = versions.any? ? versions : options.delete(:version)
_versions = Array(versions)
_versions.each do |version|
parts << quote(version)
end
message << " (#{_versions.join(", ")})"
end
message = options[:git] if options[:git]
log :gemfile, message
parts << quote(options) unless options.empty?
in_root do
str = []
if comment
comment.each_line do |comment_line|
str << indentation
str << "# #{comment_line}"
end
str << "\n"
end
str << indentation
str << "gem #{parts.join(", ")}"
append_file_with_newline "Gemfile", str.join, verbose: false
end
end
# Wraps gem entries inside a group.
#
# gem_group :development, :test do
# gem "rspec-rails"
# end
def gem_group(*names, &block)
options = names.extract_options!
str = names.map(&:inspect)
str << quote(options) unless options.empty?
str = str.join(", ")
log :gemfile, "group #{str}"
in_root do
append_file_with_newline "Gemfile", "\ngroup #{str} do", force: true
with_indentation(&block)
append_file_with_newline "Gemfile", "end", force: true
end
end
def github(repo, options = {}, &block)
str = [quote(repo)]
str << quote(options) unless options.empty?
str = str.join(", ")
log :github, "github #{str}"
in_root do
if @indentation.zero?
append_file_with_newline "Gemfile", "\ngithub #{str} do", force: true
else
append_file_with_newline "Gemfile", "#{indentation}github #{str} do", force: true
end
with_indentation(&block)
append_file_with_newline "Gemfile", "#{indentation}end", force: true
end
end
# Add the given source to +Gemfile+
#
# If block is given, gem entries in block are wrapped into the source group.
#
# add_source "http://gems.github.com/"
#
# add_source "http://gems.github.com/" do
# gem "rspec-rails"
# end
def add_source(source, options = {}, &block)
log :source, source
in_root do
if block
append_file_with_newline "Gemfile", "\nsource #{quote(source)} do", force: true
with_indentation(&block)
append_file_with_newline "Gemfile", "end", force: true
else
prepend_file "Gemfile", "source #{quote(source)}\n", verbose: false
end
end
end
# Adds configuration code to a \Rails runtime environment.
#
# By default, adds code inside the +Application+ class in
# +config/application.rb+ so that it applies to all environments.
#
# environment %(config.asset_host = "cdn.provider.com")
#
# Results in:
#
# # config/application.rb
# class Application < Rails::Application
# config.asset_host = "cdn.provider.com"
# # ...
# end
#
# If the +:env+ option is specified, the code will be added to the
# corresponding file in +config/environments+ instead.
#
# environment %(config.asset_host = "localhost:3000"), env: "development"
#
# Results in:
#
# # config/environments/development.rb
# Rails.application.configure do
# config.asset_host = "localhost:3000"
# # ...
# end
#
# +:env+ can also be an array. In which case, the code is added to each
# corresponding file in +config/environments+.
#
# The code can also be specified as the return value of the block:
#
# environment do
# %(config.asset_host = "cdn.provider.com")
# end
#
# environment(nil, env: "development") do
# %(config.asset_host = "localhost:3000")
# end
#
def environment(data = nil, options = {})
sentinel = "class Application < Rails::Application\n"
env_file_sentinel = "Rails.application.configure do\n"
data ||= yield if block_given?
in_root do
if options[:env].nil?
inject_into_file "config/application.rb", optimize_indentation(data, 4), after: sentinel, verbose: false
else
Array(options[:env]).each do |env|
inject_into_file "config/environments/#{env}.rb", optimize_indentation(data, 2), after: env_file_sentinel, verbose: false
end
end
end
end
alias :application :environment
# Runs one or more git commands.
#
# git :init
# # => runs `git init`
#
# git add: "this.file that.rb"
# # => runs `git add this.file that.rb`
#
# git commit: "-m 'First commit'"
# # => runs `git commit -m 'First commit'`
#
# git add: "good.rb", rm: "bad.cxx"
# # => runs `git add good.rb; git rm bad.cxx`
#
def git(commands = {})
if commands.is_a?(Symbol)
run "git #{commands}"
else
commands.each do |cmd, options|
run "git #{cmd} #{options}"
end
end
end
# Creates a file in +vendor/+. The contents can be specified as an
# argument or as the return value of the block.
#
# vendor "foreign.rb", <<~RUBY
# # Foreign code is fun
# RUBY
#
# vendor "foreign.rb" do
# "# Foreign code is fun"
# end
#
def vendor(filename, data = nil)
log :vendor, filename
data ||= yield if block_given?
create_file("vendor/#{filename}", optimize_indentation(data), verbose: false)
end
# Creates a file in +lib/+. The contents can be specified as an argument
# or as the return value of the block.
#
# lib "foreign.rb", <<~RUBY
# # Foreign code is fun
# RUBY
#
# lib "foreign.rb" do
# "# Foreign code is fun"
# end
#
def lib(filename, data = nil)
log :lib, filename
data ||= yield if block_given?
create_file("lib/#{filename}", optimize_indentation(data), verbose: false)
end
# Creates a Rake tasks file in +lib/tasks/+. The code can be specified as
# an argument or as the return value of the block.
#
# rakefile "bootstrap.rake", <<~RUBY
# task :bootstrap do
# puts "Boots! Boots! Boots!"
# end
# RUBY
#
# rakefile "bootstrap.rake" do
# project = ask("What is the UNIX name of your project?")
#
# <<~RUBY
# namespace :#{project} do
# task :bootstrap do
# puts "Boots! Boots! Boots!"
# end
# end
# RUBY
# end
#
def rakefile(filename, data = nil)
log :rakefile, filename
data ||= yield if block_given?
create_file("lib/tasks/#{filename}", optimize_indentation(data), verbose: false)
end
# Creates an initializer file in +config/initializers/+. The code can be
# specified as an argument or as the return value of the block.
#
# initializer "api.rb", <<~RUBY
# API_KEY = "123456"
# RUBY
#
# initializer "api.rb" do
# %(API_KEY = "123456")
# end
#
def initializer(filename, data = nil)
log :initializer, filename
data ||= yield if block_given?
create_file("config/initializers/#{filename}", optimize_indentation(data), verbose: false)
end
# Runs another generator.
#
# generate "scaffold", "Post title:string body:text"
# generate "scaffold", "Post", "title:string", "body:text"
#
# The first argument is the generator name, and the remaining arguments
# are joined together and passed to the generator.
def generate(what, *args)
log :generate, what
options = args.extract_options!
options[:abort_on_failure] = !options[:inline]
rails_command "generate #{what} #{args.join(" ")}", options
end
# Runs the specified Rake task.
#
# rake "db:migrate"
# rake "db:migrate", env: "production"
# rake "db:migrate", abort_on_failure: true
# rake "stats", capture: true
# rake "gems:install", sudo: true
#
# ==== Options
#
# [+:env+]
# The \Rails environment in which to run the task. Defaults to
# <tt>ENV["RAILS_ENV"] || "development"</tt>.
#
# [+:abort_on_failure+]
# Whether to halt the generator if the task exits with a non-success
# exit status.
#
# [+:capture+]
# Whether to capture and return the output of the task.
#
# [+:sudo+]
# Whether to run the task using +sudo+.
def rake(command, options = {})
execute_command :rake, command, options
end
# Runs the specified \Rails command.
#
# rails_command "db:migrate"
# rails_command "db:migrate", env: "production"
# rails_command "db:migrate", abort_on_failure: true
# rails_command "stats", capture: true
# rails_command "gems:install", sudo: true
#
# ==== Options
#
# [+:env+]
# The \Rails environment in which to run the command. Defaults to
# <tt>ENV["RAILS_ENV"] || "development"</tt>.
#
# [+:abort_on_failure+]
# Whether to halt the generator if the command exits with a non-success
# exit status.
#
# [+:capture+]
# Whether to capture and return the output of the command.
#
# [+:sudo+]
# Whether to run the command using +sudo+.
def rails_command(command, options = {})
if options[:inline]
log :rails, command
command, *args = Shellwords.split(command)
in_root do
silence_warnings do
::Rails::Command.invoke(command, args, **options)
end
end
else
execute_command :rails, command, options
end
end
# Make an entry in \Rails routing file <tt>config/routes.rb</tt>
#
# route "root 'welcome#index'"
# route "root 'admin#index'", namespace: :admin
def route(routing_code, namespace: nil)
namespace = Array(namespace)
namespace_pattern = route_namespace_pattern(namespace)
routing_code = namespace.reverse.reduce(routing_code) do |code, name|
"namespace :#{name} do\n#{rebase_indentation(code, 2)}end"
end
log :route, routing_code
in_root do
if namespace_match = match_file("config/routes.rb", namespace_pattern)
base_indent, *, existing_block_indent = namespace_match.captures.compact.map(&:length)
existing_line_pattern = /^[ ]{,#{existing_block_indent}}\S.+\n?/
routing_code = rebase_indentation(routing_code, base_indent + 2).gsub(existing_line_pattern, "")
namespace_pattern = /#{Regexp.escape namespace_match.to_s}/
end
inject_into_file "config/routes.rb", routing_code, after: namespace_pattern, verbose: false, force: false
if behavior == :revoke && namespace.any? && namespace_match
empty_block_pattern = /(#{namespace_pattern})((?:\s*end\n){1,#{namespace.size}})/
gsub_file "config/routes.rb", empty_block_pattern, verbose: false, force: true do |matched|
beginning, ending = empty_block_pattern.match(matched).captures
ending.sub!(/\A\s*end\n/, "") while !ending.empty? && beginning.sub!(/^[ ]*namespace .+ do\n\s*\z/, "")
beginning + ending
end
end
end
end
# Reads the given file at the source root and prints it in the console.
#
# readme "README"
def readme(path)
log File.read(find_in_source_paths(path))
end
private
# Define log for backwards compatibility. If just one argument is sent,
# invoke say, otherwise invoke say_status. Differently from say and
# similarly to say_status, this method respects the quiet? option given.
def log(*args) # :doc:
if args.size == 1
say args.first.to_s unless options.quiet?
else
args << (behavior == :invoke ? :green : :red)
say_status(*args)
end
end
# Runs the supplied command using either "rake ..." or "rails ..."
# based on the executor parameter provided.
def execute_command(executor, command, options = {}) # :doc:
log executor, command
sudo = options[:sudo] && !Gem.win_platform? ? "sudo " : ""
config = {
env: { "RAILS_ENV" => (options[:env] || ENV["RAILS_ENV"] || "development") },
verbose: false,
capture: options[:capture],
abort_on_failure: options[:abort_on_failure],
}
in_root { run("#{sudo}#{Shellwords.escape Gem.ruby} bin/#{executor} #{command}", config) }
end
# Always returns value in double quotes.
def quote(value) # :doc:
if value.respond_to? :each_pair
return value.map do |k, v|
"#{k}: #{quote(v)}"
end.join(", ")
end
return value.inspect unless value.is_a? String
"\"#{value.tr("'", '"')}\""
end
# Returns optimized string with indentation
def optimize_indentation(value, amount = 0) # :doc:
return "#{value}\n" unless value.is_a?(String)
"#{value.strip_heredoc.indent(amount).chomp}\n"
end
alias rebase_indentation optimize_indentation
# Indent the +Gemfile+ to the depth of @indentation
def indentation # :doc:
" " * @indentation
end
# Manage +Gemfile+ indentation for a DSL action block
def with_indentation(&block) # :doc:
@indentation += 1
instance_eval(&block)
ensure
@indentation -= 1
end
# Append string to a file with a newline if necessary
def append_file_with_newline(path, str, options = {})
gsub_file path, /\n?\z/, options do |match|
match.end_with?("\n") ? "" : "\n#{str}\n"
end
end
def match_file(path, pattern)
File.read(path).match(pattern) if File.exist?(path)
end
def route_namespace_pattern(namespace)
namespace.each_with_index.reverse_each.reduce(nil) do |pattern, (name, i)|
cummulative_margin = "\\#{i + 1}[ ]{2}"
blank_or_indented_line = "^[ ]*\n|^#{cummulative_margin}.*\n"
"(?:(?:#{blank_or_indented_line})*?^(#{cummulative_margin})namespace :#{name} do\n#{pattern})?"
end.then do |pattern|
/^([ ]*).+\.routes\.draw do[ ]*\n#{pattern}/
end
end
end
end
end