MuranoCLI 3.0.0 → 3.0.1

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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +50 -27
  3. data/.trustme.vim +12 -8
  4. data/bin/murano +23 -8
  5. data/docs/basic_example.rst +1 -1
  6. data/docs/completions/murano_completion-bash +88 -0
  7. data/lib/MrMurano/Account.rb +3 -3
  8. data/lib/MrMurano/Business.rb +6 -6
  9. data/lib/MrMurano/Config-Migrate.rb +1 -3
  10. data/lib/MrMurano/Config.rb +16 -8
  11. data/lib/MrMurano/Content.rb +56 -45
  12. data/lib/MrMurano/Gateway.rb +62 -21
  13. data/lib/MrMurano/Mock.rb +27 -19
  14. data/lib/MrMurano/Passwords.rb +7 -7
  15. data/lib/MrMurano/ReCommander.rb +171 -28
  16. data/lib/MrMurano/Setting.rb +38 -40
  17. data/lib/MrMurano/Solution-ServiceConfig.rb +2 -1
  18. data/lib/MrMurano/Solution-Services.rb +196 -61
  19. data/lib/MrMurano/Solution-Users.rb +7 -7
  20. data/lib/MrMurano/Solution.rb +22 -8
  21. data/lib/MrMurano/SolutionId.rb +10 -4
  22. data/lib/MrMurano/SubCmdGroupContext.rb +14 -5
  23. data/lib/MrMurano/SyncAllowed.rb +42 -0
  24. data/lib/MrMurano/SyncUpDown.rb +122 -65
  25. data/lib/MrMurano/Webservice-Cors.rb +9 -3
  26. data/lib/MrMurano/Webservice-Endpoint.rb +39 -33
  27. data/lib/MrMurano/Webservice-File.rb +35 -24
  28. data/lib/MrMurano/commands/business.rb +18 -18
  29. data/lib/MrMurano/commands/content.rb +3 -2
  30. data/lib/MrMurano/commands/devices.rb +137 -22
  31. data/lib/MrMurano/commands/globals.rb +8 -2
  32. data/lib/MrMurano/commands/keystore.rb +3 -2
  33. data/lib/MrMurano/commands/link.rb +13 -13
  34. data/lib/MrMurano/commands/login.rb +3 -2
  35. data/lib/MrMurano/commands/mock.rb +4 -3
  36. data/lib/MrMurano/commands/password.rb +4 -2
  37. data/lib/MrMurano/commands/postgresql.rb +5 -3
  38. data/lib/MrMurano/commands/settings.rb +78 -62
  39. data/lib/MrMurano/commands/show.rb +79 -74
  40. data/lib/MrMurano/commands/solution.rb +6 -4
  41. data/lib/MrMurano/commands/solution_picker.rb +5 -4
  42. data/lib/MrMurano/commands/status.rb +15 -4
  43. data/lib/MrMurano/commands/timeseries.rb +3 -2
  44. data/lib/MrMurano/commands/tsdb.rb +3 -2
  45. data/lib/MrMurano/hash.rb +6 -6
  46. data/lib/MrMurano/http.rb +66 -67
  47. data/lib/MrMurano/makePretty.rb +18 -12
  48. data/lib/MrMurano/progress.rb +9 -2
  49. data/lib/MrMurano/verbosing.rb +14 -2
  50. data/lib/MrMurano/version.rb +2 -2
  51. data/spec/GatewayDevice_spec.rb +190 -149
  52. data/spec/Mock_spec.rb +3 -3
  53. data/spec/Solution-ServiceEventHandler_spec.rb +170 -137
  54. data/spec/SyncUpDown_spec.rb +205 -191
  55. metadata +3 -2
@@ -1,4 +1,4 @@
1
- # Last Modified: 2017.08.17 /coding: utf-8
1
+ # Last Modified: 2017.08.22 /coding: utf-8
2
2
  # frozen_string_literal: true
3
3
 
4
4
  # Copyright © 2016-2017 Exosite LLC.
