pineapples 0.1.0 → 0.3.34

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.
Files changed (117) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rspec +1 -0
  4. data/.ruby-gemset +1 -0
  5. data/bin/pineapples +4 -9
  6. data/lib/pineapples/actions/apply.rb +34 -0
  7. data/lib/pineapples/actions/base/action.rb +72 -0
  8. data/lib/pineapples/actions/base/target.rb +136 -0
  9. data/lib/pineapples/actions/chmod.rb +28 -0
  10. data/lib/pineapples/actions/copy_file.rb +48 -0
  11. data/lib/pineapples/actions/create_file.rb +114 -0
  12. data/lib/pineapples/actions/directory.rb +115 -0
  13. data/lib/pineapples/actions/empty_directory.rb +71 -0
  14. data/lib/pineapples/actions/get.rb +38 -0
  15. data/lib/pineapples/actions/git.rb +17 -0
  16. data/lib/pineapples/actions/gsub_file.rb +36 -0
  17. data/lib/pineapples/actions/insert_into_file.rb +131 -0
  18. data/lib/pineapples/actions/inside.rb +69 -0
  19. data/lib/pineapples/actions/keep_file.rb +15 -0
  20. data/lib/pineapples/actions/prepend_to_class.rb +26 -0
  21. data/lib/pineapples/actions/rails/copy_migration.rb +67 -0
  22. data/lib/pineapples/actions/rails/erb_converters.rb +37 -0
  23. data/lib/pineapples/actions/rails/rails.rb +186 -0
  24. data/lib/pineapples/actions/remove_file.rb +28 -0
  25. data/lib/pineapples/actions/ruby.rb +49 -0
  26. data/lib/pineapples/actions/shell.rb +40 -0
  27. data/lib/pineapples/actions/template.rb +35 -0
  28. data/lib/pineapples/actions.rb +124 -39
  29. data/lib/pineapples/app_builder.rb +70 -0
  30. data/lib/pineapples/app_generator.rb +187 -0
  31. data/lib/pineapples/build_tasks/root_files.rb +23 -0
  32. data/lib/pineapples/error.rb +7 -0
  33. data/lib/pineapples/helpers.rb +34 -0
  34. data/lib/pineapples/parser.rb +37 -0
  35. data/lib/pineapples/setting.rb +155 -0
  36. data/lib/pineapples/settings.rb +31 -0
  37. data/lib/pineapples/templates/.buildpacks +2 -0
  38. data/lib/pineapples/templates/.editor-config +11 -0
  39. data/lib/pineapples/templates/.example.env.tt +11 -0
  40. data/lib/pineapples/templates/.example.rspec +3 -0
  41. data/lib/pineapples/templates/.gitignore +23 -0
  42. data/lib/pineapples/templates/Aptfile +0 -0
  43. data/lib/pineapples/templates/Gemfile.tt +89 -0
  44. data/lib/pineapples/templates/Guardfile +27 -0
  45. data/lib/pineapples/templates/Procfile +1 -0
  46. data/lib/pineapples/templates/README.md.tt +32 -0
  47. data/lib/pineapples/templates/Rakefile +6 -0
  48. data/lib/pineapples/templates/app/assets/javascripts/application.js +1 -0
  49. data/lib/pineapples/templates/app/assets/javascripts/libs.js +2 -0
  50. data/lib/pineapples/templates/app/assets/stylesheets/application.scss +1 -0
  51. data/lib/pineapples/templates/app/controllers/application_controller.rb.tt +48 -0
  52. data/lib/pineapples/templates/app/controllers/auth/confirmations_controller.rb +2 -0
  53. data/lib/pineapples/templates/app/controllers/pages_controller.rb +6 -0
  54. data/lib/pineapples/templates/app/helpers/application_helper.rb +59 -0
  55. data/lib/pineapples/templates/app/helpers/title_helper.rb +77 -0
  56. data/lib/pineapples/templates/app/models/user!=needs_user_model!.rb.tt +28 -0
  57. data/lib/pineapples/templates/app/views/common/_flashes.html.erb +16 -0
  58. data/lib/pineapples/templates/app/views/common/_footer.html.erb +3 -0
  59. data/lib/pineapples/templates/app/views/common/_header.html.erb +5 -0
  60. data/lib/pineapples/templates/app/views/layouts/application.html.erb.tt +33 -0
  61. data/lib/pineapples/templates/app/views/pages/home.html.erb +4 -0
  62. data/lib/pineapples/templates/bin/bundle +3 -0
  63. data/lib/pineapples/templates/bin/rails +8 -0
  64. data/lib/pineapples/templates/bin/rake +8 -0
  65. data/lib/pineapples/templates/bin/setup +33 -0
  66. data/lib/pineapples/templates/bin/spring +15 -0
  67. data/lib/pineapples/templates/browserlist +4 -0
  68. data/lib/pineapples/templates/config/application.rb.tt +43 -0
  69. data/lib/pineapples/templates/config/boot.rb +5 -0
  70. data/lib/pineapples/templates/config/database.yml.tt +20 -0
  71. data/lib/pineapples/templates/config/environment.rb +5 -0
  72. data/lib/pineapples/templates/config/environments/development.rb +64 -0
  73. data/lib/pineapples/templates/config/environments/production.rb +77 -0
  74. data/lib/pineapples/templates/config/environments/test.rb +34 -0
  75. data/lib/pineapples/templates/config/i18n-tasks.yml +13 -0
  76. data/lib/pineapples/templates/config/initializers/assets.rb +11 -0
  77. data/lib/pineapples/templates/config/initializers/backtrace_silencers.rb +7 -0
  78. data/lib/pineapples/templates/config/initializers/carrierwave.rb +22 -0
  79. data/lib/pineapples/templates/config/initializers/cookies_serializer.rb +3 -0
  80. data/lib/pineapples/templates/config/initializers/filter_parameters_logging.rb +4 -0
  81. data/lib/pineapples/templates/config/initializers/inflections.rb +16 -0
  82. data/lib/pineapples/templates/config/initializers/mime_types.rb +4 -0
  83. data/lib/pineapples/templates/config/initializers/pry.rb +10 -0
  84. data/lib/pineapples/templates/config/initializers/redis.rb +3 -0
  85. data/lib/pineapples/templates/config/initializers/session_store.rb.tt +3 -0
  86. data/lib/pineapples/templates/config/initializers/wrap_parameters.rb +14 -0
  87. data/lib/pineapples/templates/config/locales/en.yml +19 -0
  88. data/lib/pineapples/templates/config/puma.rb +21 -0
  89. data/lib/pineapples/templates/config/routes.rb.tt +10 -0
  90. data/lib/pineapples/templates/config/secrets.yml +11 -0
  91. data/lib/pineapples/templates/config/smtp.rb +11 -0
  92. data/lib/pineapples/templates/config.ru.tt +7 -0
  93. data/lib/pineapples/templates/db/migrate/01_enable_postgres_extensions.rb +15 -0
  94. data/lib/pineapples/templates/db/migrate/02_create_data_migrations.rb +14 -0
  95. data/lib/pineapples/templates/db/migrate/03_devise_create_users.rb +41 -0
  96. data/lib/pineapples/templates/db/migrate/04_add_role_field_to_users.rb +7 -0
  97. data/lib/pineapples/templates/lib/devise!=devise!/ajax_failure.rb +11 -0
  98. data/lib/pineapples/templates/lib/extensions/form_builder.rb +24 -0
  99. data/lib/pineapples/templates/lib/extensions/http_errors.rb +34 -0
  100. data/lib/pineapples/templates/lib/extensions/to_boolean.rb +45 -0
  101. data/lib/pineapples/templates/lib/logging/custom_rack_logger.rb +11 -0
  102. data/lib/pineapples/templates/lib/logging/custom_request_logger.rb +65 -0
  103. data/lib/pineapples/templates/lib/simple_form/components/append.rb +15 -0
  104. data/lib/pineapples/templates/lib/simple_form/components/prepend.rb +15 -0
  105. data/lib/pineapples/templates/lib/tasks/admin!=user_role_field!.rake +52 -0
  106. data/lib/pineapples/templates/lib/tasks/auto_annotate_models.rake +37 -0
  107. data/lib/pineapples/templates/lib/templates/erb/scaffold/_form.html.erb +14 -0
  108. data/lib/pineapples/templates/lib/virtus/virtus.rb +23 -0
  109. data/lib/pineapples/templates/public/404.html +67 -0
  110. data/lib/pineapples/templates/public/422.html +67 -0
  111. data/lib/pineapples/templates/public/500.html +66 -0
  112. data/lib/pineapples/templates/public/favicon.ico +0 -0
  113. data/lib/pineapples/templates/public/robots.txt +5 -0
  114. data/lib/pineapples/version.rb +1 -1
  115. data/lib/pineapples.rb +17 -2
  116. data/pineapples.gemspec +16 -15
  117. metadata +208 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d5ed18a509d099566d64609ab821db288ce0afe8
