MuranoCLI 3.2.0.beta.9 → 3.2.1.pre.beta.3

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 (80) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +3 -0
  3. data/Rakefile +5 -0
  4. data/dockers/README.rst +7 -0
  5. data/dockers/RELEASE.rst +6 -3
  6. data/dockers/docker-test.sh +45 -17
  7. data/docs/completions/murano_completion-bash +211 -86
  8. data/lib/MrMurano/Account.rb +72 -4
  9. data/lib/MrMurano/Business.rb +163 -2
  10. data/lib/MrMurano/Commander-Entry.rb +1 -2
  11. data/lib/MrMurano/Config.rb +19 -18
  12. data/lib/MrMurano/Content.rb +26 -19
  13. data/lib/MrMurano/Gateway.rb +51 -10
  14. data/lib/MrMurano/ReCommander.rb +1 -1
  15. data/lib/MrMurano/Solution-Services.rb +80 -35
  16. data/lib/MrMurano/Solution-Users.rb +1 -0
  17. data/lib/MrMurano/SyncRoot.rb +10 -3
  18. data/lib/MrMurano/SyncUpDown-Core.rb +47 -36
  19. data/lib/MrMurano/SyncUpDown-Item.rb +46 -14
  20. data/lib/MrMurano/SyncUpDown.rb +22 -20
  21. data/lib/MrMurano/Webservice-Endpoint.rb +20 -18
  22. data/lib/MrMurano/Webservice-File.rb +63 -20
  23. data/lib/MrMurano/commands/business.rb +14 -1
  24. data/lib/MrMurano/commands/child.rb +148 -0
  25. data/lib/MrMurano/commands/devices.rb +298 -149
  26. data/lib/MrMurano/commands/element.rb +2 -1
  27. data/lib/MrMurano/commands/globals.rb +3 -0
  28. data/lib/MrMurano/commands/network.rb +152 -33
  29. data/lib/MrMurano/commands/sync.rb +2 -2
  30. data/lib/MrMurano/commands.rb +1 -0
  31. data/lib/MrMurano/verbosing.rb +13 -2
  32. data/lib/MrMurano/version.rb +1 -1
  33. data/spec/Account_spec.rb +43 -11
  34. data/spec/Content_spec.rb +5 -3
  35. data/spec/GatewayBase_spec.rb +1 -1
  36. data/spec/GatewayDevice_spec.rb +47 -8
  37. data/spec/GatewayResource_spec.rb +1 -1
  38. data/spec/GatewaySettings_spec.rb +1 -1
  39. data/spec/HttpAuthed_spec.rb +17 -3
  40. data/spec/ProjectFile_spec.rb +59 -23
  41. data/spec/Setting_spec.rb +2 -1
  42. data/spec/Solution-ServiceConfig_spec.rb +1 -1
  43. data/spec/Solution-ServiceEventHandler_spec.rb +27 -20
  44. data/spec/Solution-ServiceModules_spec.rb +7 -5
  45. data/spec/Solution-UsersRoles_spec.rb +7 -1
  46. data/spec/Solution_spec.rb +9 -1
  47. data/spec/SyncRoot_spec.rb +5 -5
  48. data/spec/SyncUpDown_spec.rb +262 -211
  49. data/spec/Verbosing_spec.rb +49 -8
  50. data/spec/Webservice-Cors_spec.rb +10 -1
  51. data/spec/Webservice-Endpoint_spec.rb +84 -65
  52. data/spec/Webservice-File_spec.rb +16 -11
  53. data/spec/Webservice-Setting_spec.rb +7 -1
  54. data/spec/_workspace.rb +9 -0
  55. data/spec/cmd_business_spec.rb +5 -10
  56. data/spec/cmd_common.rb +67 -32
  57. data/spec/cmd_config_spec.rb +9 -14
  58. data/spec/cmd_content_spec.rb +15 -26
  59. data/spec/cmd_cors_spec.rb +9 -12
  60. data/spec/cmd_device_spec.rb +31 -45
  61. data/spec/cmd_domain_spec.rb +12 -10
  62. data/spec/cmd_element_spec.rb +18 -17
  63. data/spec/cmd_exchange_spec.rb +1 -4
  64. data/spec/cmd_init_spec.rb +56 -72
  65. data/spec/cmd_keystore_spec.rb +17 -26
  66. data/spec/cmd_link_spec.rb +13 -17
  67. data/spec/cmd_password_spec.rb +9 -10
  68. data/spec/cmd_setting_application_spec.rb +95 -68
  69. data/spec/cmd_setting_product_spec.rb +59 -37
  70. data/spec/cmd_status_spec.rb +46 -84
  71. data/spec/cmd_syncdown_application_spec.rb +28 -50
  72. data/spec/cmd_syncdown_both_spec.rb +44 -93
  73. data/spec/cmd_syncdown_unit_spec.rb +858 -0
  74. data/spec/cmd_syncup_spec.rb +21 -56
  75. data/spec/cmd_token_spec.rb +0 -3
  76. data/spec/cmd_usage_spec.rb +15 -10
  77. data/spec/dry_run_formatter.rb +1 -0
  78. data/spec/fixtures/dumped_config +4 -4
  79. data/spec/spec_helper.rb +3 -0
  80. metadata +4 -2
