app_stack 1.3.4 → 1.4.0

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 (79) hide show
  1. checksums.yaml +15 -0
  2. data/Gemfile +6 -5
  3. data/Gemfile.lock +17 -5
  4. data/README.md +13 -1
  5. data/app_stack.gemspec +6 -5
  6. data/lib/app_stack/app.rb +86 -0
  7. data/lib/app_stack/cli_options.rb +42 -36
  8. data/lib/app_stack/compare_list.rb +195 -0
  9. data/lib/app_stack/configuration.rb +119 -0
  10. data/lib/app_stack/operator.rb +60 -0
  11. data/lib/app_stack/version.rb +1 -1
  12. data/lib/app_stack.rb +34 -6
  13. data/spec/app_stack_spec.rb +17 -0
  14. data/spec/cli_options_spec.rb +20 -0
  15. data/spec/compare_list_spec.rb +18 -0
  16. data/spec/configuration_spec.rb +95 -0
  17. data/spec/examples/la-assets-helper/app_stack.yml +14 -0
  18. data/spec/examples/la-assets-helper/lib/la_assets.rb +319 -0
  19. data/spec/examples/la-import_app/Trivia.erb +2 -0
  20. data/spec/examples/la-import_app/app_stack.yml +83 -0
  21. data/spec/examples/la-mongoid/app_stack.yml +49 -0
  22. data/spec/examples/la-mongoid/config/boot.rb +15 -0
  23. data/spec/examples/la-mongoid/config/database.rb +12 -0
  24. data/spec/examples/la-mongoid/config/mongoid.yml +22 -0
  25. data/spec/examples/la-mongoid/config/mongoid.yml.erb +22 -0
  26. data/spec/examples/la-sinatra/Gemfile.erb +11 -0
  27. data/spec/examples/la-sinatra/app_stack.yml +16 -0
  28. data/spec/examples/no-stacked/some_list.rb +4 -0
  29. data/spec/examples/sample_config/app_stack.yml +82 -0
  30. data/spec/examples/sample_config/hash_export.yml +4 -0
  31. data/spec/load_stack_spec.rb +13 -0
  32. data/spec/spec_helper.rb +7 -2
  33. data/tags +87 -34
  34. metadata +66 -87
  35. data/.app_stack.yml +0 -14
  36. data/bin/config_assets +0 -3
  37. data/lib/app_stack/copy_list_builder.rb +0 -147
  38. data/lib/app_stack/local_files_parser.rb +0 -95
  39. data/lib/app_stack/stack_app.rb +0 -119
  40. data/spec/config_spec.rb +0 -82
  41. data/spec/fixtures/config_sample/sample.yml +0 -19
  42. data/spec/fixtures/sample_files/module_1/Gemfile.erb +0 -1
  43. data/spec/fixtures/sample_files/module_1/app_stack.yml +0 -10
  44. data/spec/fixtures/sample_files/module_1/config/boot_sample.rb +0 -0
  45. data/spec/fixtures/sample_files/module_1/doc/excluding.txt +0 -1
  46. data/spec/fixtures/sample_files/module_1/doc/including.txt +0 -0
  47. data/spec/fixtures/sample_files/module_1/lib/mixins/b_lib.rb +0 -0
  48. data/spec/fixtures/sample_files/module_1/lib/mixins/s_lib.rb +0 -0
  49. data/spec/fixtures/sample_files/module_1/lib/samples/a_lib.rb +0 -0
  50. data/spec/fixtures/sample_files/module_1/spec/config_spec.rb +0 -0
  51. data/spec/fixtures/sample_files/module_2/Gemfile +0 -0
  52. data/spec/fixtures/sample_files/module_2/Gemfile.liquid +0 -1
  53. data/spec/fixtures/sample_files/module_2/app/lib/sequence.rb +0 -0
  54. data/spec/fixtures/sample_files/module_2/app_stack.yml +0 -3
  55. data/spec/fixtures/sample_files/module_3/app_stack.yml +0 -3
  56. data/spec/fixtures/sample_files/module_3/bin/del +0 -0
  57. data/spec/fixtures/sample_files/module_3/bin/rm +0 -0
  58. data/spec/fixtures/sample_files/module_3/config/application.yml +0 -0
  59. data/spec/fixtures/sample_files/module_3/config/database_development.yml +0 -0
  60. data/spec/fixtures/sample_files/module_3/config/database_production.yml +0 -0
  61. data/spec/fixtures/sample_files/module_3/lib/libfile_1.rb +0 -0
  62. data/spec/fixtures/sample_files/module_3/lib/libfile_2.rb +0 -0
  63. data/spec/fixtures/sample_files/my_app/app_stack.yml +0 -15
  64. data/spec/fixtures/sample_files/my_app/doc/excluding.txt +0 -1
  65. data/spec/fixtures/sample_files/my_app/local.conf +0 -0
  66. data/spec/fixtures/sample_files/my_app/local.conf.erb +0 -1
  67. data/spec/fixtures/sample_files/my_app/spec/config_spec.rb +0 -0
  68. data/spec/fixtures/sample_files/my_app_back/app_stack.yml +0 -15
  69. data/spec/fixtures/sample_files/my_app_back/doc/excluding.txt +0 -1
  70. data/spec/fixtures/sample_files/my_app_back/local.conf +0 -0
  71. data/spec/fixtures/sample_files/my_app_back/local.conf.erb +0 -1
  72. data/spec/fixtures/sample_files/my_app_back/spec/config_spec.rb +0 -0
  73. data/spec/merger_spec.rb +0 -43
  74. data/spec/stackup_spec.rb +0 -18
  75. /data/{file.erb → spec/examples/find_conf_file/both/.app_stack.yml} +0 -0
  76. /data/spec/{fixtures/sample_files/module_1/Gemfile → examples/find_conf_file/both/app_stack.yml} +0 -0
  77. /data/spec/{fixtures/sample_files/module_1/Rakefile → examples/find_conf_file/with_dot/.app_stack.yml} +0 -0
  78. /data/spec/{fixtures/config_sample → examples/sample_config}/wrong_key.yml +0 -0
  79. /data/spec/{fixtures/config_sample → examples/sample_config}/wrong_type.yml +0 -0
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ YzE2OTk2NTU4ZWJkMTc2ZDA2NmE4OWE4NGUwMjljY2RiNGQ4NTk1Mw==
5
+ data.tar.gz: !binary |-
6
+ MTUwYjA2OTYxYjk2NWEyNzk2YWExZmRiYTA0YTU0ZjViYTI0MTEzYw==
7
+ !binary "U0hBNTEy":
8
+ metadata.gz: !binary |-
9
+ YzFmOThkNzg5YmFiMTdlNzJhODU1ZTczM2JkN2U3NDlkYzQzYmFhMTZiNjdi
10
+ YTFmZTVjYzM3MjMzZmVmYjJjNGZjMGQ4YzQwODQwYzE1MDk0M2FjYWZlNTgw
11
+ YThjM2QxYzhhZTcyNTY5MTMyMTVmZjA0NWI1ZTY3MTEwODEzOTU=
12
+ data.tar.gz: !binary |-
13
+ ZWE1OWMzNDZiZTk3ZjdhYzBhMDU5MmFmZGYwYTNjM2Q2ZWYzZDVjOGVlZWQ5
14
+ ODM0YzVhNWQ3ODMzMzU4NzhjZWRhZThkMDhkMWI3NDQzYWQ2NjAyM2RjYWQ5
15
+ YTI3YWZiMjBjYTc4ODE4NTEwOTQ2ZGVmOWFhZWI3ZDg1YWUwZTc=
data/Gemfile CHANGED
@@ -1,11 +1,12 @@
1
1
  source 'http://ruby.taobao.org'