@@ -34,11 +34,13 @@ module MrMurano
34
34
  end
35
35
 
36
36
  def remove(id)
37
+ return unless remove_item_allowed(id)
37
38
  delete('/' + id.to_s)
38
39
  end
39
40
 
40
41
  # @param modify Bool: True if item exists already and this is changing it
41
42
  def upload(_local, remote, _modify)
43
+ return unless upload_item_allowed(remote[@itemkey])
42
44
  # Roles cannot be modified, so must delete and post.
43
45
  delete('/' + remote[@itemkey]) do |request, http|
44
46
  response = http.request(request)
@@ -59,9 +61,6 @@ module MrMurano
59
61
  # for now, we'll read, modify, write
60
62
  here = []
61
63
  if local.exist?
62
- # FIXME/2017-07-18: Security/YAMLLoad: Prefer using YAML.safe_load over YAML.load.
63
- # Disabling [rubo]cop for now.
64
- # rubocop:disable Security/YAMLLoad
65
64
  local.open('rb') { |io| here = YAML.load(io) }
66
65
  here = [] if here == false
67
66
  end
@@ -69,6 +68,7 @@ module MrMurano
69
68
  Hash.transform_keys_to_symbols(i)[@itemkey] == item[@itemkey]
70
69
  end
71
70
  here << item.reject { |k, _v| %i[synckey synctype].include? k }
71
+ return unless download_item_allowed(item[@itemkey])
72
72
  local.open('wb') do |io|
73
73
  io.write here.map { |i| Hash.transform_keys_to_strings(i) }.to_yaml
74
74
  end
@@ -79,7 +79,6 @@ module MrMurano
79
79
  # for now, we'll read, modify, write
80
80
  here = []
81
81
  if local.exist?
82
- # FIXME/2017-07-18: Security/YAMLLoad: Prefer using YAML.safe_load over YAML.load.
83
82
  local.open('rb') { |io| here = YAML.load(io) }
84
83
  here = [] if here == false
85
84
  end
@@ -87,6 +86,7 @@ module MrMurano
87
86
  here.delete_if do |it|
88
87
  Hash.transform_keys_to_symbols(it)[key] == item[key]
89
88
  end
89
+ return unless removelocal_item_allowed(item[key])
90
90
  local.open('wb') do |io|
91
91
  io.write here.map { |i| Hash.transform_keys_to_strings(i) }.to_yaml
92
92
  end
@@ -109,7 +109,6 @@ module MrMurano
109
109
 
110
110
  # MAYBE/2017-07-03: Do we care if there are duplicate keys in the yaml? See dup_count.
111
111
  here = []
112
- # FIXME/2017-07-18: Security/YAMLLoad: Prefer using YAML.safe_load over YAML.load.
113
112
  from.open { |io| here = YAML.load(io) }
114
113
  here = [] if here == false
115
114
 
@@ -152,10 +151,11 @@ module MrMurano
152
151
  end
153
152
 
154
153
  # @param modify Bool: True if item exists already and this is changing it
155
- def upload(_local, _remote, _modify)
154
+ def upload(local, _remote, _modify)
156
155
  # TODO: figure out APIs for updating users.
157
156
  warning %(Updating Users is not yet implemented.)
158
157
  # post does work if the :password field is set.
158
+ return unless upload_item_allowed(local)
159
159
  end
160
160
 
161
161
  def synckey(item)
@@ -1,4 +1,4 @@
1
- # Last Modified: 2017.08.17 /coding: utf-8
1
+ # Last Modified: 2017.08.23 /coding: utf-8
2
2
  # frozen_string_literal: true
3
3
 
4
4
  # Copyright © 2016-2017 Exosite LLC.
@@ -96,7 +96,7 @@ module MrMurano
96
96
  #query.push ['limit', 20]
97
97
  query.push ['offset', total - remaining]
98
98
  elsif remaining != 0
99
- warning "Unexpected: negative remaining: ‘#{total}"
99
+ warning "Unexpected: negative remaining: #{fancy_ticks(total)}"
100
100
  remaining = 0