@@ -17,6 +17,7 @@ require 'MrMurano/Config'
17
17
  require 'MrMurano/SolutionId'
18
18
  require 'MrMurano/SyncRoot'
19
19
  require 'MrMurano/SyncUpDown'
20
+ require 'MrMurano/SyncUpDown-Item'
20
21
 
21
22
  module MrMurano
22
23
  ## The details of talking to the Gateway [Device2] service.
@@ -114,6 +115,33 @@ module MrMurano
114
115
  class Resources < GweBase
115
116
  include SyncUpDown
116
117
 
118
+ class GatewayItem < Item
119
+ # @return [String] The type of data stored in aliases for this resource.
120
+ # (string|boolean|number)
121
+ attr_accessor :format
122
+ # @return [String] Helpful unit description for the alias.
123
+ attr_accessor :unit
124
+ # @return [Boolean] List of data format validations.
125
+ attr_accessor :settable
126
+ # @return [Array] True if the cloud can write to this.
127
+ attr_accessor :allowed
128
+ # @return [String] The resource alias.
129
+ attr_accessor :alias
130
+
131
+ def reject_ephemeral!
132
+ super.reject do |attr_key, _|
133
+ [
134
+ # Server-siders:
135
+ # :format,
136
+ # :unit,
137
+ # :settable,
138
+ # :allowed,
139
+ # :alias,
140
+ ].include? attr_key
141
+ end
142
+ end
143
+ end
144
+
117
145
  def initialize
118
146
  super
119
147
  @itemkey = :alias
@@ -132,7 +160,7 @@ module MrMurano
132
160
  # convert hash to array.
133
161
  res = []
134
162
  ret[:resources].each_pair do |key, value|
135
- res << value.merge(alias: key.to_s)
163
+ res << GatewayItem.new(value.merge(alias: key.to_s))
136
164
  end
137
165
  res
138
166
  # MAYBE/2017-08-17:
@@ -144,7 +172,8 @@ module MrMurano
144
172
  res = {}
145
173
  data.each do |value|
146
174
  key = value[:alias]
147
- res[key] = value.reject { |k, _v| k == :alias }
175
+ # (lb): Item has reject method, but convert to Hash, for patch.
176
+ res[key] = value.to_h.reject { |k, _v| k == :alias }
148
177
  end
149
178
 
150
179
  patch('', resources: res)
@@ -194,8 +223,6 @@ module MrMurano
194
223
 
195
224
  def syncdown_before
196
225
  super
197
- # TEST/2017-07-02: Could there be duplicate gateway items?
198
- # [lb] just added code to SyncUpDown.locallist and is curious.
199
226
  @here = locallist
200
227
  end
201
228
 
@@ -247,10 +274,11 @@ module MrMurano
247
274
  def resources_write(file_path)
248
275
  num_synced = 0