2
2
 
3
3
  gem 'diffy'
4
- gem 'tilt', '>= 1.4.1'
5
- gem 'term-ansicolor', '>= 1.2.2'
6
- gem 'activesupport', '>= 3.2.14'
4
+ gem 'tilt'
5
+ gem 'term-ansicolor'
6
+ gem 'activesupport'
7
7
 
8
8
  group :development, :test do
9
- gem 'rspec', '>= 2.14.1'
10
- gem 'simplecov', '>= 0.7.1'
9
+ gem 'rspec'
10
+ gem 'rubocop'
11
+ gem 'simplecov'
11
12
  end
data/Gemfile.lock CHANGED
@@ -7,12 +7,18 @@ GEM
7
7
  multi_json (~> 1.3)
8
8
  thread_safe (~> 0.1)
9
9
  tzinfo (~> 0.3.37)
10
+ ast (1.1.0)
10
11
  atomic (1.1.13)
11
12
  diff-lcs (1.2.4)
12
13
  diffy (3.0.1)
13
14
  i18n (0.6.5)
14
15
  minitest (4.7.5)
15
16
  multi_json (1.8.0)
17
+ parser (2.0.0)
18
+ ast (~> 1.1)
19
+ slop (~> 3.4, >= 3.4.5)
20
+ powerpack (0.0.9)
21
+ rainbow (1.1.4)
16
22
  rspec (2.14.1)
