app_stack 1.3.4 → 1.4.0

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