249
276
  # User can blow away specs/ directory if they want; we'll just make
250
- # a new one. [This code somewhat copy-paste from make_directory.]
277
+ # a new one. [This code is somewhat copy-paste from make_directory.]
251
278
  basedir = file_path
252
279
  basedir = basedir.dirname unless basedir.extname.empty?
253
- raise 'Unexpected: bad basedir' if basedir.to_s.empty? || basedir == File::SEPARATOR
280
+ bad_basedir = basedir.to_s.empty? || basedir == File::SEPARATOR
281
+ raise 'Unexpected: bad basedir' if bad_basedir
254
282
 
255
283
  unless basedir.exist?
256
284
  if $cfg['tool.dry']
@@ -277,7 +305,7 @@ module MrMurano
277
305
  @here.each do |value|
278
306
  key = value[:alias]
279
307
  res[key] = Hash.transform_keys_to_strings(
280
- value.reject { |k, _v| k == :alias }
308
+ value.to_h.reject { |k, _v| k == :alias }
281
309
  )
282
310
  end
283
311
  ohash = ordered_hash(res)
@@ -334,7 +362,9 @@ module MrMurano
334
362
 
335
363
  res = []
336
364
  here.each_pair do |key, value|
337
- res << Hash.transform_keys_to_symbols(value).merge(alias: key.to_s)
365
+ hash = Hash.transform_keys_to_symbols(value).merge(alias: key.to_s)
366
+ item = GatewayItem.new(hash)
367
+ res << item
338
368
  end
339
369
 
340
370
  sort_by_name(res)
@@ -398,8 +428,17 @@ module MrMurano
398
428
  # @option opts [String,Pathname,IO] :key Shared secret for hash, password, token types;
399
429
  # or public key for certificate auth type.
400
430
  # May be string or IO/Pathname to file.
401
- # @option opts [String] :type One of: certificate, hash, password, signature, token
402
- DEVICE_AUTH_TYPES = %i[certificate hash password signature token].freeze
431
+ # @option opts [String] :type One of: DEVICE_AUTH_TYPES.
432
+
433
+ DEVICE_AUTH_TYPES = %i[
434
+ certificate
435
+ hash
436
+ password
437
+ signature
438
+ token
439
+ csr
440
+ ].freeze
441
+
403
442
  def enable(id, opts=nil)
404
443
  opts = {} if opts.nil?
405
444
  props = { auth: {}, locked: false }
@@ -421,6 +460,8 @@ module MrMurano
421
460
  props[:auth][:type] = opts[:type]
422
461
  end
423
462
  unless opts[:key].nil?
463
+ # 2018-07-10: (lb): I think the `read` feature is no longer used
464
+ # (no callers pass in a File object any more).
424
465
  props[:auth][:key] = opts[:key].is_a?(String) && opts[:key] || opts[:key].read
425
466
  props[:auth][:type] = :certificate if props[:auth][:type].nil?
426
467
  end
@@ -370,7 +370,7 @@ module Commander
370
370
  def verify_solutions_unmanaged!
371
371
  return if $cfg['tool.skip-managed']
372
372
  # (lb): All @exosite.com employees are welcome behind the curtain.
373
- if $cfg['user.name'] && $cfg['user.name'].end_with?("@exosite.com")
373
+ if $cfg['user.name'] && $cfg['user.name'].end_with?('@exosite.com')
374
374
  MrMurano::Verbose.verbose(
375
375
  "Welcome behind the curtain, #{$cfg['user.name']}!"
376
376
  )
@@ -53,13 +53,14 @@ module MrMurano
53
53
  end
54
54
  end
55
55
 
56
- # ??? remove
57
- def remove(name)
58
- return unless remove_item_allowed(name)
59
- delete('/' + name)
56
+ def remove(itemkey)
57
+ return unless remove_item_allowed(itemkey)
58
+ delete('/' + itemkey)
60
59
  end
61
60
 