17
23
  rspec-core (~> 2.14.0)
18
24
  rspec-expectations (~> 2.14.0)
@@ -21,10 +27,15 @@ GEM
21
27
  rspec-expectations (2.14.3)
22
28
  diff-lcs (>= 1.1.3, < 2.0)
23
29
  rspec-mocks (2.14.3)
30
+ rubocop (0.14.1)
31
+ parser (~> 2.0)
32
+ powerpack (~> 0.0.6)
33
+ rainbow (>= 1.1.4)
24
34
  simplecov (0.7.1)
25
35
  multi_json (~> 1.0)
26
36
  simplecov-html (~> 0.7.1)
27
37
  simplecov-html (0.7.1)
38
+ slop (3.4.6)
28
39
  term-ansicolor (1.2.2)
29
40
  tins (~> 0.8)
30
41
  thread_safe (0.1.2)
@@ -37,9 +48,10 @@ PLATFORMS
37
48
  ruby
38
49
 
39
50
  DEPENDENCIES
40
- activesupport (>= 3.2.14)
51
+ activesupport
41
52
  diffy
42
- rspec (>= 2.14.1)
43
- simplecov (>= 0.7.1)
44
- term-ansicolor (>= 1.2.2)
45
- tilt (>= 1.4.1)
53
+ rspec
54
+ rubocop
55
+ simplecov
56
+ term-ansicolor
57
+ tilt
data/README.md CHANGED
@@ -4,6 +4,17 @@
4
4
 
5
5
  Import source files from a chain of application modules.
6
6
 
7
+ ## Parking list for new version (1.4)
8
+
9
+ - Optional `.app_stack.yml` for stacked modules
10
+ - Better detection for yaml file error
11
+ - Better message with short file name
12
+ - Render files only if contents really changed
13
+ - <del datetime="2013-11-11T10:05:01 +0800">*new file only* mode</del>
14
+ - <del datetime="2013-11-11T10:05:15 +0800">Better simulate, new file only, force mode</del>
15
+ - Recursive stackup (?)
16
+ - Separate sync/import group instead of one single `stackapps`
17
+
7
18
  ## Synposis
8
19
 
9
20
  Put a configuration file `.app_stack.yml` in your application
@@ -13,8 +24,9 @@ directory, in format like:
13
24
 
14
25
  The directory contains other modules to import.
15
26
 
16
- stack:
27
+ import:
17
28
  - module-1
29
+ sync:
18
30
  - module-2: [defaults, tests]
19
31
  - module-3:
20
32
  - defaults
data/app_stack.gemspec CHANGED
@@ -12,11 +12,12 @@ Gem::Specification.new 'app_stack', AppStack::VERSION do |s|
12
12
  s.license = 'MIT'
13
13
  s.test_files = Dir.glob('{spec,test}/**/*.rb')
14
14
 
15
- s.add_dependency 'tilt', '>= 1.4.1'
16
- s.add_dependency 'term-ansicolor', '>= 1.2.2'
17
- s.add_dependency 'activesupport', '>= 3.2.14'
15
+ s.add_dependency 'tilt'
16
+ s.add_dependency 'term-ansicolor'
17
+ s.add_dependency 'activesupport'
18
18
  s.add_dependency 'diffy'