101
101
  end
102
102
  if aggregate.nil?
@@ -127,6 +127,8 @@ module MrMurano
127
127
  # the results.
128
128
  #path = path || '?select=id,service'
129
129
  matches = list(path)
130
+ # 2017-08-21: The only caller so far is the link command,
131
+ # which passes the Solution ID as svc_name.
130
132
  matches.select { |match| match[:service] == svc_name }
131
133
  end
132
134
 
@@ -240,23 +242,35 @@ module MrMurano
240
242
  end
241
243
  end
242
244
  unless @sid.to_s.empty? || sid.to_s == @sid.to_s
243
- warning "#{type_name} ID mismatch. Server says ‘#{sid}’, but config says ‘#{@sid}’."
245
+ warning(
246
+ "#{type_name} ID mismatch. Server says #{fancy_ticks(sid)}, " \
247
+ "but config says #{fancy_ticks(@sid)}."
248
+ )
244
249
  end
245
250
  self.sid = sid
246
251
  # Verify/set the name.
247
252
  unless @name.to_s.empty? || @meta[:name].to_s == @name.to_s
248
- warning "Name mismatch. Server says ‘#{@meta[:name]}’, but config says ‘#{@name}’."
253
+ warning(
254
+ "Name mismatch. Server says #{fancy_ticks(@meta[:name])}, " \
255
+ "but config says #{fancy_ticks(@name)}."
256
+ )
249
257
  end
250
258
  if !@meta[:name].to_s.empty?
251
259
  set_name(@meta[:name])
252
260
  unless @valid_name || type == :solution
253
- warning "Unexpected: Server returned invalid name: ‘#{@meta[:name]}’"
261
+ warning(
262
+ "Unexpected: Server returned invalid name: #{fancy_ticks(@meta[:name])}"
263
+ )
254
264
  end
255
265
  elsif @meta[:domain]
256
266
  # This could be a pre-ADC/pre-Murano business.
257
- warning "Unexpected: Server returned no name for domain: ‘#{@meta[:domain]}’"
267
+ warning(
268
+ "Unexpected: Server returned no name for domain: #{fancy_ticks(@meta[:domain])}"
269
+ )
258
270
  else
259
- warning "Unexpected: Server returned no name for solution: ‘#{@meta}’"
271
+ warning(
272
+ "Unexpected: Server returned no name for solution: #{fancy_ticks(@meta)}"
273
+ )
260
274
  end
261
275
  end
262
276
 
@@ -322,7 +336,7 @@ module MrMurano
322
336
  if @name.to_s.empty?
323
337
  ''
324
338
  else
325
- "‘#{@name}’"
339
+ fancy_ticks(@name)
326
340
  end
327
341
  end
328
342
 
@@ -1,14 +1,18 @@
1
- # Last Modified: 2017.08.17 /coding: utf-8
1
+ # Last Modified: 2017.08.23 /coding: utf-8
2
2
  # frozen_string_literal: true
3
3
 
4
4
  # Copyright © 2016-2017 Exosite LLC.
5
5
  # License: MIT. See LICENSE.txt.
6
6
  # vim:tw=0:ts=2:sw=2:et:ai
7
7
 
8
+ require 'MrMurano/verbosing'
9
+
8
10
  module MrMurano
9
11
  module SolutionId
10
12
  INVALID_SID = '-1'
11
- UNEXPECTED_TYPE_OR_ERROR_MSG = 'Unexpected result type or error: assuming empty instead'
13
+ UNEXPECTED_TYPE_OR_ERROR_MSG = (
14
+ 'Unexpected result type or error: assuming empty instead'
15
+ )
12
16
 
13
17
  attr_reader :sid
14
18
  attr_reader :valid_sid
@@ -41,7 +45,9 @@ module MrMurano
41
45
 
42
46
  def sid=(sid)
43
47
  sid = INVALID_SID if sid.nil? || !sid.is_a?(String) || sid.empty?