62
- def remove_lite(_name, item, modify=false)
61
+ def remove_or_clear(itemkey, item, modify=false)
62
+ # Note that item.phantom, well, it did, until we called reject_ephemeral!.
63
+ return unless remove_item_allowed(itemkey)
63
64
  # This is a :phantom item, which has a default script, e.g.,
64
65
  # item[:script] => "--#EVENT timer timer\n"
65
66
  localpath = tolocalpath(location, item)
@@ -71,7 +72,6 @@ module MrMurano
71
72
  def upload(localpath, thereitem, _modify=false)
72
73
  localpath = Pathname.new(localpath) unless localpath.is_a?(Pathname)
73
74
  if localpath.exist?
74
- # we assume these are small enough to slurp.
75
75
  script = localpath.read
76
76
  else
77
77
  # I.e., thereitem.phantom, an "undeletable" file that does not
@@ -84,18 +84,19 @@ module MrMurano
84
84
  script = config_vars_decode(script)
85
85
 
86
86
  name = mkname(thereitem)
87
- pst = thereitem.to_h.merge(
87
+ req_body = thereitem.to_h.merge(
88
88
  solution_id: @api_id,
89
89
  script: script,
90
90
  alias: mkalias(thereitem),
91
91
  name: name,
92
92
  )
93
- debug "f: #{localpath} >> #{pst.reject { |k, _| k == :script }.to_json}"
93
+
94
+ debug "f: #{localpath} >> #{req_body.reject { |k, _| k == :script }.to_json}"
94
95
  therealias = mkalias(thereitem)
95
- upload_script(therealias, name, localpath, pst)
96
+ upload_script(therealias, name, localpath, req_body)
96
97
  end
97
98
 
98
- def upload_script(therealias, name, localpath, pst)
99
+ def upload_script(therealias, name, localpath, req_body)
99
100
  # Try PUT. If 404, then POST.
100
101
  # I.e., PUT if not exists, else POST to create.
101
102
  updated_at = nil
@@ -105,7 +106,7 @@ module MrMurano
105
106
  get_again = false
106
107
  create_it = false
107
108
 
108
- put('/' + therealias, pst) do |request, http|
109
+ put('/' + therealias, req_body) do |request, http|
109
110
  response = http.request(request)
110
111
  isj, jsn = isJSON(response.body)
111
112
  # ORDER: An HTTPNoContent is also a HTTPSuccess, so the latter comes first.
@@ -131,7 +132,10 @@ module MrMurano
131
132
  create_it = true
132
133
  else
133
134
  relpath = localpath.sub(File.join(Dir.pwd, ''), '')
134
- if response.is_a?(Net::HTTPBadRequest) && isj && jsn[:message] == 'Validation errors'
135
+ if (
136
+ response.is_a?(Net::HTTPBadRequest) &&
137
+ isj && jsn[:message] == 'Validation errors'
138
+ )
135
139
  warning "Validation errors detected in #{relpath}:"
136
140
  puts MrMurano::Pretties.makeJsonPretty(
137
141
  jsn[:errors], Struct.new(:pretty).new(true),
@@ -141,6 +145,7 @@ module MrMurano
141
145
  end
142
146
  warning "Failed to upload: #{relpath}"
143
147
  end
148
+ response
144
149
  end
145
150
  if get_again
146
151
  ret = get('/' + CGI.escape(name))
@@ -150,7 +155,7 @@ module MrMurano
150
155
  warning "Failed to verify updated_at: #{ret}"
151
156
  end
152
157
  end
153
- post('/', pst) if create_it
158
+ post('/', req_body) if create_it
154
159
  cache_update_time_for(localpath, updated_at)
155
160
  end
156
161
 
@@ -285,12 +290,20 @@ module MrMurano
285
290
  class ModuleItem < Item
286
291
  # @return [String] Internal Alias name
287
292
  attr_accessor :alias
288
- # @return [String] Timestamp when this was updated.
289
- attr_accessor :updated_at
290
293
  # @return [String] Timestamp when this was created.
291
294
  attr_accessor :created_at
292
295
  # @return [String] The application solution's ID.
293
296
  attr_accessor :solution_id
297
+
298
+ def reject_ephemeral!
299
+ super.reject do |attr_key, _|
300
+ [
301
+ :alias,
302
+ :created_at,
303
+ :solution_id,
304
+ ].include? attr_key
305
+ end
306
+ end
294
307
  end
295
308
 
296
309
  def initialize(api_id=nil)
@@ -335,13 +348,14 @@ module MrMurano
335
348
  # sort_by_name(ret[:items])
336
349
  end
337
350
 
338
- def to_remote_item(root, path)
351
+ def to_remote_items(root, path)
339
352
  if $cfg['modules.no-nesting']
340
353
  name = path.basename.to_s.sub(/\..*/, '')
341
354
  else
342
355
  name = remote_item_nested_name(root, path)
343
356
  end
344
- ModuleItem.new(name: name)
357
+ item = ModuleItem.new(name: name)
358
+ [item]
345
359
  end
346
360
 
347
361
  def remote_item_nested_name(root, path)
@@ -349,14 +363,16 @@ module MrMurano
349
363
  root = root.expand_path
350
364
  if path.basename.sub(/\.lua$/i, '').to_s.include?('.')
351
365
  warning(
352
- "WARNING: Do not use periods in filenames. Rename: #{fancy_ticks(path.basename)}"
366
+ "WARNING: Do not use periods in filenames." +
367
+ " Rename: #{fancy_ticks(path.basename)}"
353
368
  )
354
369
  end
355
370
  path.dirname.ascend do |ancestor|
356
371
  break if ancestor == root
357
372
  if ancestor.basename.to_s.include?('.')
358
373
  warning(
359
- "WARNING: Do not use periods in directory names. Rename: #{fancy_ticks(ancestor.basename)}"
374
+ "WARNING: Do not use periods in directory names." +
375
+ " Rename: #{fancy_ticks(ancestor.basename)}"
360
376
  )
361
377
  end
362
378
  end
@@ -401,6 +417,24 @@ module MrMurano
401
417
  attr_accessor :phantom
402
418
  # @return [Boolean] True if a service that should not be deleted remotely.
403
419
  attr_accessor :undeletable
420
+
421
+ def reject_ephemeral!
422
+ super.reject do |attr_key, attr_val|
423
+ [
424
+ # Server-siders:
425
+ # :alias,
426
+ # :service,
427
+ # :event,
428
+ # :type,
429
+ :updated_at,
430
+ :created_at,
431
+ :solution_id,
432
+ :svc_alias,
433
+ :phantom,
434
+ :undeletable,
435
+ ].include?(attr_key) || ((attr_key == :type) && (attr_val.nil?))
436
+ end
437
+ end
404
438
  end
405
439
 
406
440
  def initialize(api_id=nil)
@@ -413,13 +447,15 @@ module MrMurano
413
447
  end
414
448
 
415
449
  def mkalias(remote)
416
- raise "Missing parts! #{remote.to_h.to_json}" if remote.service.nil? || remote.event.nil?
450
+ missing_parts = remote.service.nil? || remote.event.nil?
451
+ raise "Missing parts! #{remote.to_h.to_json}" if missing_parts
417
452
  #[$cfg[@solntype], remote[:service], remote[:event]].join('_')
418
453
  [@api_id, remote[:service], remote[:event]].join('_')
419
454
  end
420
455
 
421
456
  def mkname(remote)
422
- raise "Missing parts! #{remote.to_h.to_json}" if remote.service.nil? || remote.event.nil?
457
+ missing_parts = remote.service.nil? || remote.event.nil?
458
+ raise "Missing parts! #{remote.to_h.to_json}" if missing_parts
423
459
  [remote[:service], remote[:event]].join('_')
424
460
  end
425
461
 
@@ -449,10 +485,18 @@ module MrMurano
449
485
  # Substitute '{product.id}' for the actual product.id if a corresponding
450
486
  # local file does not exist, or if the local file already uses the alias.
451
487
  encode_items = {}
488
+ build_svc_alias_map_from_local(encode_items, local)
489
+ resolve_name_svc_alias!(encode_items, there)
490
+ end
491
+
492
+ def build_svc_alias_map_from_local(encode_items, local)
452
493
  local.each do |item|
453
494
  encode_items[item.service] = {} if encode_items[item.service].nil?
454
495
  encode_items[item.service][item.event] = item.svc_alias
455
496
  end
497
+ end
498
+
499
+ def resolve_name_svc_alias!(encode_items, there)
456
500
  there.map! do |item|
457
501
  if (
458
502
  !encode_items[item.service].nil? &&
@@ -536,21 +580,20 @@ module MrMurano
536
580
  "#{item[:name]}.lua"
537
581
  end
538
582
 
539
- def to_remote_item(from, path)
540
- # This allows multiple events to be in the same file. This is a lie.
541
- # This only finds the last event in a file.
542
- # :legacy support doesn't allow for that. But that's ok.
543
- path = Pathname.new(path) unless path.is_a?(Pathname)
583
+ def to_remote_items(from, path)
584
+ # Parses file which may contain multiple events. Return array of Items.
544
585
  items = []
545
586
  cur = nil
546
587
  lineno = 0
588
+ path = Pathname.new(path) unless path.is_a?(Pathname)
547
589
  path.readlines.each do |line|
548
590
  lineno += 1
549
591
  # @match_header finds a service and an event string, e.g., "--EVENT svc evt\n"
550
592
  md = @match_header.match(line)
551
593
  if !md.nil?
552
594
  cur[:line_end] = lineno - 1 unless cur.nil?
553
- cur = to_remote_item_create(md, path, line, lineno)
595
+ header = line.strip
596
+ cur = to_remote_item_create(md, path, header, lineno)
554
597
  items << cur
555
598
  elsif !cur.nil? && !cur[:script].nil?
556
599
  cur[:script] += line
@@ -563,22 +606,23 @@ module MrMurano
563
606
  else
564
607
  # If cur is nil here, then we need to do a :legacy check.
565
608
  cur = to_remote_item_legacy_check(cur, path, from, lineno)
566
- items << cur
609
+ items << cur unless cur.nil?
567
610
  end
568
611
 
569
612
  items
570
613
  end
571
614
 
572
- def to_remote_item_create(md, path, line, lineno)
615
+ def to_remote_item_create(md, path, header, lineno)
573
616
  service, config_var = decode_config_var(md[:service])
574
617
  event_event, event_type = resolve_event_type(service, md[:event])
575
- # Header line.
618
+ header = config_vars_decode(header)
576
619
  svc_alias = config_var if service != md[:service]
577
620
  EventHandlerItem.new(
578
- # Skip: name, id, line_end, diff, selected, synckey, synctype, type,
579
- # updated_at, dup_count, alias, updted_at, created_at, solution_id, phantom.
621
+ # Skipping: alias, updated_at, created_at, solution_id, phantom, undeletable
622
+ # name, id, line_end, diff, selected, synckey, synctype, updated_at, dup_count
580
623
  local_path: path,
581
- script: line,
624
+ header: header,
625
+ script: '',
582
626
  line_beg: lineno,
583
627
  service: service,
584
628
  event: event_event,
@@ -751,9 +795,10 @@ module MrMurano
751
795
  undeletable.updated_at = nil
752
796
  # Even if the user deletes the contents of a script,
753
797
  # the platform still sends the magic header.
754
- undeletable.script = (
755
- "--#EVENT #{therebox[key].service} #{therebox[key].event}\n"
798
+ undeletable.header = (
799
+ "--#EVENT #{therebox[key].service} #{therebox[key].event}"
756
800
  )
801
+ undeletable.script = ''
757
802
  undeletable.local_path = Pathname.new(
758
803
  File.join(location, tolocalname(thereitem, key))
759
804
  )
@@ -115,6 +115,7 @@ module MrMurano
115
115
 
116
116
  here.map! { |i| Hash.transform_keys_to_symbols(i) }
117
117
 
118
+ # (lb): FIXME?: To be like other localitems methods, should return Item list, not Hash.
118
119
  sort_by_name(here)
119
120
  end
120
121
  end
@@ -57,9 +57,16 @@ module MrMurano
57
57
 
58
58
  ##
59
59
  # Remove all syncables.
60
- def reset
61
- @syncset = []
62
- @synctypes = {}
60
+ def reset(syncsetypes=nil)
61
+ new_syncsetypes = [@syncset, @synctypes]
62
+ if syncsetypes.nil?
63
+ @syncset = []
64
+ @synctypes = {}
65
+ else
66
+ @syncset = syncsetypes[0]
67
+ @synctypes = syncsetypes[1]
68
+ end
69
+ new_syncsetypes
63
70
  end
64
71
 
65
72
  ##
@@ -51,21 +51,28 @@ module MrMurano
51
51
  tomod = dt[:tomod]
52
52
 
53
53
  itemkey = @itemkey.to_sym
54
- todel.each do |item|
54
+
55
+ todel.reject { |item| item[:phantom] }.each do |item|
55
56
  syncup_item(item, options, :delete, 'Removing') do |aitem|
56
- remove_lite(aitem[itemkey], aitem.reject { |k, _v| k == :local_path }, true)
57
+ remove(aitem[itemkey])
58
+ num_synced += 1
59
+ end
60
+ end
61
+ todel.select { |item| item[:phantom] }.each do |item|
62
+ syncup_item(item, options, :delete, 'Clearing') do |aitem|
63
+ remove_or_clear(aitem[itemkey], aitem.reject_ephemeral!, true)
57
64
  num_synced += 1
58
65
  end
59
66
  end
60
67
  toadd.each do |item|
61
68
  syncup_item(item, options, :create, 'Adding') do |aitem|
62
- upload(aitem[:local_path], aitem.reject { |k, _v| k == :local_path }, false)
69
+ upload(aitem[:local_path], aitem.reject_ephemeral!, false)
63
70
  num_synced += 1
64
71
  end
65
72
  end
66
73
  tomod.each do |item|
67
74
  syncup_item(item, options, :update, 'Updating') do |aitem|
68
- upload(aitem[:local_path], aitem.reject { |k, _v| k == :local_path }, true)
75
+ upload(aitem[:local_path], aitem.reject_ephemeral!, true)
69
76
  num_synced += 1
70
77
  end
71
78
  end
@@ -163,7 +170,7 @@ module MrMurano
163
170
  def dodiff(merged, local, _there=nil, options={})
164
171
  localname = dodiff_resolve_localname(merged)
165
172
  trmt, tlcl = dodiff_tempfile_paths(localname)
166
- dodiff_header_aware(merged, trmt, tlcl, options)
173
+ dodiff_header_aware(merged, local, trmt, tlcl, options)
167
174
  end
168
175
 
169
176
  def dodiff_resolve_localname(merged)
@@ -186,10 +193,10 @@ module MrMurano
186
193
  raise
187
194
  end
188
195
 
189
- def dodiff_header_aware(merged, trmt, tlcl, options)
196
+ def dodiff_header_aware(merged, local, trmt, tlcl, options)
190
197
  dodiff_download_remote(merged, trmt, options)
191
198
  MrMurano::Verbose.whirly_stop
192
- dodiff_prepare_local_and_diff(merged, trmt, tlcl, options)
199
+ dodiff_prepare_local_and_diff(merged, local, trmt, tlcl, options)
193
200
  ensure
194
201
  trmt.close
195
202
  trmt.unlink
@@ -202,13 +209,9 @@ module MrMurano
202
209
  diff_download(tmp_path, merged, options)
203
210
  end
204
211
 
205
- def dodiff_prepare_local_and_diff(merged, trmt, tlcl, options)
212
+ def dodiff_prepare_local_and_diff(merged, local, trmt, tlcl, options)
206
213
  cmd = dodiff_build_cmd(trmt, tlcl, options)
207
- merged[:exclude_header] = true
208
- outp = dodiff_flexible(merged, tlcl, local, cmd, use_header=false)
209
- return outp if outp.to_s.empty? || !merged.key?(:script)
210
- merged[:exclude_header] = false
211
- dodiff_diff_dynamic(merged, tlcl, local, cmd, use_header=true)
214
+ dodiff_flexible(merged, local, tlcl, cmd, use_header: true)
212
215
  end
213
216
 
214
217
  def dodiff_build_cmd(trmt, tlcl, options)
@@ -232,18 +235,22 @@ module MrMurano
232
235
  cmd
233
236
  end
234
237
 
235
- def dodiff_flexible(merged, tlcl, local, cmd, use_header)
236
- dodiff_local_to_tempfile(merged, tlcl, local, use_header)
238
+ def dodiff_flexible(merged, local, tlcl, cmd, use_header: true)
239
+ dodiff_local_to_tempfile(merged, local, tlcl, use_header: use_header)
237
240
  dodiff_do_diff(cmd)
238
241
  end
239
242
 
240
- def dodiff_local_to_tempfile(merged, tlcl, local, use_header)
243
+ def dodiff_local_to_tempfile(merged, local, tlcl, use_header: true)
241
244
  Pathname.new(tlcl.path).open('wb') do |io|
242
245
  # Copy the local file to a temp file, for the diff command.
243
246
  # And for resources, remove the local-only :selected key, as
244
247
  # it's not part of the remote item that gets downloaded next.
245
248
  if merged.key?(:script)
246
- io << config_vars_decode(merged[:header]) if use_header
249
+ if use_header && !merged[:header].to_s.empty?
250
+ io << config_vars_decode(merged[:header])
251
+ # Add newline after the :header.
252
+ io.puts
253
+ end
247
254
  io << config_vars_decode(merged[:script])
248
255
  else
249
256
  # For most items, read the local file.
@@ -379,17 +386,7 @@ module MrMurano
379
386
 
380
387
  items_cull_clashes!(statuses)
381
388
 
382
- # 2018-04-24: (lb): How did I not see this til now? :unselected is :notimplemented.
383
- unless options[:unselected]
384
- statuses.each do |bucket, items|
385
- select_selected(items)
386
- next unless $cfg['tool.debug']
387
- loci = items.map { |item| item.location_friendly(full_path: true) }
388
- selected_paths = loci.sort.join("\n ")
389
- selected_paths = (selected_paths.empty? && ' <none>' || "\n ") + selected_paths
390
- debug %(#{self.class}: Selected #{bucket} items:#{selected_paths})
391
- end
392
- end
389
+ item_select_selected!(statuses, options)
393
390
 
394
391
  statuses
395
392
  end
@@ -631,14 +628,6 @@ module MrMurano
631
628
  end
632
629
  end
633
630
 
634
- def select_selected(items)
635
- items.select! { |item| item[:selected] }
636
- items.map do |item|
637
- item.delete(:selected)
638
- item
639
- end
640
- end
641
-
642
631
  def items_cull_clashes!(statuses)
643
632
  clash = []
644
633
  statuses.each_value do |items|
@@ -656,6 +645,28 @@ module MrMurano
656
645
  end
657
646
  statuses[:clash] = clash
658
647
  end
648
+
649
+ def item_select_selected!(statuses, options)
650
+ # (lb): :unselected is currently only used by tests.
651
+ return if options[:unselected]
652
+ statuses.each do |bucket, items|
653
+ select_selected!(items)
654
+ debug_selected(bucket, items)
655
+ end
656
+ end
657
+
658
+ def select_selected!(items)
659
+ items.select! { |item| item[:selected] }
660
+ items.map { |item| item.delete(:selected) }
661
+ end
662
+
663
+ def debug_selected(bucket, items)
664
+ return unless $cfg['tool.debug']
665
+ loci = items.map { |item| item.location_friendly(full_path: true) }
666
+ selected_paths = loci.sort.join("\n ")
667
+ selected_paths = (selected_paths.empty? && ' <none>' || "\n ") + selected_paths
668
+ debug %(#{self.class}: Selected #{bucket} items:#{selected_paths})
669
+ end
659
670
  end
660
671
  end
661
672