MuranoCLI 3.0.0 → 3.0.1

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