44
- @valid_sid = false if sid.to_s.empty? || sid == INVALID_SID || (defined?(@sid) && sid != @sid)
48
+ if sid.to_s.empty? || sid == INVALID_SID || (defined?(@sid) && sid != @sid)
49
+ @valid_sid = false
50
+ end
45
51
  @sid = sid
46
52
  # MAGIC_NUMBER: The 2nd element is the solution ID, e.g., solution/<sid>/...
47
53
  raise "Unexpected @uriparts_sidex #{@uriparts_sidex}" unless @uriparts_sidex == 1
@@ -65,7 +71,7 @@ module MrMurano
65
71
  def endpoint(_path='')
66
72
  # This is hopefully just a DEV error, and not something user will ever see!
67
73
  return unless @uriparts[@uriparts_sidex] == INVALID_SID
68
- error("Solution ID missing! Invalid ‘#{@solntype}")
74
+ error("Solution ID missing! Invalid #{MrMurano::Verbose.fancy_ticks(@solntype)}")
69
75
  exit 2
70
76
  end
71
77
  end
@@ -1,16 +1,23 @@
1
+ # Last Modified: 2017.08.21 /coding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ # Copyright © 2016-2017 Exosite LLC.
5
+ # License: MIT. See LICENSE.txt.
6
+ # vim:tw=0:ts=2:sw=2:et:ai
1
7
 
2
8
  module MrMurano
3
9
  class SubCmdGroupHelp
4
- attr :name, :description
10
+ attr_reader :description
11
+ attr_reader :name
5
12
 
6
13
  def initialize(command)
7
14
  @name = command.syntax.to_s
8
15
  @description = command.description.to_s
9
16
  @runner = ::Commander::Runner.instance
10
17
  prefix = /^#{command.name.to_s} /
11
- cmds = @runner.instance_variable_get(:@commands).select{|n,_| n.to_s =~ prefix}
18
+ cmds = @runner.instance_variable_get(:@commands).select { |n, _| n.to_s =~ prefix }
12
19
  @commands = cmds
13
- als = @runner.instance_variable_get(:@aliases).select{|n,_| n.to_s =~ prefix}
20
+ als = @runner.instance_variable_get(:@aliases).select { |n, _| n.to_s =~ prefix }
14
21
  @aliases = als
15
22
 
16
23
  @options = {}
@@ -24,6 +31,8 @@ module MrMurano
24
31
  @description
25
32
  when :help
26
33
  nil
34
+ # rubocop:disable Style/EmptyElse: "Redundant else-clause."
35
+ # Rubocop seems incorrect about this one.
27
36
  else
28
37
  nil
29
38
  end
@@ -37,6 +46,8 @@ module MrMurano
37
46
  @commands[name.to_s]
38
47
  end
39
48
 
49
+ # rubocop:disable Style/AccessorMethodName
50
+ # "Do not prefix reader method names with get_."
40
51
  def get_help
41
52
  hf = @runner.program(:help_formatter).new(self)
42
53
  pc = Commander::HelpFormatter::ProgramContext.new(self).get_binding
@@ -45,5 +56,3 @@ module MrMurano
45
56
  end
46
57
  end
47
58
 
