pineapples 0.1.0 → 0.3.34

Sign up to get free protection for your applications and to get access to all the features.
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