19
- s.add_development_dependency 'rspec', '>= 2.14.1'
20
- s.add_development_dependency 'simplecov', '>= 0.7.1'
19
+ s.add_development_dependency 'rspec'
20
+ s.add_development_dependency 'rubocop'
21
+ s.add_development_dependency 'simplecov'
21
22
  end
22
23
 
@@ -0,0 +1,86 @@
1
+ # encoding: utf-8
2
+
3
+ module AppStack
4
+ # represent a single stacked application
5
+ class App
6
+ attr_accessor :rel_path
7
+ attr_reader :config, :conf_file, :app_stacks, :export_groups,
8
+ :sync_list, :import_list, :render_list,
9
+ :compare_list, :copy_list, :diff_list, :attrs
10
+
11
+ include AppStack::ConfigParser
12
+
13
+ def initialize(filename = nil, app_dir = nil)
14
+ @conf_file, @app_dir = filename, app_dir
15
+ @config = load_config
16
+ info 'loaded configuration: ', @config
17
+
18
+ @app_stacks = {}
19
+ @compare_list = []
20
+ @attrs = {}
21
+ end
22
+
23
+ # rubocop:disable MethodLength
24
+ def load_stack!
25
+ # load_stack.each ...
26
+ validate_config!
27
+
28
+ # parse all stacks
29
+ @import_list.merge(@sync_list).merge(@render_list).keys.each do |app|
30
+ app_dir = File.expand_path(app, File.join(directory, config.stack_dir))
31
+ # rubocop:disable LineLength
32
+ fail ParseError, "Error on #{@conf_file}, stack app #{app} not found in #{app_dir}" unless File.directory?(app_dir)
33
+ # rubocop:enable LineLength
34
+ @app_stacks[app] = App.new(AppStack.find_conf_file(app_dir), app_dir)
35
+ @app_stacks[app].rel_path = File.join(directory, config.stack_dir, app)
36
+ @app_stacks[app].parse_export_groups!
37
+ @attrs.merge! @app_stacks[app].config.attrs
38
+
39
+ # info "load app #{app.bold} as", @app_stacks[app]
40
+ info "load app #{app.bold}"
41
+ end
42
+
43
+ @attrs.merge! config.attrs
44
+ info 'merged attrs', attrs
45
+ @app_stacks
46
+ end
47
+ # rubocop:enable MethodLength
48
+
49
+ def stackup!
50
+ load_stack!
51
+ load_compare_list!
52
+ build_copy_list!
53
+ merge!
54
+ end
55
+
56
+ def options
57
+ AppStack.options
58
+ end
59
+
60
+ def directory
61
+ @app_dir ||= (conf_file && File.dirname(conf_file))
62
+ File.expand_path(@app_dir)
63
+ end
64
+
65
+ # use the direct name of the config file as app name
66
+ def app_name
67
+ directory && File.basename(directory)
68
+ end
69
+
70
+ # echo message only if verbose specified to be true
71
+ def info(msg, var = nil)
72
+ return unless options && options.verbose
73
+
74
+ msg = "[#{app_name || self.class.name}] ".green + msg
75
+ msg += "\n" if var && var.inspect.size > 30
76
+
77
+ print msg
78
+ if var
79
+ print Term::ANSIColor.bold
80
+ var.inspect.size > 30 ? PP.pp(var) : PP.singleline_pp(var)
81
+ print Term::ANSIColor.reset
82
+ end
83
+ puts unless var && var.inspect.size > 30
84
+ end
85
+ end
86
+ end
@@ -2,51 +2,57 @@
2
2
 
3
3
  require 'optparse'
4
4
 
5
- # parse command line options based
6
- # rubocop:disable MethodLength
7
- class CliOptions
8
- attr_accessor :force, :simulate, :verbose, :conf_file
9
-
10
- def initialize(argv = ARGV)
11
- parse_opt!(argv)
12
- case argv.size
13
- when 1 then self.conf_file = argv.shift
14
- when 0 then self.conf_file = AppStack::DEFAULT_CONF_FILE
15
- else fail "too many configuration files (#{argv.join(', ')})."
5
+ module AppStack
6
+ # parse command line options based
7
+ # rubocop:disable MethodLength
8
+ class CliOptions
9
+ attr_accessor :force, :simulate, :verbose, :recursive, :conf_file
10
+
11
+ def initialize(argv = ARGV)
12
+ parse_opt!(argv)
13
+ case argv.size
14
+ when 1 then self.conf_file = argv.shift
15
+ when 0 then self.conf_file = nil
16
+ else fail "too many configuration files (#{argv.join(', ')})."
17
+ end
16
18
  end