48
-
49
- # vim: set ai et sw=2 ts=2 :
@@ -0,0 +1,42 @@
1
+ # Last Modified: 2017.08.23 /coding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ # Copyright © 2016-2017 Exosite LLC.
5
+ # License: MIT. See LICENSE.txt.
6
+ # vim:tw=0:ts=2:sw=2:et:ai
7
+
8
+ #require 'MrMurano/progress'
9
+ #require 'MrMurano/verbosing'
10
+ #require 'MrMurano/hash'
11
+
12
+ module MrMurano
13
+ module SyncAllowed
14
+ def sync_item_allowed(actioning, item_name)
15
+ if $cfg['tool.dry']
16
+ MrMurano::Verbose.whirly_interject do
17
+ say("--dry: Not #{actioning} item: #{fancy_ticks(item_name)}")
18
+ end
19
+ false
20
+ else
21
+ true
22
+ end
23
+ end
24
+
25
+ def remove_item_allowed(id)
26
+ sync_item_allowed('removing', id)
27
+ end
28
+
29
+ def upload_item_allowed(id)
30
+ sync_item_allowed('uploading', id)
31
+ end
32
+
33
+ def download_item_allowed(id)
34
+ sync_item_allowed('downloading', id)
35
+ end
36
+
37
+ def removelocal_item_allowed(id)
38
+ sync_item_allowed('removing-local', id)
39
+ end
40
+ end
41
+ end
42
+
@@ -1,4 +1,4 @@
1
- # Last Modified: 2017.08.18 /coding: utf-8
1
+ # Last Modified: 2017.08.23 /coding: utf-8
2
2
  # frozen_string_literal: true
3
3
 
4
4
  # Copyright © 2016-2017 Exosite LLC.
@@ -18,6 +18,7 @@ require 'MrMurano/verbosing'
18
18
  require 'MrMurano/hash'
19
19
  #require 'MrMurano/Config'
20
20
  #require 'MrMurano/ProjectFile'
21
+ require 'MrMurano/SyncAllowed'
21
22
  ##require 'MrMurano/SyncRoot'
22
23
 
23
24
  module MrMurano
@@ -27,6 +28,8 @@ module MrMurano
27
28
  # pulling those things.
28
29
  #
29
30
  module SyncUpDown
31
+ include SyncAllowed
32
+
30
33
  # This is one item that can be synced.
31
34
  class Item
32
35
  # @return [String] The name of this item.
@@ -49,8 +52,6 @@ module MrMurano
49
52
  attr_accessor :synckey
50
53
  # @return [String] The syncable type.
51
54
  attr_accessor :synctype
52
- # @return [String] For device2, the event type.
53
- attr_accessor :type
54
55
  # @return [String] The updated_at time from the server is used to detect changes.
55
56
  attr_accessor :updated_at
56
57
  # @return [Integer] Positive if multiple conflicting files found for same item.
@@ -295,11 +296,10 @@ module MrMurano
295
296
  # @param local [Pathname] Full path of where to download to
296
297
  # @param item [Item] The item to download
297
298
  def download(local, item)
298
- # if item[:bundled]
299
- # warning "Not downloading into bundled item #{synckey(item)}"
300
- # return
301
- # end
302
- local.dirname.mkpath
299
+ #if item[:bundled]
300
+ # warning "Not downloading into bundled item #{synckey(item)}"
301
+ # return
302
+ #end
303
303
  id = item[@itemkey.to_sym]
304
304
  if id.nil?
305
305
  debug "!!! Missing '#{@itemkey}', using :id instead!"
@@ -307,9 +307,14 @@ module MrMurano
307
307
  id = item[:id]
308
308
  raise "Both #{@itemkey} and id in item are nil!" if id.nil?
309
309
  end
310
+
311
+ relpath = local.relative_path_from(Pathname.pwd).to_s
312
+ return unless download_item_allowed(relpath)
313
+
314
+ local.dirname.mkpath
310
315
  local.open('wb') do |io|
311
316
  fetch(id) do |chunk|
312
- io.write chunk
317
+ io.write config_vars_encode chunk
313
318
  end
314
319
  end
315
320
  update_mtime(local, item)
@@ -390,6 +395,7 @@ module MrMurano
390
395
  # @param dest [Pathname] Full path of item to be removed
391
396
  # @param item [Item] Full details of item to be removed
392
397
  def removelocal(dest, _item)
398
+ return unless removelocal_item_allowed(dest)
393
399
  dest.unlink if dest.exist?
394
400
  end
395
401
 
@@ -408,7 +414,9 @@ module MrMurano
408
414
  end
409
415
 
410
416
  def diff_item_write(io, merged, _local, _remote)
411
- io << merged[:local_path].read
417
+ contents = merged[:local_path].read
418
+ contents = config_vars_decode(contents)
419
+ io << contents
412
420
  end