4
- data.tar.gz: b992e9901788c706d83b2c725754c59be5f35c93
3
+ metadata.gz: 4168dcbfa92187c3d8bcf7b67f594cb7a3308067
4
+ data.tar.gz: 9fd839446bd1602e213722deb11049aa9cd9c642
5
5
  SHA512:
6
- metadata.gz: 8cefd568c868fa1950afc8482cde7ac622634e21a8560bfc8b2289ccf017a021b34dd00e0ef7479a125b12cc30081bbd288a98cba2e420b6b2920fb3791b6d6b
7
- data.tar.gz: 5bbe87d1fbacc84230db364348e83eb86c4bac7422306786cf752cc798d88bcfb8b127349cf64ef298486da1f2c3d505cef2fda20fd2dd35bf97572af503f6db
6
+ metadata.gz: 92f8f1dfe6d724cc76085cd4cda7f55c0ba0bb03557292b156310b3613c5afbd70d75537abffc289c513a7280068ba4b2d1f9c3d152f276e8e84867be3279fb5
7
+ data.tar.gz: fedbe56b5c2475020d96dabe94a904cf4b11ff6c41bf3994a98e3a7724b32ad0b9581d5b3a61613da04d4887f5976452ccf0ba41dba520d8297fba1a11b551dd
data/.gitignore CHANGED
@@ -1,3 +1,4 @@
1
+ *.gem
1
2
  /.bundle/