17
- end
18
19
 
19
- # parse command line options
20
- def parse_opt!(argv)
21
- opt_parser = OptionParser.new do |opts|
22
- opts.banner = 'Usage: appstack [options] [config_file]'
20
+ # parse command line options
21
+ def parse_opt!(argv)
22
+ opt_parser = OptionParser.new do |opts|
23
+ opts.banner = 'Usage: appstack [options] [config_file]'
23
24
 
24
- opts.separator ''
25
- opts.separator 'Available options:'
25
+ opts.separator ''
26
+ opts.separator 'Available options:'
26
27
 
27
- opts.on('-f', '--force', 'force overwrite') do
28
- self.force = true
29
- end
28
+ opts.on('-f', '--force', 'force overwrite') do
29
+ self.force = true
30
+ end
30
31
 
31
- opts.on('-s', '--simulate', 'simulate only, do not copy') do
32
- self.simulate = true
33
- end
32
+ opts.on('-s', '--simulate', 'simulate only, do not copy') do
33
+ self.simulate = true
34
+ end
34
35
 
35
- opts.on('--verbose', 'show debug messages') do
36
- self.verbose = true
37
- end
36
+ opts.on('-r', '--recursive', 'recursively stackup all apps') do
37
+ self.recursive = true
38
+ end
38
39
 
39
- opts.on_tail('-h', '--help', 'show this message') do
40
- puts opts
41
- exit
42
- end
40
+ opts.on('--verbose', 'show debug messages') do
41
+ self.verbose = true
42
+ end
43
+
44
+ opts.on_tail('-h', '--help', 'show this message') do
45
+ puts opts
46
+ exit
47
+ end
43
48
 
44
- opts.on_tail('-v', '--version', 'show version number') do
45
- puts AppStack::VERSION
46
- exit
49
+ opts.on_tail('-v', '--version', 'show version number') do
50
+ puts AppStack::VERSION
51
+ exit
52
+ end
47
53
  end
48
- end
49
54
 
50
- opt_parser.parse!(argv)
55
+ opt_parser.parse!(argv)
56
+ end
51
57
  end
52
58
  end