413
421
 
414
422
  #
@@ -458,33 +466,36 @@ module MrMurano
458
466
  # Get a list of SyncUpDown::Item's, or a class derived thereof.
459
467
  bitems = localitems(location)
460
468
  # Use synckey for quicker merging.
461
- # 2017-07-02: Argh. If two files have the same identity, this
462
- # simple loop masks that there are two files with the same identity!
463
469
  #bitems.each { |b| items[synckey(b)] = b }
464
- warns = {}
470
+ # 2017-07-02: If two files have the same identity, the simple loop
471
+ # masks that there are two files with the same identity. So check
472
+ # first for duplicates, and then process each item.
473
+ seen = {}
465
474
  bitems.each do |item|
466
475
  skey = synckey(item)
467
- if items.key? skey
468
- warns[skey] = 0 unless warns.key?(skey)
469
- if warns[skey].zero?
470
- items[skey][:dup_count] = warns[skey]
471
- # The dumb_synckey is just so we don't overwrite the
472
- # original item, or other duplicates, in the hash.
473
- dumb_synckey = "#{skey}-#{warns[skey]}"
474
- # This just sets the alias for the output, so duplicates look unique.
475
- item[@itemkey.to_sym] = dumb_synckey
476
- # Don't delete the original item, so that other dupes see it.
477
- #items.delete(skey)
478
- msg = "Duplicate local file(s) found for ‘#{skey}’"
479
- msg += " for ‘#{self.class.description}’" if self.class.description.to_s != ''
480
- #msg += '!'
481
- warning(msg)
476
+ seen[skey] = seen.key?(skey) && seen[skey] + 1 || 1
477
+ end
478
+ counts = {}
479
+ bitems.each do |item|
480
+ skey = synckey(item)
481
+ if seen[skey] > 1
482
+ if items[skey].nil?
483
+ items[skey] = MrMurano::EventHandler::EventHandlerItem.new(item)
484
+ items[skey][:dup_count] = 0
482
485
  end
483
- warns[skey] += 1
484
- item[:dup_count] = warns[skey]
485
- dumb_synckey = "#{skey}-#{warns[skey]}"
486
- item[@itemkey.to_sym] = dumb_synckey
487
- items[dumb_synckey] = item
486
+ counts[skey] = counts.key?(skey) && counts[skey] + 1 || 1
487
+ # Use a unique synckey so all duplicates make it in the list.
488
+ uniq_synckey = "#{skey}-#{counts[skey]}"
489
+ item[:dup_count] = counts[skey]
490
+ # This sets the alias for the output, so duplicates look unique.
491
+ item[@itemkey.to_sym] = uniq_synckey
492
+ items[uniq_synckey] = item
493
+ msg = "Duplicate definition found for #{fancy_ticks(skey)}"
494
+ if self.class.description.to_s != ''
495
+ msg += " for #{fancy_ticks(self.class.description)}"
496
+ end
497
+ warning(msg)
498
+ warning(" #{item.local_path}")
488
499
  else
489
500
  items[skey] = item
490
501
  end
@@ -495,9 +506,10 @@ module MrMurano
495
506
  # MEH/2017-07-31: This message is a little misleading on syncdown,
496
507
  # e.g., in rspec ./spec/cmd_syncdown_spec.rb, one test blows away
497
508
  # local directories and does a syncdown, and on stderr you'll see
498
- # Skipping missing location ‘/tmp/d20170731-3150-1f50uj4/project/specs/resources.yaml’ (Resources)
509
+ # Skipping missing location
510
+ # ‘/tmp/d20170731-3150-1f50uj4/project/specs/resources.yaml’ (Resources)
499
511
  # but then later in the syncdown, that directory and file gets created.
500
- msg = "Skipping missing location ‘#{location}"
512
+ msg = "Skipping missing location #{fancy_ticks(location)}"
501
513
  unless self.class.description.to_s.empty?
502
514
  msg += " (#{Inflecto.pluralize(self.class.description)})"