2
3
  /.yardoc
3
4
  /Gemfile.lock
data/.rspec CHANGED
@@ -1,2 +1,3 @@
1
+ --require spec_helper
1
2
  --format documentation
2
3
  --color
data/.ruby-gemset ADDED
@@ -0,0 +1 @@
1
+ pineapples
data/bin/pineapples CHANGED
@@ -6,13 +6,8 @@ $LOAD_PATH << source_path
6
6
 
7
7
  require 'pineapples'
8
8
 
9
- if ['create', '--create'].include? ARGV[0]
10
- ARGV.shift
11
- puts "[WARNING] the suspenders create argument is deprecated. Just use `suspenders #{ARGV.join}` instead"
12
- end
9
+ $terminal.indent_size = 2
13
10
 
14
- templates_root = File.expand_path(File.join('..', 'templates'), File.dirname(__FILE__))
15
- Pineapples::AppGenerator.source_root templates_root
16
- Pineapples::AppGenerator.source_paths << Rails::Generators::AppGenerator.source_root << templates_root
17
-
18
- Pineapples::AppGenerator.start
11
+ options = Pineapples::Parser.parse(ARGV)
12
+ generator = Pineapples::AppGenerator.new(options)
13
+ generator.start!
@@ -0,0 +1,34 @@
1
+ module Pineapples
2
+ module Actions
3
+ # Loads an external file and execute it in the instance binding.
4
+ #
5
+ # ==== Parameters
6
+ # path<String>:: The path to the file to execute. Can be a web address or
7
+ # a relative path from the source root.
8
+ #
9
+ # ==== Examples
10
+ #
11
+ # apply "http://gist.github.com/103208"
12
+ #
13
+ # apply "recipes/jquery.rb"
14
+ #
15
+ def apply(path, options = {})
16
+ verbose = options.fetch(:verbose, verbose?)
17
+ is_uri = path =~ %r{^https?\://}
18
+ path = find_in_source_paths(path) if !is_uri
19
+ color = options.fetch(:color, DEFAULT_COLOR)
20
+
21
+ say_status(:apply, path, color, verbose)
22
+
23
+ indent(verbose) do
24
+ if is_uri
25
+ contents = open(path, 'Accept' => 'application/x-thor-template') { |io| io.read }
26
+ else
27
+ contents = open(path) { |io| io.read }
28
+ end
29
+
30
+ instance_eval(contents, path)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,72 @@
1
+ module Pineapples
2
+ module Actions
3
+ class Action
4
+ STATUS_COLORS = {creative: :light_green,
5
+ destructive: :light_red,
6
+ neutral: :light_blue,
7
+ warning: :light_yellow}
8
+
9
+ # Reference to AppGenerator instance
10
+ attr_reader :generator
11
+
12
+ attr_reader :colors
13
+
14
+ # Options hash
15
+ attr_accessor :options
16
+
17
+ # Let status colors set by .status_color macro be inherited
18
+ def self.inherited(child_class)
19
+ super
20
+ child_class.instance_variable_set('@colors', self.colors)
21
+ end
22
+
23
+ def self.status_color(status, color)
24
+ colors[status] = color
25
+ end
26
+
27
+ def self.colors
28
+ @colors ||= {}
29
+ end
30
+
31
+ def initialize(generator, options = {})
32
+ @generator = generator
33
+ @options = options
34
+ end
35
+
36
+ def invoke!
37
+ raise NotImplementedError, "You should really implement invoke! method on Action subclasses"
38
+ end
39
+
40
+ def skip?
41
+ @skip = false if @skip.nil?
42
+ @skip
43
+ end
44
+
45
+ protected
46
+
47
+ def pretend?
48
+ generator.pretend?
49
+ end
50
+
51
+ def execute?
52
+ !pretend?
53
+ end
54
+
55
+ def verbose?
56
+ options[:verbose] || generator.verbose?
57
+ end
58
+
59
+ # used to log action actions (no pun intended)
60
+ def say_status(status, message = self.message, color = nil)
61
+ color = color(status) if color.nil?
62
+ generator.say_status(status, message, color) if verbose?
63
+ end
64
+
65
+ def color(status)
66
+ color = self.class.colors[status]
67
+ color = STATUS_COLORS[color] if STATUS_COLORS.keys.include?(color)
68
+ end
69
+
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,136 @@
1
+ module Pineapples
2
+ module Actions
3
+ class Target
4
+ PASS_MATCH = /!=(.*?)!/
5
+ GUARD_MATCH = /!(.*?)!/
6
+ EVAL_MATCH = /%(.*?)%/
7
+
8
+ attr_reader :given,
9
+ :fullpath,
10
+ :relative,
11
+ :action
12
+
13
+ def initialize(target, action)
14
+ raise Error, 'Target should not be falsy' if !target
15
+
16
+ @action = action
17
+ @given = target.to_s
18
+
19
+ evaluate_pass_method!
20
+ evaluate_guard_methods!
21
+ evaluate_filename_method!
22
+
23
+ @fullpath = File.expand_path(@given, generator.current_app_dir)
24
+ @relative = generator.relative_to_current_app_dir(@fullpath)
25
+ end
26
+
27
+ def generator
28
+ action.generator
29
+ end
30
+
31
+ def skip?
32
+ @skip = false if @skip.nil?
33
+ @skip
34
+ end
35
+
36
+ private
37
+
38
+ def evaluate_pass_method!
39
+ if pass_match
40
+ raise Error, methods_missing_error_message(pass_methods) if !generator_pass_method
41
+
42
+ @skip = !generator.send(generator_pass_method)
43
+ @given.gsub!(pass_match[0], '')
44
+ end
45
+ end
46
+
47
+ def evaluate_guard_methods!
48
+ if guard_match
49
+ raise Error, methods_missing_error_message(guard_methods) if !generator_guard_method
50
+
51
+ @skip = generator.send(generator_guard_method)
52
+ @given.gsub!(guard_match[0], '')
53
+ end
54
+ end
55
+
56
+ def evaluate_filename_method!
57
+ if eval_match
58
+ raise Error, methods_missing_error_message(filename_method) if !generator_filename_method
59
+
60
+ filename = generator.send(filename_method)
61
+ @given.gsub!(eval_match[0], filename)
62
+ end
63
+ end
64
+
65
+ def pass_match
66
+ PASS_MATCH.match(@given)
67
+ end
68
+
69
+ def guard_match
70
+ GUARD_MATCH.match(@given)
71
+ end
72
+
73
+ def eval_match
74
+ EVAL_MATCH.match(@given)
75
+ end
76
+
77
+ def generator_pass_method
78
+ if @pass_method.nil?
79
+ pass_methods.each do |method|
80
+ if generator.respond_to?(method, true)
81
+ @pass_method = method
82
+ break
83
+ end
84
+ end
85
+ end
86
+ @pass_method = false if @pass_method.nil?
87
+ @pass_method
88
+ end
89
+
90
+ def generator_guard_method
91
+ if @guard_method.nil?
92
+ guard_methods.each do |method|
93
+ if generator.respond_to?(method, true)
94
+ @guard_method = method
95
+ break
96
+ end
97
+ end
98
+ end
99
+ @guard_method = false if @guard_method.nil?
100
+ @guard_method
101
+ end
102
+
103
+ def generator_filename_method
104
+ if @filename_method.nil?
105
+ @filename_method = generator.respond_to?(filename_method, true) ? filename_method : false
106
+ end
107
+ @filename_method
108
+ end
109
+
110
+ def guard_methods
111
+ guard_method = guard_match[1].strip
112
+ predicate_method = guard_method + '?'
113
+ [guard_method, predicate_method]
114
+ end
115
+
116
+ def pass_methods
117
+ pass_method = pass_match[1].strip
118
+ predicate_method = pass_method + '?'
119
+ [pass_method, predicate_method]
120
+ end
121
+
122
+ def filename_method
123
+ eval_match[1].strip
124
+ end
125
+
126
+ def methods_missing_error_message(methods)
127
+ plural = methods.is_a?(Array)
128
+ methods = methods.join(', ') if plural
129
+ methods_message = plural ? 'methods' : 'method'
130
+
131
+ "No instance #{methods_message} #{methods} for AppGenerator, can't evaluate filepath #{given}"
132
+ end
133
+ end
134
+
135
+ end
136
+ end
@@ -0,0 +1,28 @@
1
+ module Pineapples
2
+ module Actions
3
+ # Changes the mode of the given file or directory.
4
+ #
5
+ # ==== Parameters
6
+ # mode<Integer>:: the file mode
7
+ # path<String>:: the name of the file to change mode
8
+ # options<Hash>:: give :verbose => false to not log the status.
9
+ #
10
+ # ==== Example
11
+ #
12
+ # chmod "script/server", 0755
13
+ #
14
+ def chmod(path, mode, options = {})
15
+ return unless behaviour == :invoke
16
+
17
+ verbose = options.fetch(:verbose, verbose?)
18
+ execute = !options.fetch(:pretend, pretend?)
19
+ color = options.fetch(:color, DEFAULT_COLOR)
20
+
21
+ relative_path = relative_to_app_root(File.join(app_root, path))
22
+
23
+ say_status(:chmod, relative_path, color, verbose)
24
+
25
+ FileUtils.chmod_R(mode, path) if execute
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,48 @@
1
+ require_relative 'create_file'
2
+
3
+ module Pineapples
4
+ module Actions
5
+ # Copies the file from the relative source to the relative destination. If
6
+ # the destination is not given it's assumed to be equal to the source.
7
+ #
8
+ # ==== Parameters
9
+ # source<String>:: the relative path to the source root.
10
+ # target<String>:: the relative path to the destination root.
11
+ # options<Hash>:: give :verbose => false to not log the status, and
12
+ # :mode => :preserve, to preserve the file mode from the source.
13
+
14
+ #
15
+ # ==== Examples
16
+ #
17
+ # copy_file "README", "doc/README"
18
+ #
19
+ # copy_file "doc/README"
20
+ #
21
+ def copy_file(source, *args, &block)
22
+ options = args.last.is_a?(Hash) ? args.pop : {}
23
+ target = args.first || source
24
+
25
+ action CopyFile.new(self, source, target, options, &block)
26
+ end
27
+
28
+ class CopyFile < CreateFile
29
+ attr_reader :source
30
+
31
+ def initialize(generator, source, target, options, &block)
32
+ @source = File.expand_path(generator.find_in_source_paths(source.to_s))
33
+ @content = File.binread(@source)
34
+ @content = block.call(@content) if block
35
+ super(generator, target, @content, options)
36
+ end
37
+
38
+ def invoke!
39
+ super
40
+ if options[:mode] == :preserve
41
+ mode = File.stat(source).mode
42
+ generator.chmod(target, mode, options)
43
+ end
44
+ end
45
+
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,114 @@
1
+ require_relative 'empty_directory'
2
+
3
+ module Pineapples
4
+ module Actions
5
+ # Create a new file relative to the destination root with the given data,
6
+ # which is the return value of a block or a data string.
7
+ #
8
+ # ==== Parameters
9
+ # target<String>:: the relative path to the destination root.
10
+ # content<String|NilClass>:: the data to append to the file.
11
+ # options<Hash>:: give :verbose => false to not log the status.
12
+ #
13
+ # ==== Examples
14
+ #
15
+ # create_file "lib/fun_party.rb" do
16
+ # hostname = ask("What is the virtual hostname I should use?")
17
+ # "vhost.name = #{hostname}"
18
+ # end
19
+ #
20
+ # create_file "config/apache.conf", "your apache config"
21
+ #
22
+ def create_file(target, *args, &block)
23
+ options = args.last.is_a?(Hash) ? args.pop : {}
24
+ content = args.first
25
+ action CreateFile.new(self, target, block || content, options)
26
+ end
27
+
28
+ def ask_file_collision(target) # rubocop:disable MethodLength
29
+ return true if @always_force
30
+ options = '[Ynaq]'
31
+
32
+ question_string = "Overwrite #{target}?"
33
+ options = ['Yes', 'No', 'Always', 'Quit']
34
+ colors = [:light_green, :light_red, :light_yellow, :light_red]
35
+ options_with_color = options.map.with_index { |option, index| option.send(colors[index])}
36
+
37
+ loop do
38
+ answer_index = Ask.list(question_string, options_with_color, {clear: false, response: false})
39
+ answer = options[answer_index]
40
+
41
+ case answer
42
+ when 'Yes'
43
+ return true
44
+ when 'No'
45
+ return false
46
+ when 'Always'
47
+ return @always_force = true
48
+ when 'Quit'
49
+ say 'Aborting'
50
+ raise SystemExit
51
+ end
52
+ end
53
+ end
54
+
55
+ class CreateFile < EmptyDirectory
56
+ attr_reader :content
57
+
58
+ status_color :identical, :neutral
59
+ status_color :force, :warning
60
+ status_color :skip, :warning
61
+ status_color :conflict, :destructive
62
+
63
+ def initialize(generator, target, content, options = {})
64
+ super(generator, target, options)
65
+ @content = content
66
+ end
67
+
68
+ def identical?
69
+ exists? && File.binread(target.fullpath) == render
70
+ end
71
+
72
+ def render
73
+ @render ||= content.is_a?(Proc) ? content.call : content.to_s
74
+ end
75
+
76
+ def invoke!
77
+ invoke_with_conflict_check do
78
+ FileUtils.mkdir_p(File.dirname(target.fullpath))
79
+ File.open(target.fullpath, 'wb') { |file| file.write render }
80
+ end
81
+ target.given
82
+ end
83
+
84
+ protected
85
+
86
+ def on_conflict_behaviour(&block)
87
+ if identical?
88
+ say_status :identical
89
+ else
90
+ force_or_skip_or_conflict(options[:force], options[:skip], &block)
91
+ end
92
+ end
93
+
94
+ def force_or_skip_or_conflict(force, skip, &block)
95
+ if force
96
+ say_status :force
97
+ block.call if execute?
98
+ elsif skip
99
+ say_status :skip
100
+ else
101
+ say_status :conflict
102
+ force_or_skip_or_conflict(force_on_collision?, true, &block)
103
+ end
104
+ end
105
+
106
+ # Shows the file collision menu to the user and gets the result.
107
+ #
108
+ def force_on_collision?
109
+ generator.ask_file_collision(target)
110
+ end
111
+
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,115 @@
1
+ module Pineapples
2
+ module Actions
3
+ # Copies recursively the files from source directory to root directory.
4
+ # If any of the files finishes with .tt, it's considered to be a template
5
+ # and is placed in the destination without the extension .tt. If any
6
+ # empty directory is found, it's copied and all .empty_directory files are
7
+ # ignored. If any file name is wrapped within % signs, the text within
8
+ # the % signs will be executed as a method and replaced with the returned
9
+ # value. Let's suppose a doc directory with the following files:
10
+ #
11
+ # doc/
12
+ # components/.empty_directory
13
+ # README
14
+ # rdoc.rb.tt
15
+ # %app_name%.rb
16
+ #
17
+ # When invoked as:
18
+ #
19
+ # directory "doc"
20
+ #
21
+ # It will create a doc directory in the destination with the following
22
+ # files (assuming that the `app_name` method returns the value "blog"):
23
+ #
24
+ # doc/
25
+ # components/
26
+ # README
27
+ # rdoc.rb
28
+ # blog.rb
29
+ #
30
+ # ==== Parameters
31
+ # source<String>:: the relative path to the source root.
32
+ # target<String>:: the relative path to the destination root.
33
+ # options<Hash>:: give :verbose => false to not log the status.
34
+ # If :recursive => false, does not look for paths recursively.
35
+ # If :mode => :preserve, preserve the file mode from the source.
36
+ # If :exclude_pattern => /regexp/, prevents copying files that match that regexp.
37
+ #
38
+ # ==== Examples
39
+ #
40
+ # directory "doc"
41
+ # directory "doc", "docs", :recursive => false
42
+ #
43
+ def directory(source, *args, &block)
44
+ options = args.last.is_a?(Hash) ? args.pop : {}
45
+ target = args.first || source
46
+ action Directory.new(self, source, target, options, &block)
47
+ end
48
+
49
+ class Directory < EmptyDirectory
50
+ attr_reader :source
51
+
52
+ def initialize(generator, source, target, options, &block)
53
+ super(generator, target, {recursive: true}.merge(options))
54
+
55
+ @source = File.expand_path(generator.find_in_source_paths(source.to_s))
56
+ @block = block
57
+ end
58
+
59
+ def invoke!
60
+ generator.empty_directory(target.given, options)
61
+ execute!
62
+ end
63
+
64
+ def revoke!
65
+ execute!
66
+ end
67
+
68
+ protected
69
+
70
+ def execute!
71
+ lookup = escape_globs(source)
72
+ lookup = options[:recursive] ? File.join(lookup, '**') : lookup
73
+ lookup = file_level_lookup(lookup)
74
+
75
+ excluded_files = Array(options[:exclude])
76
+ exclude_pattern = options[:exclude_pattern]
77
+
78
+ files(lookup).sort.each do |file_source|
79
+ next if File.directory?(file_source)
80
+ next if exclude_pattern && file_source.match(exclude_pattern)
81
+ next if excluded_files.any? do |excluded_file|
82
+ File.basename(excluded_file) == File.basename(file_source)
83
+ end
84
+
85
+ file_target = File.join(target.given, file_source.gsub(source, '.'))
86
+ file_target.gsub!('/./', '/')
87
+
88
+ case file_source
89
+ when /\.empty_directory$/
90
+ dirname = File.dirname(file_target).gsub(/\/\.$/, '')
91
+ next if dirname == target.given
92
+ generator.empty_directory(dirname, options)
93
+ when /#{TEMPLATE_EXTNAME}$/
94
+ generator.template(file_source, file_target[0..-4], options, &@block)
95
+ else
96
+ generator.copy_file(file_source, file_target, options, &@block)
97
+ end
98
+ end
99
+ end
100
+
101
+ def file_level_lookup(previous_lookup)
102
+ File.join(previous_lookup, '*')
103
+ end
104
+
105
+ def files(lookup)
106
+ Dir.glob(lookup, File::FNM_DOTMATCH)
107
+ end
108
+
109
+ def escape_globs(path)
110
+ path.to_s.gsub(/[*?{}\[\]]/, '\\\\\\&')
111
+ end
112
+ end
113
+
114
+ end
115
+ end
@@ -0,0 +1,71 @@
1
+ module Pineapples
2
+ module Actions
3
+ # Creates an empty directory.
4
+ #
5
+ # ==== Parameters
6
+ # target<String>:: the relative path to the app root.
7
+ # options<Hash>:: give :verbose => false to not log the status.
8
+ #
9
+ # ==== Examples
10
+ #
11
+ # empty_directory "doc"
12
+ #
13
+ def empty_directory(target, options = {})
14
+ action EmptyDirectory.new(self, target, options)
15
+ end
16
+
17
+ class EmptyDirectory < Action
18
+ attr_reader :target,
19
+ :skip
20
+
21
+ status_color :create, :creative
22
+ status_color :remove, :destructive
23
+ status_color :exist, :neutral
24
+
25
+ def initialize(generator, target, options = {})
26
+ super(generator, options)
27
+
28
+ @target = Target.new(target, self)
29
+ @skip = @target.skip?
30
+ end
31
+
32
+ def invoke!
33
+ invoke_with_conflict_check do
34
+ ::FileUtils.mkdir_p(target.fullpath)
35
+ end
36
+ end
37
+
38
+ def revoke!
39
+ say_status :remove
40
+ ::FileUtils.rm_rf(target.fullpath) if execute? && exists?
41
+ given_target
42
+ end
43
+
44
+ def exists?
45
+ ::File.exist?(target.fullpath)
46
+ end
47
+
48
+ protected
49
+
50
+ def invoke_with_conflict_check(&block)
51
+ if exists?
52
+ on_conflict_behaviour(&block)
53
+ else
54
+ say_status :create
55
+ block.call if execute?
56
+ end
57
+
58
+ target.fullpath
59
+ end
60
+
61
+ def on_conflict_behaviour(&block)
62
+ say_status :exist
63
+ end
64
+
65
+ def message
66
+ target.relative
67
+ end
68
+
69
+ end
70
+ end
71
+ end