@@ -0,0 +1,195 @@
1
+ # encoding: utf-8
2
+
3
+ module AppStack
4
+
5
+ # items as copy list candidate
6
+ class CompareFile
7
+ attr_accessor :from_app, :from_file, :to_app, :to_file,
8
+ :import, :render, :diff
9
+ def initialize(opts)
10
+ opts.each { |k, v| send("#{k}=".to_sym, v) }
11
+ @to_file ||= to_app.get_file_path(from_file_short_name)
12
+ end
13
+
14
+ def render?
15
+ render ? true : false
16
+ end
17
+
18
+ def import?
19
+ import ? true : false
20
+ end
21
+
22
+ def sync?
23
+ import ? false : true
24
+ end
25
+
26
+ def operation
27
+ return 'render' if render?
28
+ import? ? 'import' : 'sync'
29
+ end
30
+
31
+ def diff?
32
+ return true unless File.exists?(to_file)
33
+ c_file = from_file
34
+ if render?
35
+ c_file = Tempfile.new(File.basename(from_file)).path
36
+ render! c_file
37
+ end
38
+
39
+ @diff ||= Diffy::Diff.new(to_file, c_file, source: 'files')
40
+ @diff && @diff.to_s.chomp.size > 0 ? true : false
41
+ end
42
+
43
+ def local_new?
44
+ File.exists?(to_file) && File.mtime(to_file) > File.mtime(from_file)
45
+ end
46
+
47
+ def remote_new?
48
+ return true unless File.exists?(to_file)
49
+ File.mtime(from_file) > File.mtime(to_file)
50
+ end
51
+
52
+ def from_file_short_name
53
+ from_file.sub(/^#{from_app.directory}\//, '')
54
+ end
55
+
56
+ def to_file_short_name
57
+ to_file.sub(/^#{to_app.directory}\//, '')
58
+ end
59
+
60
+ def label
61
+ "#{operation} #{from_file_short_name.blue} from " +
62
+ "#{from_app.app_name.bold} as #{to_file_short_name.blue}"
63
+ end
64
+
65
+ def skip(msg)
66
+ "Skip #{operation} ".white + from_file_short_name.blue + ' from '.white +
67
+ from_app.app_name.bold + ': ' + msg
68
+ end
69
+
70
+ def process
71
+ render? ? render! : copy!
72
+ end
73
+
74
+ private
75
+
76
+ def render!(to = nil)
77
+ to ||= to_file
78
+ # return if @options.simulate
79
+ oh = File.open(to, 'wb')
80
+ if from_file.match(/\.liquid$/)
81
+ oh.write Liquid::Template
82
+ .parse(File.open(from_file, 'r:utf-8').read).render(to_app.attrs)
83
+ else
84
+ tilt = Tilt.new(from_file)
85
+ oh.write tilt.render(OpenStruct.new(to_app.attrs))
86
+ end
87
+ oh.close
88
+ end
89
+
90
+ def copy!
91
+ target_dir = File.dirname(to_file)
92
+ FileUtils.mkdir_p target_dir unless File.directory?(target_dir)
93
+ FileUtils.cp_r from_file, to_file
94
+ end
95
+
96
+ end
97
+
98
+ # reopen app class, extend with load_compare list function
99
+ class App
100
+ def load_compare_list!
101
+ # @import_list.merge(@sync_list).merge(@render_list
102
+ load_copy_list!(@import_list, import: true)
103
+ load_copy_list!(@sync_list)
104
+ load_render_list!(@render_list, render: true)
105
+ load_local_render_list!(render: true)
106
+ end
107
+
108
+ def load_copy_list!(list, opts = {})
109
+ list.each do |app, exp|
110
+ exp.each do |p| # string as group name or hash as file mapping
111
+ opts.merge! from_app: @app_stacks[app.to_s], to_app: self
112
+ p.is_a?(String) ? load_group!(p, opts) : load_file_map!(p, opts)
113
+ end
114
+ end
115
+ end
116
+
117
+ # rubocop:disable MethodLength
118
+ def load_render_list!(list, opts = {})
119
+ list.each do |appname, hsh|
120
+ app = @app_stacks[appname.to_s]
121
+ hsh.each do |fr, to|
122
+ to = fr.sub(/\.(erb|liquid|slim)$/, '') unless to.size > 0
123
+ from_file = app.get_file_path(fr)
124
+ if File.exists?(from_file)
125
+ @compare_list << CompareFile.new(
126
+ opts.merge(from_app: app, to_app: self,
127
+ from_file: from_file,
128
+ to_file: get_file_path(to)))
129
+ else
130
+ warn "[WARN] #{fr.blue} not found in #{app.app_name.bold}, skip."
131
+ end
132
+ end
133
+ end
134
+ end
135
+
136
+ def load_local_render_list!(opts = {})
137
+ config.render_local.each do |fr, to|
138
+ from_file = get_file_path(fr)
139
+ if File.exists?(from_file)
140
+ @compare_list << CompareFile.new(
141
+ opts.merge(from_app: self, to_app: self,
142
+ from_file: from_file, to_file: get_file_path(to)))
143
+ else
144
+ warn "[WARN] #{fr.blue} not found in #{app_name.bold} (local), skip."
145
+ end
146
+ end
147
+ end
148
+
149
+ # rubocop:disable LineLength
150
+ def load_group!(group, opts)
151
+ app = opts[:from_app]
152
+ fail ParseError, "Error on #{@conf_file}, #{app.app_name.bold} did not export group #{group.bold}" unless app.export_groups[group.to_s]
153
+ app.get_group_files(group, opts[:from_app]).each do |full_path|
154
+ @compare_list << CompareFile.new(opts.merge(from_file: full_path))
155
+ end
156
+ end
157
+
158
+ def load_file_map!(hsh, opts)
159
+ hsh.each do |f, t|
160
+ t = f unless t && t.size > 0
161
+ from_file = opts[:from_app].get_file_path(f)
162
+ if File.exists?(from_file)
163
+ @compare_list << CompareFile.new(
164
+ opts.merge(from_file: opts[:from_app].get_file_path(f),
165
+ to_file: opts[:to_app].get_file_path(t)))
166
+ else
167
+ warn "[WARN] #{f.blue} not found in #{opts[:from_app].app_name.bold}, skip."
168
+ end
169
+ end
170
+ end
171
+
172
+ def get_group_files(group, app)
173
+ file_list = []
174
+ export_groups[group.to_s].each do |fp|
175
+ if fp.match(/[\?\*\+\{\}]+/)
176
+ Dir[get_file_path(fp)].each { |f| file_list << f }
177
+ else
178
+ file = get_file_path(fp)
179
+
180
+ if File.exists?(file)
181
+ file_list << file
182
+ else
183
+ warn "[WARN] #{fp.blue} not found for #{app.app_name.bold}, group #{group.bold}, skip."
184
+ end
185
+ end
186
+ end
187
+ file_list
188
+ end
189
+ # rubocop:enable LineLength, MethodLength
190
+
191
+ def get_file_path(fp)
192
+ File.expand_path(fp, directory)
193
+ end
194
+ end
195
+ end
@@ -0,0 +1,119 @@
1
+ # encoding: utf-8
2
+
3
+ module AppStack
4
+ class ParseError < ArgumentError; end
5
+
6
+ # configuration from yaml file
7
+ class Configuration
8
+ attr_accessor :import, :sync, :export, :exclude, :render, :render_local,
9
+ :attrs, :stack_dir
10
+
11
+ # rubocop:disable LineLength
12
+ def initialize(filename = nil)
13
+ # use yaml file to set configuration
14
+ config = default_config.dup
15
+ YAML.load(File.open(filename, 'r:utf-8').read).each do |k, v|
16
+ fail ParseError, "unkown option `#{k}` in #{filename}" unless default_config[k.to_sym]
17
+ fail ParseError, "'#{k}' must be a #{default_config[k.to_sym].class.to_s}" unless v.is_a?(default_config[k.to_sym].class)
18
+ config[k.to_sym] = v
19
+ end if filename
20
+
21
+ config.each { |k, v| send("#{k}=", v) }
22
+ end
23
+ # rubocop:enable LineLength
24
+
25
+ def default_config
26
+ {
27
+ import: [],
28
+ sync: [],
29
+ render: [],
30
+ render_local: {},
31
+ export: [],
32
+ exclude: [],
33
+ attrs: {},
34
+ stack_dir: '..'
35
+ }
36
+ end
37
+ end
38
+
39
+ # reopen App, mixin
40
+ module ConfigParser
41
+ def load_config
42
+ info 'load configuration file from:', @conf_file
43
+ Configuration.new(@conf_file)
44
+ end
45
+
46
+ def validate_config!
47
+ parse_export_groups!
48
+ @sync_list = parse_stack_list!('sync')
49
+ info 'load sync list as', @sync_list
50
+ @import_list = parse_stack_list!('import')
51
+ info 'load import list as', @import_list
52
+ @render_list = parse_render_list!
53
+
54
+ # rubocop:disable LineLength
55
+ config.exclude.each { |f| fail "Error on #{@conf_file} for exclude settings, #{f.inspect} is not a String" unless f.is_a?(String) }
56
+ end
57
+
58
+ # rubocop:disable LineLength, MethodLength
59
+ def parse_export_groups!
60
+ @export_groups = { 'default' => [] }
61
+ config.export.each do |exp|
62
+ case
63
+ when exp.is_a?(Hash) then @export_groups.merge!(exp)
64
+ when exp.is_a?(String) then @export_groups['default'] << exp
65
+ else fail ParseError, "Error on #{@conf_file}, wrong type for export: '#{exp.inspect}'"
66
+ end
67
+ end
68
+
69
+ # validate export file list format
70
+ @export_groups.each do |gname, list|
71
+ fail ParseError, "Error on #{@conf_file}, group name must be an String, not #{gname}" unless gname.is_a?(String)
72
+ fail ParseError, "Error on #{@conf_file} export group #{gname}, export files should be defined as a Array of String, #{list} is not an array." unless list.is_a?(Array)
73
+ list.each { |f| fail ParseError, "Error on #{@conf_file} export group #{gname}, export files should be defined as a Array of string, not #{f}" unless f.is_a?(String) }
74
+ end
75
+ @export_groups
76
+ end
77
+
78
+ private
79
+
80
+ def parse_stack_list!(list_type)
81
+ hlist = {}
82
+ config.send(list_type.to_sym).each do |exp|
83
+ case
84
+ when exp.is_a?(String) then # exp is the app name with default group
85
+ hlist[exp] = ['default']
86
+ when exp.is_a?(Hash)
87
+ exp.each do |app, list|
88
+ fail ParseError, "Error on #{@conf_file}, #{list_type} group, #{app} is not a String" unless app.is_a?(String)
89
+ fail ParseError, "Error on #{@conf_file}, #{list_type} group, #{list} is not a Array" unless list.is_a?(Array)
90
+ list.each do |g|
91
+ fail ParseError, "Error on #{@conf_file}, #{app} in #{list_type}, #{g} should be an String (file group) or a Hash (file mapping)" unless g.is_a?(Hash) || g.is_a?(String)
92
+ end
93
+ end
94
+ hlist.merge!(exp)
95
+ else fail ParseError, "Error on #{@conf_file}, #{list_type} group, should be an array of String or Hash." unless list.is_a?(Array)
96
+ end
97
+ end
98
+ hlist
99
+ end
100
+
101
+ def parse_render_list!
102
+ hlist = {}
103
+ config.render.each do |exp|
104
+ fail ParseError, "Error on #{@conf_file}, render group, #{exp.inspect} is not a Hash" unless exp.is_a?(Hash)
105
+ exp.each do |app, list|
106
+ fail ParseError, "Error on #{@conf_file}, #{list_type} group, #{app} is not a String" unless app.is_a?(String)
107
+ fail ParseError, "Error on #{@conf_file}, render for #{app}, #{list.inspect} is not a Hash" unless list.is_a?(Hash)
108
+ list.each do |fr, to|
109
+ fail ParseError, "Error on #{@conf_file}, render for #{app}, #{fr.inspect} is not a String" unless fr.is_a?(String)
110
+ fail ParseError, "Error on #{@conf_file}, render for #{app}, #{to.inspect} is not a String" unless to.is_a?(String)
111
+ end
112
+ end
113
+ hlist.merge! exp
114
+ end
115
+ hlist
116
+ end
117
+ # rubocop:enable LineLength
118
+ end
119
+ end
@@ -0,0 +1,60 @@
1
+ # encoding: utf-8
2
+
3
+ module AppStack
4
+ # mixin app class for build copy list
5
+ # rubocop:disable MethodLength
6
+ class App
7
+ def build_copy_list!
8
+ @copy_list, @diff_list = [], []
9
+ compare_list.each do |f|
10
+ info f.label
11
+ if File.exists?(f.to_file)
12
+ if f.import?
13
+ info f.skip('target exists')
14
+ next
15
+ end
16
+
17
+ if f.diff?
18
+ info f.label, 'add to diff list'
19
+ @diff_list << f
20
+ else
21
+ info f.skip('not changed')
22
+ next
23
+ end
24
+ end
25
+ info f.label, 'add to copy list'
26
+ @copy_list << f
27
+ end
28
+ end
29
+
30
+ def merge!
31
+ case
32
+ when AppStack.options.force then do_copy!
33
+ when AppStack.options.simulate then do_simulate!
34
+ else diff_list.size > 0 ? do_warn : do_copy!
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def do_copy!
41
+ @copy_list.map(&:process)
42
+ end
43
+
44
+ def do_simulate!
45
+ @copy_list.each { |f| puts f.label }
46
+ end
47
+
48
+ def do_warn
49
+ puts 'These files are changed remotely:'
50
+ i = 1
51
+ @diff_list.each do |f|
52
+ puts "[#{i.to_s.bold}] " + f.to_file_short_name.blue
53
+ puts "from: #{f.from_file_short_name.blue}"
54
+ puts f.diff.to_s(:color)
55
+ i += 1
56
+ end
57
+ puts 'use -f (--force) option to overwrite all files.'
58
+ end
59
+ end
60
+ end