503
515
  end
@@ -605,7 +617,21 @@ module MrMurano
605
617
  pattern = pattern.gsub(%r{^\*\*\/}, '')
606
618
  end
607
619
 
608
- ::File.fnmatch(pattern, path)
620
+ ignore = ::File.fnmatch(pattern, path)
621
+ debug "Excluded #{path}" if ignore
622
+ ignore
623
+ end
624
+
625
+ def resolve_config_var_usage!(there, local)
626
+ # pass; derived classes should implement.
627
+ end
628
+
629
+ def config_vars_decode(script)
630
+ script
631
+ end
632
+
633
+ def config_vars_encode(script)
634
+ script
609
635
  end
610
636
 
611
637
  #######################################################################
@@ -669,16 +695,11 @@ module MrMurano
669
695
 
670
696
  def syncup_item(item, options, action, verbage)
671
697
  if options[action]
672
- if !$cfg['tool.dry']
673
- prog_msg = "#{verbage.capitalize} item #{item[:synckey]}"
674
- prog_msg += " (#{item[:synctype]})" if $cfg['tool.verbose']
675
- sync_update_progress(prog_msg)
676
- yield item
677
- else
678
- MrMurano::Verbose.whirly_interject do
679
- say("--dry: Not #{verbage.downcase} item #{item[:synckey]}")
680
- end
681
- end
698
+ # It's up to the callback to check and honor $cfg['tool.dry'].
699
+ prog_msg = "#{verbage.capitalize} item #{item[:synckey]}"
700
+ prog_msg += " (#{item[:synctype]})" if $cfg['tool.verbose']
701
+ sync_update_progress(prog_msg)
702
+ yield item
682
703
  elsif $cfg['tool.verbose']
683
704
  MrMurano::Verbose.whirly_interject do
684
705
  say("--no-#{action}: Not #{verbage.downcase} item #{item[:synckey]}")
@@ -734,15 +755,11 @@ module MrMurano
734
755
 
735
756
  def syncdown_item(item, into, options, action, verbage)
736
757
  if options[action]
737
- if !$cfg['tool.dry']
738
- prog_msg = "#{verbage.capitalize} item #{item[:synckey]}"
739
- prog_msg += " (#{item[:synctype]})" if $cfg['tool.verbose']
740
- sync_update_progress(prog_msg)
741
- dest = tolocalpath(into, item)
742
- yield dest, item
743
- else
744
- say("--dry: Not #{verbage.downcase} item #{item[:synckey]}")
745
- end
758
+ prog_msg = "#{verbage.capitalize} item #{item[:synckey]}"
759
+ prog_msg += " (#{item[:synctype]})" if $cfg['tool.verbose']
760
+ sync_update_progress(prog_msg)
761
+ dest = tolocalpath(into, item)
762
+ yield dest, item
746
763
  elsif $cfg['tool.verbose']
747
764
  say("--no-#{action}: Not #{verbage.downcase} item #{item[:synckey]}")
748
765
  end
@@ -760,7 +777,7 @@ module MrMurano
760
777
  tlcl = Tempfile.new([tolocalname(merged, @itemkey) + '_local_', '.lua'])
761
778
  Pathname.new(tlcl.path).open('wb') do |io|
762
779
  if merged.key?(:script)
763
- io << merged[:script]
780
+ io << config_vars_decode(merged[:script])
764
781
  else
765
782
  # For most items, read the local file.
766
783
  # For resources, it's a bit trickier.
@@ -864,15 +881,25 @@ module MrMurano
864
881
 
865
882
  tomod, unchg = items_mods_and_chgs(options, therebox, localbox)
866
883
 
884
+ clash = items_cull_clashes!([toadd, todel, tomod, unchg])
885
+
867
886
  if options[:unselected]
868
- { toadd: toadd, todel: todel, tomod: tomod, unchg: unchg, skipd: [] }
887
+ {
888
+ toadd: toadd,
889
+ todel: todel,
890
+ tomod: tomod,
891
+ unchg: unchg,
892
+ skipd: [],
893
+ clash: clash,
894
+ }
869
895
  else
870
896
  {
871
- toadd: toadd.select { |i| i[:selected] }.map { |i| i.delete(:selected); i },
872
- todel: todel.select { |i| i[:selected] }.map { |i| i.delete(:selected); i },
873
- tomod: tomod.select { |i| i[:selected] }.map { |i| i.delete(:selected); i },
874
- unchg: unchg.select { |i| i[:selected] }.map { |i| i.delete(:selected); i },
897
+ toadd: select_selected(toadd),
898
+ todel: select_selected(todel),
899
+ tomod: select_selected(tomod),
900
+ unchg: select_selected(unchg),
875
901
  skipd: [],
902
+ clash: select_selected(clash),
876
903
  }
877
904
  end
878
905
  end
@@ -928,7 +955,7 @@ module MrMurano
928
955
  skip_sol = true if tested && !passed
929
956
  end
930
957
  return nil unless skip_sol
931
- ret = { toadd: [], todel: [], tomod: [], unchg: [], skipd: [] }
958
+ ret = { toadd: [], todel: [], tomod: [], unchg: [], skipd: [], clash: [] }
932
959
  ret[:skipd] << { synckey: self.class.description }
933
960
  ret
934
961
  end
@@ -945,8 +972,11 @@ module MrMurano
945
972
  def items_lists(options, selected)
946
973
  # Fetch arrays of items there, and items here/local.
947
974
  there = list
948
- there = _matcher(there, selected)
949
975
  local = locallist(skip_warn: options[:skip_missing_warning])
976
+
977
+ resolve_config_var_usage!(there, local)
978
+
979
+ there = _matcher(there, selected)
950
980
  local = _matcher(local, selected)
951
981
 
952
982
  therebox = {}
@@ -960,10 +990,12 @@ module MrMurano
960
990
  local.each do |item|
961
991
  skey = synckey(item)
962
992
  # 2017-07-02: Check for local duplicates.
963
- skey += "-#{item[:dup_count]}" unless item[:dup_count].nil?
993
+ unless item[:dup_count].nil? || item[:dup_count].zero?
994
+ skey += "-#{item[:dup_count]}"
995
+ end
964
996
  item[:synckey] = skey
965
997
  item[:synctype] = self.class.description
966
- localbox[item[:synckey]] = item
998
+ localbox[skey] = item
967
999
  end
968
1000
 
969
1001
  # Some items are considered "undeletable", meaning if a
@@ -990,6 +1022,8 @@ module MrMurano
990
1022
  unchg = []
991
1023
 
992
1024
  (localbox.keys & therebox.keys).each do |key|
1025
+ # Skip this item if it's got duplicate conflicts.
1026
+ next if !localbox[key].is_a?(Hash) && localbox[key].dup_count == 0
993
1027
  # Want 'local' to override 'there' except for itemkey.
994
1028
  if options[:asdown]
995
1029
  mrg = therebox[key].reject { |k, _v| k == @itemkey.to_sym }
@@ -1021,6 +1055,29 @@ module MrMurano
1021
1055
  list.sort_by(&:name)
1022
1056
  end
1023
1057
  end
1058
+
1059
+ def select_selected(items)
1060
+ items.select { |i| i[:selected] }.map { |i| i.delete(:selected); i }
1061
+ end
1062
+
1063
+ def items_cull_clashes!(items_list)
1064
+ items_list = [items_list] unless items_list.is_a?(Array)
1065
+ clash = []
1066
+ items_list.each do |items|
1067
+ items.select! do |item|
1068
+ if item[:dup_count].nil?
1069
+ true
1070
+ elsif item[:dup_count].zero?
1071
+ # This is the control item.
1072
+ false
1073
+ else
1074
+ clash.push(item)
1075
+ false
1076
+ end
1077
+ end
1078
+ end
1079
+ clash
1080
+ end
1024
1081
  end
1025
1082
  end
1026
1083